diff --git a/__tests__/unit/mocks/sdkManager.ts b/__tests__/unit/mocks/sdkManager.ts index 406f55340..df58d3298 100644 --- a/__tests__/unit/mocks/sdkManager.ts +++ b/__tests__/unit/mocks/sdkManager.ts @@ -2,11 +2,10 @@ import { SdkManager } from '@internxt-mobile/services/common/sdk/SdkManager'; export const SdkManagerMock: SdkManager = { getApiSecurity: jest.fn(), - auth: jest.fn()(), authV2: jest.fn()(), - users: jest.fn()(), + usersV2: jest.fn()(), + usersV2WithoutToken: jest.fn()(), payments: jest.fn()(), - storage: jest.fn()(), storageV2: jest.fn()(), share: jest.fn()(), trash: jest.fn()(), diff --git a/android/app/build.gradle b/android/app/build.gradle index 0c3e734e9..40d4c69c5 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -88,8 +88,8 @@ android { applicationId 'com.internxt.cloud' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 103 - versionName "1.7.2" + versionCode 106 + versionName "1.8.0" buildConfigField("boolean", "REACT_NATIVE_UNSTABLE_USE_RUNTIME_SCHEDULER_ALWAYS", (findProperty("reactNative.unstable_useRuntimeSchedulerAlways") ?: true).toString()) } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index cdc2e15b8..237bf46d8 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ - + @@ -15,7 +16,7 @@ - + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 678adabe5..58b15c564 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -2,5 +2,6 @@ Internxt cover false - 1.7.2 + 1.8.0 + automatic \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties index 252298523..40197d18b 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -55,4 +55,5 @@ EX_DEV_CLIENT_NETWORK_INSPECTOR=true # Use legacy packaging to compress native libraries in the resulting APK. expo.useLegacyPackaging=false -android.compileSdkVersion=34 \ No newline at end of file +android.compileSdkVersion=34 +expo.jsEngine=hermes \ No newline at end of file diff --git a/app.config.ts b/app.config.ts index 78c470b4e..5e9fc775f 100644 --- a/app.config.ts +++ b/app.config.ts @@ -25,6 +25,7 @@ const appConfig: ExpoConfig & { extra: AppEnv & { NODE_ENV: AppStage; RELEASE_ID resizeMode: 'cover', backgroundColor: '#091e42', }, + userInterfaceStyle: 'automatic', updates: { url: 'https://u.expo.dev/680f4feb-6315-4a50-93ec-36dcd0b831d2', diff --git a/assets/lang/strings.ts b/assets/lang/strings.ts index f25f04e02..64881f1be 100644 --- a/assets/lang/strings.ts +++ b/assets/lang/strings.ts @@ -193,7 +193,8 @@ const strings = new LocalizedStrings({ title: 'Folder is empty', message: 'Tap the + button to upload a file or create something new', }, - searchInThisFolder: 'Search in this folder', + searchInThisFolder: 'Search items in this folder', + searchInAllFolders: 'Search all my files', encrypting: 'Encrypting', decrypting: 'Decrypting', downloadingPercent: 'Downloading... {0}%', @@ -256,6 +257,8 @@ const strings = new LocalizedStrings({ legal: 'Legal', debug: 'Debug', signOut: 'Log out', + darkMode: 'Dark mode', + darkModeDescription: 'Change the app theme', }, AccountScreen: { title: 'Account', @@ -297,7 +300,7 @@ const strings = new LocalizedStrings({ }, twoFactor: { title: 'Two factor authentication (2FA)', - text: 'Two-factor authentication provides an extra layer of security by requiring an extra verification when you log in. In adittion to your password, you’ll also need a generated code.', + text: 'Two-factor authentication provides an extra layer of security by requiring an extra verification when you log in. In addition to your password, you’ll also need a generated code.', enable: 'Enable 2FA', disable: 'Disable 2FA', }, @@ -444,6 +447,23 @@ const strings = new LocalizedStrings({ }, }, modals: { + GlobalSearchModal: { + searchPlaceholder: 'Search all my files', + searching: 'Searching...', + noResultsTitle: 'No results found', + noResultsMessage: 'Try a different search', + searchPromptTitle: 'Search files and folders', + searchPromptMessage: 'Type to start your search', + clear: 'Clear', + opening: 'Opening...', + searchError: 'Search error. Try again.', + tryAgainButton: 'Try again', + }, + duplicatedFiles: { + duplicateFilesTitle: 'Duplicate Files Found', + duplicateFilesMessage: 'The following files already exist: %s\n\nDo you want to upload them with a new name?', + duplicateFilesAction: 'Upload with new name', + }, rename: { title: 'Rename', label: 'Name', @@ -896,6 +916,7 @@ const strings = new LocalizedStrings({ message: 'Prueba a subir un archivo o crear una carpeta', }, searchInThisFolder: 'Buscar en esta carpeta', + searchInAllFolders: 'Buscar en todos mis archivos', encrypting: 'Encriptando', decrypting: 'Desencriptando', downloadingPercent: 'Descargando... {0}%', @@ -958,6 +979,8 @@ const strings = new LocalizedStrings({ legal: 'Legal', debug: 'Debug', signOut: 'Cerrar sesión', + darkMode: 'Modo oscuro', + darkModeDescription: 'Cambiar apariencia de la aplicación', }, AccountScreen: { title: 'Cuenta', @@ -1143,6 +1166,23 @@ const strings = new LocalizedStrings({ }, }, modals: { + GlobalSearchModal: { + searchPlaceholder: 'Buscar en todos mis archivos', + searching: 'Buscando...', + noResultsTitle: 'No se encontraron resultados', + noResultsMessage: 'Intenta con una búsqueda diferente', + searchPromptTitle: 'Buscar archivos y carpetas', + searchPromptMessage: 'Escribe para comenzar tu búsqueda', + clear: 'Limpiar', + opening: 'Abriendo...', + searchError: 'Error al buscar. Inténtalo de nuevo.', + tryAgainButton: 'Intentar de nuevo', + }, + duplicatedFiles: { + duplicateFilesTitle: 'Archivos duplicados encontrados', + duplicateFilesMessage: 'Los siguientes archivos ya existen: %s\n\n¿Quieres subirlos con un nuevo nombre?', + duplicateFilesAction: 'Subir con nuevo nombre', + }, rename: { title: 'Renombrar', label: 'Nombre', diff --git a/ios/Internxt/Info.plist b/ios/Internxt/Info.plist index e7ad9773b..c932f54da 100644 --- a/ios/Internxt/Info.plist +++ b/ios/Internxt/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.7.2 + 1.8.0 CFBundleSignature ???? CFBundleURLTypes @@ -78,7 +78,7 @@ UIInterfaceOrientationLandscapeRight UIUserInterfaceStyle - Light + Automatic UIViewControllerBasedStatusBarAppearance diff --git a/ios/Internxt/Supporting/Expo.plist b/ios/Internxt/Supporting/Expo.plist index c85a80f68..6fa3e65fc 100644 --- a/ios/Internxt/Supporting/Expo.plist +++ b/ios/Internxt/Supporting/Expo.plist @@ -9,7 +9,7 @@ EXUpdatesLaunchWaitMs 0 EXUpdatesRuntimeVersion - 1.7.2 + 1.8.0 EXUpdatesURL https://u.expo.dev/680f4feb-6315-4a50-93ec-36dcd0b831d2 diff --git a/package.json b/package.json index 3c7c79d1a..9dadf34cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "drive-mobile", - "version": "v1.7.2", + "version": "v1.8.0", "private": true, "license": "GNU", "scripts": { @@ -37,7 +37,7 @@ "@internxt/lib": "^1.2.0", "@internxt/mobile-sdk": "^0.2.41", "@internxt/rn-crypto": "0.1.15", - "@internxt/sdk": "^1.4.96", + "@internxt/sdk": "1.9.26", "@react-native-async-storage/async-storage": "1.21.0", "@react-navigation/bottom-tabs": "^6.2.0", "@react-navigation/native": "^6.1.18", @@ -167,6 +167,7 @@ "jest-expo": "~50.0.4", "metro-react-native-babel-transformer": "^0.77.0", "patch-package": "^7.0.0", + "path": "^0.12.7", "postcss": "^8.2.15", "prettier": "^2.3.2", "react-native-svg-transformer": "^0.14.3", diff --git a/src/@inxt-js/services/share.ts b/src/@inxt-js/services/share.ts deleted file mode 100644 index 8a3e8b241..000000000 --- a/src/@inxt-js/services/share.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { constants } from '../../services/AppService'; - -interface GenerateShareLinkResponse { - token: string; -} - -interface GenerateShareLinkRequestBody { - isFolder: boolean; - views: number; - encryptionKey: string; - fileToken: string; - bucket: string; -} - -interface GetShareInfoResponse { - user: string; - token: string; - file: string; - encryptionKey: string; - mnemonic: string; - isFolder: boolean; - views: number; - bucket: string; - fileToken: string; - fileMeta: { - folderId: string; - name: string; - type: string; - size: number; - }; -} - -/** - * @deprecated use drive.share instead - * - * Creates a share link token - * - * @param headers headers to be included in the request - * @param fileId File id to create the token for - * @param params - * @returns The generated share link token - */ -export function generateShareLinkToken( - headers: Headers, - fileId: string, - params: GenerateShareLinkRequestBody, -): Promise { - return fetch(`${constants.DRIVE_API_URL}/storage/share/file/${fileId}`, { - method: 'POST', - headers, - body: JSON.stringify(params), - }) - .then((res) => { - return res.json(); - }) - .then((res: GenerateShareLinkResponse) => { - if ((res as unknown as { error: string }).error === 'Internal Server Error') { - throw new Error('Server error'); - } - return res.token; - }); -} - -export function getShareInfo(token: string): Promise { - return fetch(`${constants.DRIVE_API_URL}/storage/share/${token}`).then((res) => res.json()); -} - -const shareService = { - generateShareLinkToken, - getShareInfo, -}; - -export default shareService; diff --git a/src/App.tsx b/src/App.tsx index 60c5c9705..bf6033b81 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,8 @@ import * as NavigationBar from 'expo-navigation-bar'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { + Appearance, AppStateStatus, - ColorValue, KeyboardAvoidingView, NativeEventSubscription, Platform, @@ -19,16 +19,17 @@ import AppToast from './components/AppToast'; import ChangeProfilePictureModal from './components/modals/ChangeProfilePictureModal'; import DeleteAccountModal from './components/modals/DeleteAccountModal'; import EditNameModal from './components/modals/EditNameModal'; -import InviteFriendsModal from './components/modals/InviteFriendsModal'; import LanguageModal from './components/modals/LanguageModal'; import LinkCopiedModal from './components/modals/LinkCopiedModal'; import PlansModal from './components/modals/PlansModal'; import { DriveContextProvider } from './contexts/Drive'; import { getRemoteUpdateIfAvailable, useLoadFonts } from './helpers'; +import useGetColor from './hooks/useColor'; import Navigation from './navigation'; import { LockScreen } from './screens/common/LockScreen'; import analyticsService from './services/AnalyticsService'; import appService from './services/AppService'; +import asyncStorageService from './services/AsyncStorageService'; import authService from './services/AuthService'; import { logger } from './services/common'; import { time } from './services/common/time'; @@ -45,15 +46,43 @@ let listener: NativeEventSubscription | null = null; export default function App(): JSX.Element { const dispatch = useAppDispatch(); const tailwind = useTailwind(); + const getColor = useGetColor(); + const { isReady: fontsAreReady } = useLoadFonts(); const { user } = useAppSelector((state) => state.auth); - const { screenLocked, lastScreenLock, initialScreenLocked } = useAppSelector((state) => state.app); + const { screenLocked, lastScreenLock, initialScreenLocked, screenLockEnabled } = useAppSelector((state) => state.app); + const [currentTheme, setCurrentTheme] = useState<'light' | 'dark'>('light'); + + useEffect(() => { + const initializeTheme = async () => { + const savedTheme = await asyncStorageService.getThemePreference(); + const themeToApply = savedTheme || Appearance.getColorScheme() || 'light'; + + setCurrentTheme(themeToApply as 'light' | 'dark'); + Appearance.setColorScheme(themeToApply); + }; + + initializeTheme(); + const subscription = Appearance.addChangeListener(({ colorScheme }) => { + asyncStorageService.getThemePreference().then((savedTheme) => { + if (!savedTheme && colorScheme) { + setCurrentTheme(colorScheme); + } + }); + }); + + return () => { + subscription?.remove(); + if (!screenLockEnabled) { + dispatch(appActions.setInitialScreenLocked(false)); + dispatch(appActions.setScreenLocked(false)); + } + }; + }, []); - const { color: whiteColor } = tailwind('text-white'); const [isAppInitialized, setIsAppInitialized] = useState(false); const { isLinkCopiedModalOpen, - isInviteFriendsModalOpen, isDeleteAccountModalOpen, isEditNameModalOpen, isChangeProfilePictureModalOpen, @@ -69,16 +98,16 @@ export default function App(): JSX.Element { }; const onLinkCopiedModalClosed = () => dispatch(uiActions.setIsLinkCopiedModalOpen(false)); - const onInviteFriendsModalClosed = () => dispatch(uiActions.setIsInviteFriendsModalOpen(false)); const onDeleteAccountModalClosed = () => dispatch(uiActions.setIsDeleteAccountModalOpen(false)); const onEditNameModalClosed = () => dispatch(uiActions.setIsEditNameModalOpen(false)); const onChangeProfilePictureModalClosed = () => dispatch(uiActions.setIsChangeProfilePictureModalOpen(false)); const onLanguageModalClosed = () => dispatch(uiActions.setIsLanguageModalOpen(false)); const onPlansModalClosed = () => dispatch(uiActions.setIsPlansModalOpen(false)); + const handleAppStateChange = (state: AppStateStatus) => { if (state === 'active') { dispatch(appActions.setLastScreenLock(Date.now())); - dispatch(authThunks.refreshTokensThunk()); + dispatch(authThunks.checkAndRefreshTokenThunk()); dispatch(paymentsThunks.checkShouldDisplayBilling()); } @@ -163,11 +192,6 @@ export default function App(): JSX.Element { // Initialize app useEffect(() => { - if (Platform.OS === 'android') { - NavigationBar.setBackgroundColorAsync(whiteColor as ColorValue); - NavigationBar.setButtonStyleAsync('dark'); - } - authService.addLoginListener(onUserLoggedIn); authService.addLogoutListener(onUserLoggedOut); @@ -181,12 +205,30 @@ export default function App(): JSX.Element { }; }, []); + useEffect(() => { + const configureNavigationBar = async () => { + if (Platform.OS === 'android') { + try { + const backgroundColor = getColor('bg-surface'); + const isDark = currentTheme === 'dark'; + + await NavigationBar.setBackgroundColorAsync(backgroundColor); + await NavigationBar.setButtonStyleAsync(isDark ? 'light' : 'dark'); + } catch (error) { + logger.error('Error configuring navigation bar:', error); + } + } + }; + + configureNavigationBar(); + }, [getColor, currentTheme]); + return ( {isAppInitialized && fontsAreReady ? ( - + - {initialScreenLocked ? null : } + {initialScreenLocked && screenLocked ? null : } - { const tailwind = useTailwind(); const getColor = useGetColor(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === 'dark'; const isTitleString = typeof props.title === 'string'; - const typeBgStyle = { - accept: props.disabled - ? props.loading - ? tailwind('bg-primary-dark') - : tailwind('bg-gray-40') - : tailwind('bg-primary'), - 'accept-2': props.disabled ? tailwind('bg-gray-40') : tailwind('bg-primary/10'), - cancel: tailwind('bg-gray-5'), - 'cancel-2': tailwind('bg-primary/10'), - delete: props.disabled ? tailwind('bg-gray-40') : tailwind('bg-red'), - white: { - ...tailwind('bg-white'), - ...({ - borderColor: 'rgba(0,0,0,0.1)', - borderWidth: 1, - shadowColor: '#000000', - shadowOffset: { - width: 0, - height: 1, - }, - shadowOpacity: 0.16, - shadowRadius: 1.51, - elevation: 2, - } as ViewStyle), - }, - }[props.type]; - const typeTextStyle = { - accept: tailwind('text-white'), - 'accept-2': props.disabled ? tailwind('text-white') : tailwind('text-primary'), - cancel: props.disabled ? tailwind('text-gray-40') : tailwind('text-gray-80'), - 'cancel-2': tailwind('text-primary'), - delete: tailwind('text-white'), - white: tailwind('text-gray-80'), - }[props.type]; - const typeUnderlayColor = { - accept: getColor('text-primary-dark'), - 'accept-2': getColor('text-primary/20'), - cancel: getColor('text-gray-10'), - 'cancel-2': getColor('text-gray-10'), - delete: getColor('text-red-dark'), - white: getColor('text-gray-1'), - }[props.type]; + + const getButtonStyles = () => { + switch (props.type) { + case 'accept': + return { + backgroundColor: props.disabled + ? props.loading + ? getColor('text-primary-dark') + : getColor('bg-gray-30') + : getColor('text-primary'), + textColor: getColor('text-white'), + underlayColor: getColor('text-primary-dark'), + }; + + case 'accept-2': + return { + backgroundColor: props.disabled ? getColor('bg-gray-10') : getColor('bg-primary-10'), + textColor: props.disabled ? getColor('text-gray-40') : getColor('text-primary'), + underlayColor: getColor('bg-primary-20'), + }; + + case 'cancel': + return { + backgroundColor: getColor('bg-gray-5'), + textColor: props.disabled ? getColor('text-gray-40') : getColor('text-gray-80'), + underlayColor: getColor('bg-gray-10'), + }; + + case 'cancel-2': + return { + backgroundColor: getColor('bg-primary-10'), + textColor: getColor('text-primary'), + underlayColor: getColor('bg-gray-10'), + }; + + case 'delete': + return { + backgroundColor: props.disabled ? getColor('bg-gray-30') : getColor('text-red'), + textColor: getColor('text-white'), + underlayColor: getColor('text-red-dark'), + }; + + case 'white': + return { + backgroundColor: getColor('bg-surface'), + textColor: getColor('text-gray-80'), + underlayColor: getColor('bg-gray-5'), + borderColor: getColor('border-gray-20'), + borderWidth: 1, + shadowColor: isDark ? 'transparent' : '#000000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: isDark ? 0 : 0.16, + shadowRadius: 1.51, + elevation: isDark ? 0 : 2, + }; + + default: + return { + backgroundColor: getColor('text-primary'), + textColor: getColor('text-white'), + underlayColor: getColor('text-primary-dark'), + }; + } + }; + + const buttonStyles = getButtonStyles(); const renderContent = () => { const title = isTitleString ? ( - + {props.title} ) : ( @@ -72,10 +97,10 @@ const AppButton = (props: AppButtonProps): JSX.Element => { ); return ( - + {props.loading && ( - + )} {title} @@ -86,8 +111,21 @@ const AppButton = (props: AppButtonProps): JSX.Element => { return ( diff --git a/src/components/AppScreen/index.tsx b/src/components/AppScreen/index.tsx index c1b9f06f4..a990b0415 100644 --- a/src/components/AppScreen/index.tsx +++ b/src/components/AppScreen/index.tsx @@ -1,6 +1,6 @@ import { StatusBar, StatusBarStyle } from 'expo-status-bar'; import React from 'react'; -import { Keyboard, StyleProp, View, ViewStyle } from 'react-native'; +import { Keyboard, Platform, StyleProp, View, ViewStyle, useColorScheme } from 'react-native'; import { TouchableWithoutFeedback } from 'react-native-gesture-handler'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTailwind } from 'tailwind-rn'; @@ -22,11 +22,18 @@ interface AppScreenProps { const AppScreen = (props: AppScreenProps): JSX.Element => { const tailwind = useTailwind(); const getColor = useGetColor(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === 'dark'; const safeAreaInsets = useSafeAreaInsets(); const propsStyle = Object.assign({}, props.style || {}) as Record; - const safeAreaColor = props.safeAreaColor || getColor('text-white'); - const backgroundColor = props.backgroundColor || getColor('text-white'); - const statusBarStyle = props.statusBarStyle || 'dark'; + + const safeAreaColor = props.safeAreaColor || getColor('bg-surface'); + const backgroundColor = props.backgroundColor || getColor('bg-surface'); + + const statusBarStyle = props.statusBarStyle || (isDark ? 'light' : 'dark'); + const statusBarBackgroundColor = Platform.OS === 'android' ? backgroundColor : undefined; + const statusBarTranslucent = props.statusBarTranslucent ?? (Platform.OS === 'android' ? false : undefined); + const onBackgroundPressed = () => { Keyboard.dismiss(); }; @@ -49,8 +56,8 @@ const AppScreen = (props: AppScreenProps): JSX.Element => {