diff --git a/android/app/build.gradle b/android/app/build.gradle index 40a3494ac..c20f0e5dd 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -93,8 +93,8 @@ android { applicationId 'com.internxt.cloud' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 125 - versionName "1.10.0" + versionCode 129 + versionName "1.10.2" buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\"" } diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 8f9a60531..f0d4ea285 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ Internxt automatic - 1.10.0 + 1.10.2 contain false \ No newline at end of file diff --git a/assets/lang/strings.ts b/assets/lang/strings.ts index 9667d4559..cf28bca07 100644 --- a/assets/lang/strings.ts +++ b/assets/lang/strings.ts @@ -858,6 +858,7 @@ const translations = { SignOutModal: { title: 'Log out from this account?', signingOut: 'Saving your latest Photos data…', + signingOutGeneric: 'Signing out…', }, ConfirmMoveItemModal: { title: 'Move {0} item(s) from', @@ -1868,6 +1869,7 @@ const translations = { SignOutModal: { title: '¿Salir de esta cuenta?', signingOut: 'Guardando tus últimos datos de Photos…', + signingOutGeneric: 'Cerrando sesión…', }, ConfirmMoveItemModal: { title: 'Mover {0} item(s) desde', diff --git a/ios/Internxt/Info.plist b/ios/Internxt/Info.plist index 02a5e6b8b..941e5c8da 100644 --- a/ios/Internxt/Info.plist +++ b/ios/Internxt/Info.plist @@ -23,7 +23,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.10.0 + 1.10.2 CFBundleSignature ???? CFBundleURLTypes diff --git a/ios/Internxt/Supporting/Expo.plist b/ios/Internxt/Supporting/Expo.plist index f8db468be..778050ce9 100644 --- a/ios/Internxt/Supporting/Expo.plist +++ b/ios/Internxt/Supporting/Expo.plist @@ -9,7 +9,7 @@ EXUpdatesLaunchWaitMs 0 EXUpdatesRuntimeVersion - 1.10.0 + 1.10.2 EXUpdatesURL https://u.expo.dev/680f4feb-6315-4a50-93ec-36dcd0b831d2 diff --git a/package.json b/package.json index f1efa8300..41aa2c033 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "drive-mobile", - "version": "v1.10.0", + "version": "v1.10.2", "private": true, "license": "GNU", "scripts": { diff --git a/src/App.tsx b/src/App.tsx index 86f08598b..c618ebd06 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -249,13 +249,7 @@ function AppContent(): JSX.Element { onClose={onChangeProfilePictureModalClosed} /> - { - if (navigationRef.isReady()) { - navigationRef.reset({ index: 0, routes: [{ name: 'SignIn' }] }); - } - }} - /> + ) : ( diff --git a/src/components/SearchInput/SearchInput.tsx b/src/components/SearchInput/SearchInput.tsx index 9b333b6f6..9d03b036e 100644 --- a/src/components/SearchInput/SearchInput.tsx +++ b/src/components/SearchInput/SearchInput.tsx @@ -1,4 +1,4 @@ -import { MagnifyingGlass, XCircle } from 'phosphor-react-native'; +import { MagnifyingGlassIcon, XCircleIcon } from 'phosphor-react-native'; import { createRef, useState } from 'react'; import { StyleProp, TextInput, TouchableOpacity, View, ViewStyle } from 'react-native'; import { useTailwind } from 'tailwind-rn'; @@ -50,7 +50,7 @@ export function SearchInput(props: SearchInputProps): JSX.Element { {!isFocused && ( - + )} @@ -62,8 +62,14 @@ export function SearchInput(props: SearchInputProps): JSX.Element { value={props.value} style={[ styles.fontWeight.regular, - tailwind('text-base pl-3 h-9 flex-1'), - { marginBottom: 2, color: getColor('text-gray-100') }, + tailwind('pl-3 h-9 flex-1'), + { + fontSize: 16, + color: getColor('text-gray-100'), + paddingVertical: 0, + textAlignVertical: 'center', + includeFontPadding: false, + }, ]} placeholder={props.placeholder || ''} placeholderTextColor={getColor('text-gray-80')} @@ -72,7 +78,7 @@ export function SearchInput(props: SearchInputProps): JSX.Element { {!!props.value && ( - + )} diff --git a/src/components/modals/SignOutModal/index.tsx b/src/components/modals/SignOutModal/index.tsx index fdc009e5f..2c8d249ac 100644 --- a/src/components/modals/SignOutModal/index.tsx +++ b/src/components/modals/SignOutModal/index.tsx @@ -3,6 +3,7 @@ import { View } from 'react-native'; import { useTailwind } from 'tailwind-rn'; import strings from '../../../../assets/lang/strings'; +import appService from '../../../services/AppService'; import { useAppDispatch, useAppSelector } from '../../../store/hooks'; import { authSelectors, authThunks } from '../../../store/slices/auth'; import { uiActions } from '../../../store/slices/ui'; @@ -12,11 +13,7 @@ import LoadingSpinner from '../../LoadingSpinner'; import UserProfilePicture from '../../UserProfilePicture'; import CenterModal from '../CenterModal'; -interface SignOutModalProps { - readonly onSignedOut: () => void; -} - -function SignOutModal({ onSignedOut }: SignOutModalProps): JSX.Element { +function SignOutModal(): JSX.Element { const tailwind = useTailwind(); const dispatch = useAppDispatch(); const userFullName = useAppSelector(authSelectors.userFullName); @@ -40,7 +37,6 @@ function SignOutModal({ onSignedOut }: SignOutModalProps): JSX.Element { await dispatch(authThunks.signOutThunk({ reason: 'manual' })); isSigningOutRef.current = false; setIsSigningOut(false); - onSignedOut(); onClosed(); }; @@ -57,7 +53,9 @@ function SignOutModal({ onSignedOut }: SignOutModalProps): JSX.Element { - {strings.modals.SignOutModal.signingOut} + {appService.isPhotosEnabled + ? strings.modals.SignOutModal.signingOut + : strings.modals.SignOutModal.signingOutGeneric} ) : ( diff --git a/src/navigation/AppStackNavigator.tsx b/src/navigation/AppStackNavigator.tsx new file mode 100644 index 000000000..8ee60a3e1 --- /dev/null +++ b/src/navigation/AppStackNavigator.tsx @@ -0,0 +1,51 @@ +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { Platform } from 'react-native'; + +import { LargeShareUploadScreen } from '../screens/LargeShareUpload/LargeShareUploadScreen'; +import { DeactivatedAccountScreen } from '../screens/DeactivatedAccountScreen'; +import DebugScreen from '../screens/DebugScreen'; +import { TrashScreen } from '../screens/common/TrashScreen'; +import { DrivePreviewScreen } from '../screens/drive/DrivePreviewScreen'; +import { PhotoPreviewScreen } from '../screens/PhotoPreviewScreen'; +import { SettingsNavigator } from './SettingsNavigator'; +import AuthenticatedNavigator from './TabExplorerNavigator'; +import ShareExtensionView from '../shareExtension/ShareExtensionView.android'; +import { RootStackParamList } from '../types/navigation'; + +const Stack = createNativeStackNavigator(); + +export default function AppStackNavigator(): JSX.Element { + return ( + + + + + + + + + {Platform.OS === 'android' && ( + + )} + {Platform.OS === 'ios' && ( + + )} + + ); +} diff --git a/src/navigation/AuthStackNavigator.tsx b/src/navigation/AuthStackNavigator.tsx new file mode 100644 index 000000000..8e5a9f88c --- /dev/null +++ b/src/navigation/AuthStackNavigator.tsx @@ -0,0 +1,27 @@ +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { Platform } from 'react-native'; + +import SignInScreen from '../screens/SignInScreen'; +import WebLoginScreen from '../screens/WebLoginScreen'; +import DebugScreen from '../screens/DebugScreen'; +import ShareExtensionView from '../shareExtension/ShareExtensionView.android'; +import { RootStackParamList } from '../types/navigation'; + +const Stack = createNativeStackNavigator(); + +export default function AuthStackNavigator(): JSX.Element { + return ( + + + + + {Platform.OS === 'android' && ( + + )} + + ); +} diff --git a/src/navigation/RootNavigator.tsx b/src/navigation/RootNavigator.tsx index d9ef3b4b4..a9f12244b 100644 --- a/src/navigation/RootNavigator.tsx +++ b/src/navigation/RootNavigator.tsx @@ -1,26 +1,14 @@ import { NavigationContainerRef } from '@react-navigation/native'; -import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import { Platform, View } from 'react-native'; -import { LargeShareUploadScreen } from '../screens/LargeShareUpload/LargeShareUploadScreen'; +import { View } from 'react-native'; -import SignInScreen from '../screens/SignInScreen'; -import WebLoginScreen from '../screens/WebLoginScreen'; import { useAppSelector } from '../store/hooks'; import { RootStackParamList } from '../types/navigation'; -import AuthenticatedNavigator from './TabExplorerNavigator'; import { useDeepLinks } from '../hooks/useDeepLinks'; -import { DeactivatedAccountScreen } from '../screens/DeactivatedAccountScreen'; -import DebugScreen from '../screens/DebugScreen'; -import { TrashScreen } from '../screens/common/TrashScreen'; -import { DrivePreviewScreen } from '../screens/drive/DrivePreviewScreen'; -import { PhotoPreviewScreen } from '../screens/PhotoPreviewScreen'; -import { SettingsNavigator } from './SettingsNavigator'; -import ShareExtensionView from '../shareExtension/ShareExtensionView.android'; import { useIosPendingShareHandoff } from '../shareExtension/hooks/useIosPendingShareHandoff'; import { useAndroidShareIntent } from '../shareExtension/useAndroidShareIntent'; - -const Stack = createNativeStackNavigator(); +import AppStackNavigator from './AppStackNavigator'; +import AuthStackNavigator from './AuthStackNavigator'; type Props = { navigationContainerRef?: NavigationContainerRef; @@ -35,43 +23,7 @@ function AppNavigator({ navigationContainerRef }: Readonly): JSX.Element if (isLoggedIn == null) return ; - const initialRouteName: keyof RootStackParamList = isLoggedIn ? 'TabExplorer' : 'SignIn'; - - return ( - - - - - - - - - - - {Platform.OS === 'android' && ( - - )} - {Platform.OS === 'ios' && ( - - )} - - ); + return isLoggedIn ? : ; } export default AppNavigator; diff --git a/src/navigation/TabExplorerNavigator.tsx b/src/navigation/TabExplorerNavigator.tsx index bb4033a86..73de7a67d 100644 --- a/src/navigation/TabExplorerNavigator.tsx +++ b/src/navigation/TabExplorerNavigator.tsx @@ -5,7 +5,6 @@ import { BottomTabBarProps } from '@react-navigation/bottom-tabs/lib/typescript/ import { useEffect } from 'react'; import { AppState, AppStateStatus, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { authThunks } from 'src/store/slices/auth'; import { runBackupCycleThunk } from 'src/store/slices/photos'; import { storageThunks } from 'src/store/slices/storage'; import { useTailwind } from 'tailwind-rn'; @@ -60,7 +59,6 @@ export default function TabExplorerNavigator(props: RootStackScreenProps<'TabExp } catch { const isDeletingAccount = await asyncStorageService.getItem(AsyncStorageKey.IsDeletingAccount); if (isDeletingAccount) { - dispatch(authThunks.signOutThunk({ reason: 'manual' })); props.navigation.replace('DeactivatedAccount'); } } diff --git a/src/screens/DeactivatedAccountScreen/DeactivatedAccountScreen.tsx b/src/screens/DeactivatedAccountScreen/DeactivatedAccountScreen.tsx index 760c3e726..1f2014300 100644 --- a/src/screens/DeactivatedAccountScreen/DeactivatedAccountScreen.tsx +++ b/src/screens/DeactivatedAccountScreen/DeactivatedAccountScreen.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { RootStackScreenProps } from '../../types/navigation'; import InternxtLogo from '../../../assets/logo.svg'; import AppScreen from '../../components/AppScreen'; import AppButton from '../../components/AppButton'; @@ -12,12 +11,15 @@ import { Dimensions, Linking, View } from 'react-native'; import AppVersionWidget from 'src/components/AppVersionWidget'; import { LinearGradient } from 'expo-linear-gradient'; import appService from 'src/services/AppService'; +import { useAppDispatch } from 'src/store/hooks'; +import { authThunks } from 'src/store/slices/auth'; -export function DeactivatedAccountScreen({ navigation }: RootStackScreenProps<'DeactivatedAccount'>): JSX.Element { +export function DeactivatedAccountScreen(): JSX.Element { const tailwind = useTailwind(); + const dispatch = useAppDispatch(); const handleGoToSignIn = () => { - navigation.replace('SignIn'); + dispatch(authThunks.signOutThunk({ reason: 'manual' })); }; const handleGoToSignUp = async () => { const webAuthUrl = appService.urls.webAuth.signup; diff --git a/src/screens/PhotoPreviewScreen/index.tsx b/src/screens/PhotoPreviewScreen/index.tsx index db0e7d609..50807d1a3 100644 --- a/src/screens/PhotoPreviewScreen/index.tsx +++ b/src/screens/PhotoPreviewScreen/index.tsx @@ -1,6 +1,6 @@ import { useNavigation } from '@react-navigation/native'; import strings from 'assets/lang/strings'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { StatusBar, View } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; @@ -53,27 +53,32 @@ export const PhotoPreviewScreen = ({ route }: Props): JSX.Element => { } }, []); - const handleSwipeDown = useCallback(() => { + const hasClosedRef = useRef(false); + const closePreview = useCallback(() => { + if (hasClosedRef.current) { + return; + } + hasClosedRef.current = true; navigation.goBack(); }, [navigation]); + const handleSwipeDown = useCallback(() => { + closePreview(); + }, [closePreview]); + const exitVideoMode = useCallback(() => { setHasVideoStarted(false); resetVideoPlayer(); setIsUiVisible(true); }, [resetVideoPlayer]); - const closePreviewScreen = useCallback(() => { - navigation.goBack(); - }, [navigation]); - const handleClose = useCallback(() => { if (hasVideoStarted) { exitVideoMode(); } else { - closePreviewScreen(); + closePreview(); } - }, [hasVideoStarted, exitVideoMode, closePreviewScreen]); + }, [hasVideoStarted, exitVideoMode, closePreview]); const handleVideoPlay = useCallback(() => { setHasVideoStarted(true); @@ -132,8 +137,8 @@ export const PhotoPreviewScreen = ({ route }: Props): JSX.Element => { }, [onItemChanged]), onAfterTrash: useCallback(async () => { await onItemChanged?.(); - navigation.goBack(); - }, [onItemChanged, navigation]), + closePreview(); + }, [onItemChanged, closePreview]), onAfterRestore: useCallback( async (restoredItems: TimelinePhotoItem[]) => { for (const restoredItem of restoredItems) { diff --git a/src/screens/PhotosScreen/EnableBackupBottomSheet.tsx b/src/screens/PhotosScreen/EnableBackupBottomSheet.tsx index 9b390ff13..6f6387ab1 100644 --- a/src/screens/PhotosScreen/EnableBackupBottomSheet.tsx +++ b/src/screens/PhotosScreen/EnableBackupBottomSheet.tsx @@ -60,7 +60,7 @@ const EnableBackupBottomSheet = ({ isOpen, onClose, onSuccess }: EnableBackupBot }; return ( - + { @@ -51,7 +50,12 @@ const ActionRow = ({ icon, label, onPress, isDestructive, isLast }: ActionRowPro style={[tailwind('flex-row items-center px-4'), styles.row]} hitSlop={{ top: 2, bottom: 2 }} > - + {icon} - + {actionStrings.back} diff --git a/src/screens/SignInScreen/index.tsx b/src/screens/SignInScreen/index.tsx index cb8058894..a90a9e6c1 100644 --- a/src/screens/SignInScreen/index.tsx +++ b/src/screens/SignInScreen/index.tsx @@ -69,7 +69,6 @@ function SignInScreen(): JSX.Element { return ( {isDark ? ( diff --git a/src/screens/WebLoginScreen/index.tsx b/src/screens/WebLoginScreen/index.tsx index dc55116a6..589b44867 100644 --- a/src/screens/WebLoginScreen/index.tsx +++ b/src/screens/WebLoginScreen/index.tsx @@ -48,16 +48,16 @@ function WebLoginScreen({ route, navigation }: RootStackScreenProps<'WebLogin'>) privateKey, }); - await dispatch( - authThunks.signInThunk({ - user: result.user, - token: result.token, - newToken: result.newToken, - }), - ).unwrap(); - setLoadingState('success'); - setTimeout(() => navigation.replace('TabExplorer', { screen: 'Home' }), 800); + setTimeout(() => { + dispatch( + authThunks.signInThunk({ + user: result.user, + token: result.token, + newToken: result.newToken, + }), + ); + }, 800); } catch (error) { const castedError = errorService.castError(error); setLoadingState('error'); diff --git a/src/screens/drive/DriveFolderScreen/DriveFolderScreenHeader.tsx b/src/screens/drive/DriveFolderScreen/DriveFolderScreenHeader.tsx index 5ad895317..db48d6248 100644 --- a/src/screens/drive/DriveFolderScreen/DriveFolderScreenHeader.tsx +++ b/src/screens/drive/DriveFolderScreen/DriveFolderScreenHeader.tsx @@ -133,7 +133,7 @@ export const DriveFolderScreenHeader: React.FC = ( onFocusChange={handleSearchInputFocusChange} /> - = ( {strings.screens.drive.searchInAllFolders} - + */} diff --git a/src/screens/drive/DriveFolderScreen/search/GlobalSearchModal.tsx b/src/screens/drive/DriveFolderScreen/search/GlobalSearchModal.tsx index 649bb4b00..487f5e4f9 100644 --- a/src/screens/drive/DriveFolderScreen/search/GlobalSearchModal.tsx +++ b/src/screens/drive/DriveFolderScreen/search/GlobalSearchModal.tsx @@ -6,16 +6,8 @@ import { DriveListType } from '@internxt-mobile/types/drive/ui'; import { useNavigation } from '@react-navigation/native'; import { ArrowLeft, MagnifyingGlass } from 'phosphor-react-native'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { - ActivityIndicator, - KeyboardAvoidingView, - Modal, - Platform, - SafeAreaView, - TextInput, - TouchableOpacity, - View, -} from 'react-native'; +import { ActivityIndicator, KeyboardAvoidingView, Modal, Platform, TextInput, TouchableOpacity, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import AppText from 'src/components/AppText'; import DriveList from 'src/components/drive/lists/DriveList/DriveList'; import useGetColor from 'src/hooks/useColor'; @@ -39,6 +31,7 @@ interface GlobalSearchModalProps { export const GlobalSearchModal: React.FC = ({ visible, onClose, onItemPress }) => { const tailwind = useTailwind(); const getColor = useGetColor(); + const insets = useSafeAreaInsets(); const [inputRef, setInputRef] = useState(null); const [isNavigating, setIsNavigating] = useState(false); const navigation = useNavigation['navigation']>(); @@ -273,8 +266,19 @@ export const GlobalSearchModal: React.FC = ({ visible, o }; return ( - - + + {/* Header */} @@ -299,9 +303,19 @@ export const GlobalSearchModal: React.FC = ({ visible, o onChangeText={updateQuery} placeholder={strings.modals.GlobalSearchModal.searchPlaceholder} placeholderTextColor={getColor('text-gray-40')} - style={[tailwind('flex-1 ml-2 text-base'), { color: getColor('text-gray-100') }]} + style={[ + tailwind('flex-1 ml-2'), + { + fontSize: 16, + color: getColor('text-gray-100'), + paddingVertical: 0, + textAlignVertical: 'center', + includeFontPadding: false, + }, + ]} autoCapitalize="none" autoCorrect={false} + spellCheck={false} returnKeyType="search" editable={!isNavigating} /> @@ -363,7 +377,7 @@ export const GlobalSearchModal: React.FC = ({ visible, o renderEmptyState() )} - + ); }; diff --git a/src/store/slices/photos/index.spec.ts b/src/store/slices/photos/index.spec.ts index 6914f8647..ed5adb137 100644 --- a/src/store/slices/photos/index.spec.ts +++ b/src/store/slices/photos/index.spec.ts @@ -104,6 +104,12 @@ jest.mock('src/store/slices/storage', () => ({ }, })); +jest.mock('src/services/AppService', () => ({ + ...jest.requireActual('src/services/AppService'), + __esModule: true, + default: { isPhotosEnabled: true }, +})); + jest.mock('./selectors', () => ({ hasPhotosFeatureAccess: jest.fn().mockReturnValue(true), })); diff --git a/src/store/slices/photos/index.ts b/src/store/slices/photos/index.ts index 956787d5b..12907814c 100644 --- a/src/store/slices/photos/index.ts +++ b/src/store/slices/photos/index.ts @@ -1,6 +1,7 @@ import { AxiosResponseError } from '@internxt/sdk/dist/shared/types/errors'; import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { Platform } from 'react-native'; +import appService from 'src/services/AppService'; import asyncStorageService from 'src/services/AsyncStorageService'; import errorService from 'src/services/ErrorService'; import { networkMonitorService, NetworkStateType } from 'src/services/NetworkMonitorService'; @@ -387,6 +388,10 @@ export const forceRefreshThunk = createAsyncThunk( 'photos/runBackupCycle', async (_, { getState, dispatch }) => { + if (!appService.isPhotosEnabled) { + return; + } + const { syncStatus } = getState().photos; if (syncStatus === 'scanning' || syncStatus === 'uploading' || syncStatus === 'pausing') { return;