From 088dd8cb5e817f0847901b3f82b5df6f7a89629f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Warcho=C5=82?= <61014013+war-in@users.noreply.github.com> Date: Wed, 6 Aug 2025 11:39:16 +0200 Subject: [PATCH] Revert "[CP Staging] Revert "Bring back new SignInPage"" --- __mocks__/@ua/react-native-airship.ts | 9 +- .../ReactNativeHybridApp.kt | 18 ++ .../hybrid-app/ios/ReactNativeHybridApp.mm | 12 ++ .../src/NativeReactNativeHybridApp.ts | 3 + modules/hybrid-app/src/index.native.ts | 9 + modules/hybrid-app/src/index.ts | 12 ++ modules/hybrid-app/src/types.ts | 3 + src/App.tsx | 91 +++++---- src/CONFIG.ts | 11 ++ src/Expensify.tsx | 19 +- src/HybridAppHandler.tsx | 20 +- src/components/ScreenWrapper/index.tsx | 2 +- .../GoogleSignIn/index.native.tsx | 20 +- src/libs/HybridApp.ts | 112 +++++++++++ .../Navigation/AppNavigator/AuthScreens.tsx | 33 ++-- .../Navigation/AppNavigator/PublicScreens.tsx | 4 +- .../Navigation/AppNavigator/index.native.tsx | 17 +- .../PushNotification/index.native.ts | 23 ++- src/libs/actions/App.ts | 1 + src/libs/actions/Delegate.ts | 1 + src/libs/actions/HybridApp/index.ts | 71 ++++++- src/libs/actions/HybridApp/types.ts | 11 ++ src/libs/actions/QueuedOnyxUpdates.ts | 1 + src/libs/actions/Session/index.ts | 180 +++++++----------- src/libs/actions/SignInRedirect.ts | 5 + src/libs/actions/User.ts | 5 + src/pages/ErrorPage/SessionExpiredPage.tsx | 2 +- .../BaseOnboardingAccounting.tsx | 2 +- .../BaseOnboardingInterestedFeatures.tsx | 2 +- .../FloatingActionButtonAndPopover.tsx | 2 +- src/pages/settings/InitialSettingsPage.tsx | 3 +- .../MergeAccounts/MergeResultPage.tsx | 2 +- src/pages/signin/SignUpWelcomeForm.tsx | 16 +- src/types/onyx/HybridApp.ts | 33 ++++ src/types/onyx/TryNewDot.ts | 6 +- 35 files changed, 553 insertions(+), 208 deletions(-) create mode 100644 src/libs/HybridApp.ts create mode 100644 src/libs/actions/HybridApp/types.ts diff --git a/__mocks__/@ua/react-native-airship.ts b/__mocks__/@ua/react-native-airship.ts index a60053df475e0..08ec1b781fc21 100644 --- a/__mocks__/@ua/react-native-airship.ts +++ b/__mocks__/@ua/react-native-airship.ts @@ -6,6 +6,13 @@ enum EventType { PushReceived = 'com.airship.push_received', } +// eslint-disable-next-line no-restricted-syntax +enum PermissionStatus { + Granted = 'granted', + Denied = 'denied', + NotDetermined = 'not_determined', +} + // eslint-disable-next-line @typescript-eslint/no-namespace namespace iOS { /** @@ -71,4 +78,4 @@ const Airship: Partial = { export default Airship; -export {EventType, iOS}; +export {EventType, iOS, PermissionStatus}; diff --git a/modules/hybrid-app/android/src/main/java/com/expensify/reactnativehybridapp/ReactNativeHybridApp.kt b/modules/hybrid-app/android/src/main/java/com/expensify/reactnativehybridapp/ReactNativeHybridApp.kt index 8c836ec97c427..d366c6c732948 100644 --- a/modules/hybrid-app/android/src/main/java/com/expensify/reactnativehybridapp/ReactNativeHybridApp.kt +++ b/modules/hybrid-app/android/src/main/java/com/expensify/reactnativehybridapp/ReactNativeHybridApp.kt @@ -51,4 +51,22 @@ class ReactNativeHybridApp(reactContext: ReactApplicationContext) : override fun onURLListenerAdded() { Log.d(NAME, "`onURLListenerAdded` should never be called in standalone `New Expensify` app") } + + override fun signInToOldDot( + autoGeneratedLogin: String, + autoGeneratedPassword: String, + authToken: String, + email: String, + policyID: String + ) { + Log.d(NAME, "`signInToOldDot` should never be called in standalone `New Expensify` app") + } + + override fun signOutFromOldDot() { + Log.d(NAME, "`signOutFromOldDot` should never be called in standalone `New Expensify` app") + } + + override fun clearOldDotAfterSignOut() { + Log.d(NAME, "`clearOldDotAfterSignOut` should never be called in standalone `New Expensify` app") + } } diff --git a/modules/hybrid-app/ios/ReactNativeHybridApp.mm b/modules/hybrid-app/ios/ReactNativeHybridApp.mm index 3935f40eb9a7e..6dfd968f01333 100644 --- a/modules/hybrid-app/ios/ReactNativeHybridApp.mm +++ b/modules/hybrid-app/ios/ReactNativeHybridApp.mm @@ -43,6 +43,18 @@ - (void)onURLListenerAdded { NSLog(@"[ReactNativeHybridApp] `onURLListenerAdded` should never be called in standalone `New Expensify` app"); } +- (void)signInToOldDot:(NSString *)autoGeneratedLogin autoGeneratedPassword:(NSString *)autoGeneratedPassword authToken:(NSString *)authToken email:(NSString *)email policyID:(NSString *)policyID { + NSLog(@"[ReactNativeHybridApp] `signInToOldDot` should never be called in standalone `New Expensify` app"); +} + +- (void)signOutFromOldDot { + NSLog(@"[ReactNativeHybridApp] `signOutFromOldDot` should never be called in standalone `New Expensify` app"); +} + +- (void)clearOldDotAfterSignOut { + NSLog(@"[ReactNativeHybridApp] `clearOldDotAfterSignOut` should never be called in standalone `New Expensify` app"); +} + - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { diff --git a/modules/hybrid-app/src/NativeReactNativeHybridApp.ts b/modules/hybrid-app/src/NativeReactNativeHybridApp.ts index 87bd482faf284..d88aa84b5b606 100644 --- a/modules/hybrid-app/src/NativeReactNativeHybridApp.ts +++ b/modules/hybrid-app/src/NativeReactNativeHybridApp.ts @@ -12,6 +12,9 @@ export interface Spec extends TurboModule { getHybridAppSettings: () => Promise; getInitialURL(): Promise; onURLListenerAdded: () => void; + signInToOldDot: (autoGeneratedLogin: string, autoGeneratedPassword: string, authToken: string, email: string, policyID: string) => void; + signOutFromOldDot: () => void; + clearOldDotAfterSignOut: () => void; } export default TurboModuleRegistry.getEnforcing('ReactNativeHybridApp'); diff --git a/modules/hybrid-app/src/index.native.ts b/modules/hybrid-app/src/index.native.ts index 2fa0c8463c549..0d5770c6aa0d9 100644 --- a/modules/hybrid-app/src/index.native.ts +++ b/modules/hybrid-app/src/index.native.ts @@ -29,6 +29,15 @@ const HybridAppModule: HybridAppModuleType = { onURLListenerAdded() { ReactNativeHybridApp.onURLListenerAdded(); }, + signInToOldDot({autoGeneratedLogin, autoGeneratedPassword, authToken, email, policyID}) { + ReactNativeHybridApp.signInToOldDot(autoGeneratedLogin, autoGeneratedPassword, authToken, email, policyID); + }, + signOutFromOldDot() { + ReactNativeHybridApp.signOutFromOldDot(); + }, + clearOldDotAfterSignOut() { + ReactNativeHybridApp.clearOldDotAfterSignOut(); + }, }; export default HybridAppModule; diff --git a/modules/hybrid-app/src/index.ts b/modules/hybrid-app/src/index.ts index 813fe7af880f7..6e0534d37d4b6 100644 --- a/modules/hybrid-app/src/index.ts +++ b/modules/hybrid-app/src/index.ts @@ -38,6 +38,18 @@ const HybridAppModule: HybridAppModuleType = { // eslint-disable-next-line no-console console.warn('HybridAppModule: `onURLListenerAdded` should never be called on web'); }, + signInToOldDot() { + // eslint-disable-next-line no-console + console.warn('HybridAppModule: `signInToOldDot` should never be called on web'); + }, + signOutFromOldDot() { + // eslint-disable-next-line no-console + console.warn('HybridAppModule: `signOutFromOldDot` should never be called on web'); + }, + clearOldDotAfterSignOut() { + // eslint-disable-next-line no-console + console.warn('HybridAppModule: `clearOldDotAfterSignOut` should never be called on web'); + }, }; export default HybridAppModule; diff --git a/modules/hybrid-app/src/types.ts b/modules/hybrid-app/src/types.ts index ed227123a9b9a..508091e2c82e1 100644 --- a/modules/hybrid-app/src/types.ts +++ b/modules/hybrid-app/src/types.ts @@ -8,6 +8,9 @@ type HybridAppModuleType = { getHybridAppSettings: () => Promise; getInitialURL(): Promise; onURLListenerAdded: () => void; + signInToOldDot: (args: {autoGeneratedLogin: string; autoGeneratedPassword: string; authToken: string; email: string; policyID: string}) => void; + signOutFromOldDot: () => void; + clearOldDotAfterSignOut: () => void; }; export default HybridAppModuleType; diff --git a/src/App.tsx b/src/App.tsx index a7510270bdb90..f42ac14638825 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -41,6 +41,7 @@ import {CurrentReportIDContextProvider} from './hooks/useCurrentReportID'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; import HybridAppHandler from './HybridAppHandler'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; +import './libs/HybridApp'; import {AttachmentModalContextProvider} from './pages/media/AttachmentModalScreen/AttachmentModalContext'; import './setup/backgroundTask'; import './setup/hybridApp'; @@ -69,47 +70,57 @@ function App() { - - - - - - - - - + + + + + + + + + + diff --git a/src/CONFIG.ts b/src/CONFIG.ts index aa6e3a325d499..aa7b3d3963a13 100644 --- a/src/CONFIG.ts +++ b/src/CONFIG.ts @@ -95,7 +95,18 @@ export default { GOOGLE_SIGN_IN: { // cspell:disable-next-line WEB_CLIENT_ID: '921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com', + // cspell:disable-next-line IOS_CLIENT_ID: '921154746561-s3uqn2oe4m85tufi6mqflbfbuajrm2i3.apps.googleusercontent.com', + HYBRID_APP: { + // cspell:disable-next-line + IOS_CLIENT_ID: '1008697809946-sh04nqq0hea396s1qdqqbj6ia649odb2.apps.googleusercontent.com', + WEB_CLIENT_ID: { + // cspell:disable-next-line + IOS: '1008697809946-5e095eqem3o6ugtpc2rjf7v880tcp28p.apps.googleusercontent.com', + // cspell:disable-next-line + ANDROID: '240677659774-86pov3adub93cv4b8uj13g7varolmk2l.apps.googleusercontent.com', + }, + }, }, GCP_GEOLOCATION_API_KEY: googleGeolocationAPIKey, FIREBASE_WEB_CONFIG: { diff --git a/src/Expensify.tsx b/src/Expensify.tsx index ee719dc938070..032d416e79142 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -105,6 +105,7 @@ function Expensify() { const [currentOnboardingPurposeSelected] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, {canBeMissing: true}); const [currentOnboardingCompanySize] = useOnyx(ONYXKEYS.ONBOARDING_COMPANY_SIZE, {canBeMissing: true}); const [onboardingInitialPath] = useOnyx(ONYXKEYS.ONBOARDING_LAST_VISITED_PATH, {canBeMissing: true}); + const [hybridApp] = useOnyx(ONYXKEYS.HYBRID_APP, {canBeMissing: true}); useDebugShortcut(); usePriorityMode(); @@ -121,10 +122,22 @@ function Expensify() { const isAuthenticated = useIsAuthenticated(); const autoAuthState = useMemo(() => session?.autoAuthState ?? '', [session]); - const shouldInit = isNavigationReady && hasAttemptedToOpenPublicRoom && !!preferredLocale; + const isSplashReadyToBeHidden = splashScreenState === CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN; const isSplashVisible = splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE; - const isHybridAppReady = splashScreenState === CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN && isAuthenticated; - const shouldHideSplash = shouldInit && (CONFIG.IS_HYBRID_APP ? isHybridAppReady : isSplashVisible); + + const shouldInit = isNavigationReady && hasAttemptedToOpenPublicRoom && !!preferredLocale; + const shouldHideSplash = (isSplashReadyToBeHidden || isSplashVisible) && shouldInit && !hybridApp?.loggedOutFromOldDot; + + // This effect is closing OldDot sign out modal based on splash screen state + useEffect(() => { + if (!isSplashReadyToBeHidden || !isNavigationReady || !hasAttemptedToOpenPublicRoom || !hybridApp?.loggedOutFromOldDot) { + return; + } + + setSplashScreenState(CONST.BOOT_SPLASH_STATE.HIDDEN); + HybridAppModule.clearOldDotAfterSignOut(); + }, [hasAttemptedToOpenPublicRoom, hybridApp?.loggedOutFromOldDot, isNavigationReady, isSplashReadyToBeHidden, setSplashScreenState]); + const initializeClient = () => { if (!Visibility.isVisible()) { return; diff --git a/src/HybridAppHandler.tsx b/src/HybridAppHandler.tsx index 2b425b34072d2..a73c93e3c868a 100644 --- a/src/HybridAppHandler.tsx +++ b/src/HybridAppHandler.tsx @@ -2,15 +2,22 @@ import HybridAppModule from '@expensify/react-native-hybrid-app'; import {useContext, useEffect} from 'react'; import CONFIG from './CONFIG'; import CONST from './CONST'; -import {signInAfterTransitionFromOldDot} from './libs/actions/Session'; +import useOnyx from './hooks/useOnyx'; +import {parseHybridAppSettings} from './libs/actions/HybridApp'; +import {setupNewDotAfterTransitionFromOldDot} from './libs/actions/Session'; import Log from './libs/Log'; +import ONYXKEYS from './ONYXKEYS'; import SplashScreenStateContext from './SplashScreenStateContext'; +import isLoadingOnyxValue from './types/utils/isLoadingOnyxValue'; function HybridAppHandler() { - const {setSplashScreenState} = useContext(SplashScreenStateContext); + const {splashScreenState, setSplashScreenState} = useContext(SplashScreenStateContext); + const [tryNewDot, tryNewDotMetadata] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {canBeMissing: true}); + + const isLoading = isLoadingOnyxValue(tryNewDotMetadata); useEffect(() => { - if (!CONFIG.IS_HYBRID_APP) { + if (!CONFIG.IS_HYBRID_APP || isLoading) { return; } @@ -21,11 +28,14 @@ function HybridAppHandler() { return; } - signInAfterTransitionFromOldDot(hybridAppSettings).then(() => { + setupNewDotAfterTransitionFromOldDot(parseHybridAppSettings(hybridAppSettings), tryNewDot).then(() => { + if (splashScreenState !== CONST.BOOT_SPLASH_STATE.VISIBLE) { + return; + } setSplashScreenState(CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN); }); }); - }, [setSplashScreenState]); + }, [isLoading, setSplashScreenState, splashScreenState, tryNewDot]); return null; } diff --git a/src/components/ScreenWrapper/index.tsx b/src/components/ScreenWrapper/index.tsx index a2d6474b21c0a..f745e615c3cba 100644 --- a/src/components/ScreenWrapper/index.tsx +++ b/src/components/ScreenWrapper/index.tsx @@ -20,7 +20,7 @@ import NarrowPaneContext from '@libs/Navigation/AppNavigator/Navigators/NarrowPa import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportsSplitNavigatorParamList, RootNavigatorParamList} from '@libs/Navigation/types'; -import closeReactNativeApp from '@userActions/HybridApp'; +import {closeReactNativeApp} from '@userActions/HybridApp'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; diff --git a/src/components/SignInButtons/GoogleSignIn/index.native.tsx b/src/components/SignInButtons/GoogleSignIn/index.native.tsx index 4a70a10ef0171..70144444d282f 100644 --- a/src/components/SignInButtons/GoogleSignIn/index.native.tsx +++ b/src/components/SignInButtons/GoogleSignIn/index.native.tsx @@ -1,20 +1,32 @@ import {GoogleSignin, statusCodes} from '@react-native-google-signin/google-signin'; import React from 'react'; import IconButton from '@components/SignInButtons/IconButton'; +import getPlatform from '@libs/getPlatform'; import Log from '@libs/Log'; -import * as Session from '@userActions/Session'; +import {beginGoogleSignIn} from '@userActions/Session'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import type {GoogleSignInProps} from '.'; import type GoogleError from './types'; +/** + * Helper function returning webClientId based on a platform used + */ +function getWebClientId() { + if (!CONFIG.IS_HYBRID_APP) { + return CONFIG.GOOGLE_SIGN_IN.WEB_CLIENT_ID; + } + + return getPlatform() === CONST.PLATFORM.ANDROID ? CONFIG.GOOGLE_SIGN_IN.HYBRID_APP.WEB_CLIENT_ID.ANDROID : CONFIG.GOOGLE_SIGN_IN.HYBRID_APP.WEB_CLIENT_ID.IOS; +} + /** * Google Sign In method for iOS and android that returns identityToken. */ function googleSignInRequest() { GoogleSignin.configure({ - webClientId: CONFIG.GOOGLE_SIGN_IN.WEB_CLIENT_ID, - iosClientId: CONFIG.GOOGLE_SIGN_IN.IOS_CLIENT_ID, + webClientId: getWebClientId(), + iosClientId: CONFIG.IS_HYBRID_APP ? CONFIG.GOOGLE_SIGN_IN.HYBRID_APP.IOS_CLIENT_ID : CONFIG.GOOGLE_SIGN_IN.IOS_CLIENT_ID, offlineAccess: false, }); @@ -25,7 +37,7 @@ function googleSignInRequest() { GoogleSignin.signIn() .then((response) => response.idToken) - .then((token) => Session.beginGoogleSignIn(token)) + .then((token) => beginGoogleSignIn(token)) .catch((error: GoogleError | undefined) => { // Handle unexpected error shape if (error?.code === undefined) { diff --git a/src/libs/HybridApp.ts b/src/libs/HybridApp.ts new file mode 100644 index 0000000000000..e1579b673fb37 --- /dev/null +++ b/src/libs/HybridApp.ts @@ -0,0 +1,112 @@ +import HybridAppModule from '@expensify/react-native-hybrid-app'; +import Onyx from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import CONFIG from '@src/CONFIG'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Credentials, HybridApp, Session, TryNewDot} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {closeReactNativeApp, setReadyToShowAuthScreens, setUseNewDotSignInPage} from './actions/HybridApp'; +import {isAnonymousUser} from './actions/Session'; +import Log from './Log'; +import {getCurrentUserEmail} from './Network/NetworkStore'; + +let currentHybridApp: OnyxEntry; +let currentTryNewDot: OnyxEntry; +let currentCredentials: OnyxEntry; +let currentSession: OnyxEntry; + +Onyx.connect({ + key: ONYXKEYS.HYBRID_APP, + callback: (hybridApp) => { + currentHybridApp = hybridApp; + handleChangeInHybridAppSignInFlow(hybridApp, currentTryNewDot, currentCredentials, currentSession); + }, +}); + +Onyx.connect({ + key: ONYXKEYS.NVP_TRY_NEW_DOT, + callback: (tryNewDot) => { + currentTryNewDot = tryNewDot; + handleChangeInHybridAppSignInFlow(currentHybridApp, tryNewDot, currentCredentials, currentSession); + }, +}); + +Onyx.connect({ + key: ONYXKEYS.CREDENTIALS, + callback: (credentials) => { + currentCredentials = credentials; + handleChangeInHybridAppSignInFlow(currentHybridApp, currentTryNewDot, credentials, currentSession); + }, +}); + +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (session: OnyxEntry) => { + if (!currentSession?.authToken && session?.authToken) { + handleChangeInHybridAppSignInFlow(currentHybridApp, currentTryNewDot, currentCredentials, session); + } else if (isAnonymousUser(currentSession) && !isAnonymousUser(session)) { + handleChangeInHybridAppSignInFlow(currentHybridApp, currentTryNewDot, currentCredentials, session, true); + } + currentSession = session; + }, +}); + +let activePolicyID: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, + callback: (newActivePolicyID) => { + activePolicyID = newActivePolicyID; + }, +}); + +function shouldUseOldApp(tryNewDot: TryNewDot) { + if (isEmptyObject(tryNewDot) || isEmptyObject(tryNewDot.classicRedirect)) { + return true; + } + return tryNewDot.classicRedirect.dismissed; +} + +function handleChangeInHybridAppSignInFlow( + hybridApp: OnyxEntry, + tryNewDot: OnyxEntry, + credentials: OnyxEntry, + session: OnyxEntry, + usingSignInModal = false, +) { + if (!CONFIG.IS_HYBRID_APP) { + return; + } + + if (!session?.authToken || (!hybridApp?.useNewDotSignInPage && !usingSignInModal)) { + return; + } + + if (isAnonymousUser()) { + setUseNewDotSignInPage(false).then(() => { + setReadyToShowAuthScreens(true); + }); + return; + } + + if (tryNewDot !== undefined && !!credentials?.autoGeneratedLogin && !!credentials?.autoGeneratedPassword) { + // It's better to not pass function directly to Log.info to avoid bugs with evaluation + const shouldUseOD = shouldUseOldApp(tryNewDot); + Log.info(`[HybridApp] Performing sign-in${shouldUseOD ? '' : ' (in background)'} on OldDot side`); + HybridAppModule.signInToOldDot({ + autoGeneratedLogin: credentials.autoGeneratedLogin, + autoGeneratedPassword: credentials.autoGeneratedPassword, + authToken: session.authToken, + email: getCurrentUserEmail() ?? '', + // eslint-disable-next-line rulesdir/no-default-id-values + policyID: activePolicyID ?? '', + }); + setUseNewDotSignInPage(false).then(() => { + if (shouldUseOD) { + closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false}); + } else { + Log.info('[HybridApp] The user should see NewDot. There is no need to block the user on the `SignInPage` until the sign-in process is completed on the OldDot side.'); + setReadyToShowAuthScreens(true); + } + }); + } +} diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index afe54338d961d..7c7fbba7b2e52 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -308,26 +308,23 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie init(); } - // In Hybrid App we decide to call one of those method when booting ND and we don't want to duplicate calls - if (!CONFIG.IS_HYBRID_APP) { - // If we are on this screen then we are "logged in", but the user might not have "just logged in". They could be reopening the app - // or returning from background. If so, we'll assume they have some app data already and we can call reconnectApp() instead of openApp() and connect() for delegator from OldDot. - if (SessionUtils.didUserLogInDuringSession() || delegatorEmail) { - if (delegatorEmail) { - connect(delegatorEmail, true) - ?.then((success) => { - App.setAppLoading(!!success); - }) - .finally(() => { - setIsDelegatorFromOldDotIsReady(true); - }); - } else { - App.openApp(); - } + // If we are on this screen then we are "logged in", but the user might not have "just logged in". They could be reopening the app + // or returning from background. If so, we'll assume they have some app data already and we can call reconnectApp() instead of openApp() and connect() for delegator from OldDot. + if (SessionUtils.didUserLogInDuringSession() || delegatorEmail) { + if (delegatorEmail) { + connect(delegatorEmail, true) + ?.then((success) => { + App.setAppLoading(!!success); + }) + .finally(() => { + setIsDelegatorFromOldDotIsReady(true); + }); } else { - Log.info('[AuthScreens] Sending ReconnectApp'); - App.reconnectApp(initialLastUpdateIDAppliedToClient); + App.openApp(); } + } else { + Log.info('[AuthScreens] Sending ReconnectApp'); + App.reconnectApp(initialLastUpdateIDAppliedToClient); } App.setUpPoliciesAndNavigate(session); diff --git a/src/libs/Navigation/AppNavigator/PublicScreens.tsx b/src/libs/Navigation/AppNavigator/PublicScreens.tsx index 993117174ba69..e5ea03e14387d 100644 --- a/src/libs/Navigation/AppNavigator/PublicScreens.tsx +++ b/src/libs/Navigation/AppNavigator/PublicScreens.tsx @@ -5,7 +5,6 @@ import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigati import {InternalPlatformAnimations} from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation'; import type {PublicScreensParamList} from '@navigation/types'; import ConnectionCompletePage from '@pages/ConnectionCompletePage'; -import SessionExpiredPage from '@pages/ErrorPage/SessionExpiredPage'; import LogInWithShortLivedAuthTokenPage from '@pages/LogInWithShortLivedAuthTokenPage'; import AppleSignInDesktopPage from '@pages/signin/AppleSignInDesktopPage'; import GoogleSignInDesktopPage from '@pages/signin/GoogleSignInDesktopPage'; @@ -13,7 +12,6 @@ import SAMLSignInPage from '@pages/signin/SAMLSignInPage'; import SignInPage from '@pages/signin/SignInPage'; import UnlinkLoginPage from '@pages/UnlinkLoginPage'; import ValidateLoginPage from '@pages/ValidateLoginPage'; -import CONFIG from '@src/CONFIG'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; import defaultScreenOptions from './defaultScreenOptions'; @@ -33,7 +31,7 @@ function PublicScreens() { { + if (!CONFIG.IS_HYBRID_APP) { + return authenticated; + } + + return authenticated && hybridApp?.readyToShowAuthScreens; + }, [hybridApp?.readyToShowAuthScreens, authenticated]); + + if (shouldShowAuthScreens) { const AuthScreens = require('./AuthScreens').default; // These are the protected screens and only accessible when an authToken is present diff --git a/src/libs/Notification/PushNotification/index.native.ts b/src/libs/Notification/PushNotification/index.native.ts index a38e9d98a8e45..854aeba8074f2 100644 --- a/src/libs/Notification/PushNotification/index.native.ts +++ b/src/libs/Notification/PushNotification/index.native.ts @@ -1,7 +1,8 @@ import type {PushPayload} from '@ua/react-native-airship'; -import Airship, {EventType} from '@ua/react-native-airship'; +import Airship, {EventType, PermissionStatus} from '@ua/react-native-airship'; import Log from '@libs/Log'; import ShortcutManager from '@libs/ShortcutManager'; +import CONFIG from '@src/CONFIG'; import ForegroundNotifications from './ForegroundNotifications'; import type {NotificationDataMap, NotificationTypes} from './NotificationType'; import NotificationType from './NotificationType'; @@ -79,18 +80,28 @@ const register: Register = (notificationID) => { Airship.contact .getNamedUserId() .then((userID) => { - if (userID === notificationID.toString()) { + // In the HybridApp, the contact identity is set on the YAPL side after sign-in. + // Since the Airship instance is shared between NewDot and OldDot, + // NewDot users won't see the push notification permission prompt as we return early in this case. + // Therefore, we cannot handle the HybridApp scenario here. + if (!CONFIG.IS_HYBRID_APP && userID === notificationID.toString()) { // No need to register again for this notificationID. return; } - // Get permissions to display push notifications (prompts user on iOS, but not Android) - Airship.push.enableUserNotifications().then((isEnabled) => { - if (isEnabled) { + // Get permissions to display push notifications if not determined (prompts user on iOS, but not Android) + Airship.push.getNotificationStatus().then(({notificationPermissionStatus}) => { + if (notificationPermissionStatus !== PermissionStatus.NotDetermined) { return; } - Log.info('[PushNotification] User has disabled visible push notifications for this app.'); + Airship.push.enableUserNotifications().then((isEnabled) => { + if (isEnabled) { + return; + } + + Log.info('[PushNotification] User has disabled visible push notifications for this app.'); + }); }); // Register this device as a named user in AirshipAPI. diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index b18bb62c2c23c..eed52c57f4590 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -136,6 +136,7 @@ const KEYS_TO_PRESERVE: OnyxKey[] = [ ONYXKEYS.NVP_PREFERRED_LOCALE, ONYXKEYS.CREDENTIALS, ONYXKEYS.PRESERVED_USER_SESSION, + ONYXKEYS.HYBRID_APP, ]; Onyx.connect({ diff --git a/src/libs/actions/Delegate.ts b/src/libs/actions/Delegate.ts index f60b5b1dfd881..0a06a3627c820 100644 --- a/src/libs/actions/Delegate.ts +++ b/src/libs/actions/Delegate.ts @@ -70,6 +70,7 @@ const KEYS_TO_PRESERVE_DELEGATE_ACCESS = [ ONYXKEYS.IS_LOADING_APP, ONYXKEYS.HAS_LOADED_APP, ONYXKEYS.STASHED_CREDENTIALS, + ONYXKEYS.HYBRID_APP, // We need to preserve the sidebar loaded state since we never unmount the sidebar when connecting as a delegate // This allows the report screen to load correctly when the delegate token expires and the delegate is returned to their original account. diff --git a/src/libs/actions/HybridApp/index.ts b/src/libs/actions/HybridApp/index.ts index 941d91ada9655..9c9342305a364 100644 --- a/src/libs/actions/HybridApp/index.ts +++ b/src/libs/actions/HybridApp/index.ts @@ -2,6 +2,8 @@ import HybridAppModule from '@expensify/react-native-hybrid-app'; import Onyx from 'react-native-onyx'; import CONFIG from '@src/CONFIG'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {HybridApp} from '@src/types/onyx'; +import type HybridAppSettings from './types'; function closeReactNativeApp({shouldSignOut, shouldSetNVP}: {shouldSignOut: boolean; shouldSetNVP: boolean}) { if (CONFIG.IS_HYBRID_APP) { @@ -11,4 +13,71 @@ function closeReactNativeApp({shouldSignOut, shouldSetNVP}: {shouldSignOut: bool HybridAppModule.closeReactNativeApp({shouldSignOut, shouldSetNVP}); } -export default closeReactNativeApp; +/* + * Parses initial settings passed from OldDot app + */ +function parseHybridAppSettings(hybridAppSettings: string): HybridAppSettings { + return JSON.parse(hybridAppSettings) as HybridAppSettings; +} + +/* + * Changes value of `readyToShowAuthScreens` + */ +function setReadyToShowAuthScreens(readyToShowAuthScreens: boolean) { + // This value is only relevant for HybridApp, so we can skip it in other environments. + if (!CONFIG.IS_HYBRID_APP) { + return; + } + Onyx.merge(ONYXKEYS.HYBRID_APP, {readyToShowAuthScreens}); +} + +function setUseNewDotSignInPage(useNewDotSignInPage: boolean) { + // This value is only relevant for HybridApp, so we can skip it in other environments. + if (!CONFIG.IS_HYBRID_APP) { + return Promise.resolve(); + } + return Onyx.merge(ONYXKEYS.HYBRID_APP, {useNewDotSignInPage}); +} + +function setClosingReactNativeApp(closingReactNativeApp: boolean) { + // This value is only relevant for HybridApp, so we can skip it in other environments. + if (!CONFIG.IS_HYBRID_APP) { + return; + } + Onyx.merge(ONYXKEYS.HYBRID_APP, {closingReactNativeApp}); +} + +/* + * Starts HybridApp sign-in flow from the beginning. + */ +function resetSignInFlow() { + // This value is only relevant for HybridApp, so we can skip it in other environments. + if (!CONFIG.IS_HYBRID_APP) { + return; + } + + Onyx.merge(ONYXKEYS.HYBRID_APP, { + readyToShowAuthScreens: false, + useNewDotSignInPage: true, + }); +} + +/* + * Updates Onyx state after start of React Native runtime based on initial `useNewDotSignInPage` value + */ +function prepareHybridAppAfterTransitionToNewDot(hybridApp: HybridApp) { + if (hybridApp?.useNewDotSignInPage) { + return Onyx.merge(ONYXKEYS.HYBRID_APP, { + ...hybridApp, + readyToShowAuthScreens: !(hybridApp?.useNewDotSignInPage ?? false), + }); + } + + // When we transition with useNewDotSignInPage === false, it means that we're already authenticated on NewDot side. + return Onyx.merge(ONYXKEYS.HYBRID_APP, { + ...hybridApp, + readyToShowAuthScreens: true, + }); +} + +export {parseHybridAppSettings, setReadyToShowAuthScreens, resetSignInFlow, prepareHybridAppAfterTransitionToNewDot, setUseNewDotSignInPage, setClosingReactNativeApp, closeReactNativeApp}; diff --git a/src/libs/actions/HybridApp/types.ts b/src/libs/actions/HybridApp/types.ts new file mode 100644 index 0000000000000..5d4ab1431cbc7 --- /dev/null +++ b/src/libs/actions/HybridApp/types.ts @@ -0,0 +1,11 @@ +import type ONYXKEYS from '@src/ONYXKEYS'; +import type {TryNewDot} from '@src/types/onyx'; +import type HybridApp from '@src/types/onyx/HybridApp'; + +type HybridAppSettings = { + [ONYXKEYS.HYBRID_APP]: HybridApp; + [ONYXKEYS.NVP_TRY_NEW_DOT]?: TryNewDot; + [ONYXKEYS.ACCOUNT]?: {shouldUseStagingServer: boolean}; +}; + +export default HybridAppSettings; diff --git a/src/libs/actions/QueuedOnyxUpdates.ts b/src/libs/actions/QueuedOnyxUpdates.ts index ef3f2c9c8ad81..e419c759950f1 100644 --- a/src/libs/actions/QueuedOnyxUpdates.ts +++ b/src/libs/actions/QueuedOnyxUpdates.ts @@ -32,6 +32,7 @@ function flushQueue(): Promise { if (!currentAccountID && !CONFIG.IS_TEST_ENV && !CONFIG.E2E_TESTING) { const preservedKeys: OnyxKey[] = [ + ONYXKEYS.NVP_TRY_NEW_DOT, ONYXKEYS.NVP_TRY_FOCUS_MODE, ONYXKEYS.PREFERRED_THEME, ONYXKEYS.NVP_PREFERRED_LOCALE, diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index a6b662dd40a6d..1df837bbc4f6d 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -3,7 +3,7 @@ import throttle from 'lodash/throttle'; import type {ChannelAuthorizationData} from 'pusher-js/types/src/core/auth/options'; import type {ChannelAuthorizationCallback} from 'pusher-js/with-encryption'; import {InteractionManager, Linking} from 'react-native'; -import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {OnyxEntry, OnyxKey, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as PersistedRequests from '@libs/actions/PersistedRequests'; import * as API from '@libs/API'; @@ -41,13 +41,15 @@ import NetworkConnection from '@libs/NetworkConnection'; import Pusher from '@libs/Pusher'; import {getReportIDFromLink, parseReportRouteParams as parseReportRouteParamsReportUtils} from '@libs/ReportUtils'; import * as SessionUtils from '@libs/SessionUtils'; +import {resetDidUserLogInDuringSession} from '@libs/SessionUtils'; import {clearSoundAssetsCache} from '@libs/Sound'; import Timers from '@libs/Timers'; import {hideContextMenu} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; -import {KEYS_TO_PRESERVE, openApp, reconnectApp} from '@userActions/App'; +import {KEYS_TO_PRESERVE, openApp} from '@userActions/App'; import {KEYS_TO_PRESERVE_DELEGATE_ACCESS} from '@userActions/Delegate'; import * as Device from '@userActions/Device'; -import closeReactNativeApp from '@userActions/HybridApp'; +import * as HybridAppActions from '@userActions/HybridApp'; +import type HybridAppSettings from '@userActions/HybridApp/types'; import redirectToSignIn from '@userActions/SignInRedirect'; import Timing from '@userActions/Timing'; import * as Welcome from '@userActions/Welcome'; @@ -57,6 +59,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; +import type {TryNewDot} from '@src/types/onyx'; import type Credentials from '@src/types/onyx/Credentials'; import type Locale from '@src/types/onyx/Locale'; import type Response from '@src/types/onyx/Response'; @@ -70,6 +73,7 @@ const INVALID_TOKEN = 'pizza'; let session: Session = {}; let authPromiseResolver: ((value: boolean) => void) | null = null; +let isHybridAppSetupFinished = false; let hasSwitchedAccountInHybridMode = false; Onyx.connect({ @@ -83,7 +87,7 @@ Onyx.connect({ authPromiseResolver(true); authPromiseResolver = null; } - if (CONFIG.IS_HYBRID_APP && session.authToken && session.authToken !== INVALID_TOKEN) { + if (CONFIG.IS_HYBRID_APP && isHybridAppSetupFinished && session.authToken && session.authToken !== INVALID_TOKEN && !isAnonymousUser(value)) { HybridAppModule.sendAuthToken({authToken: session.authToken}); } }, @@ -244,7 +248,7 @@ function isExpiredSession(sessionCreationDate: number): boolean { return new Date().getTime() - sessionCreationDate >= CONST.SESSION_EXPIRATION_TIME_MS; } -function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean, shouldKillHybridApp = true, shouldForceUseStashedSession?: boolean) { +function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean, shouldSignOutFromOldDot = true, shouldForceUseStashedSession?: boolean) { Log.info('Redirecting to Sign In because signOut() was called'); hideContextMenu(false); @@ -264,10 +268,9 @@ function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSess return; } - // In the HybridApp, we want the Old Dot to handle the sign out process - if (CONFIG.IS_HYBRID_APP && shouldKillHybridApp) { - closeReactNativeApp({shouldSignOut: true, shouldSetNVP: false}); - return; + // When signing out from the HybridApp, we need to sign out from the oldDot app as well + if (CONFIG.IS_HYBRID_APP && shouldSignOutFromOldDot) { + HybridAppModule.signOutFromOldDot(); } const isSupportal = isSupportAuthToken(); @@ -455,6 +458,7 @@ function beginSignIn(email: string) { const params: BeginSignInParams = {email}; + // eslint-disable-next-line rulesdir/no-api-side-effects-method API.read(READ_COMMANDS.BEGIN_SIGNIN, params, {optimisticData, successData, failureData}); } @@ -517,122 +521,76 @@ function signUpUser() { API.write(WRITE_COMMANDS.SIGN_UP_USER, params, {optimisticData, successData, failureData}); } -function getLastUpdateIDAppliedToClient(): Promise { - return new Promise((resolve) => { - Onyx.connect({ - key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, - callback: (value) => resolve(value ?? 0), - }); - }); -} +function setupNewDotAfterTransitionFromOldDot(hybridAppSettings: HybridAppSettings, tryNewDot?: TryNewDot) { + const {hybridApp, ...newDotOnyxValues} = hybridAppSettings; -type HybridAppSettings = { - email: string; - authToken: string; - accountID: number; - autoGeneratedLogin: string; - autoGeneratedPassword: string; - clearOnyxOnStart: boolean; - completedHybridAppOnboarding: boolean; - isSingleNewDotEntry: boolean; - isStaging: boolean; - primaryLogin: string; - encryptedAuthToken: string; - nudgeMigrationTimestamp?: string; - oldDotOriginalAccountEmail?: string; - stashedAuthToken?: string; - stashedAccountID?: string; - requiresTwoFactorAuth: boolean; - needsTwoFactorAuthSetup: boolean; -}; + const clearOnyxBeforeSignIn = () => { + if (!hybridApp.useNewDotSignInPage) { + return Promise.resolve(); + } -function signInAfterTransitionFromOldDot(hybridAppSettings: string) { - const { - email, - authToken, - encryptedAuthToken, - accountID, - autoGeneratedLogin, - autoGeneratedPassword, - clearOnyxOnStart, - completedHybridAppOnboarding, - nudgeMigrationTimestamp, - isSingleNewDotEntry, - isStaging, - primaryLogin, - oldDotOriginalAccountEmail, - stashedAuthToken, - stashedAccountID, - requiresTwoFactorAuth, - needsTwoFactorAuthSetup, - } = JSON.parse(hybridAppSettings) as HybridAppSettings; - - const clearOnyxForNewAccount = () => { - if (!clearOnyxOnStart) { + return redirectToSignIn(); + }; + + const resetDidUserLoginDuringSessionIfNeeded = () => { + if (newDotOnyxValues.nvp_tryNewDot === undefined || tryNewDot?.classicRedirect?.dismissed !== true) { return Promise.resolve(); } - // We also need to reset: - // - IS_LOADING_APP after sign in to ensure the condition to show ExplanationModal runs once - // https://github.com/Expensify/App/issues/57575#issuecomment-2780189425 - return Onyx.clear(KEYS_TO_PRESERVE) - .then(() => Onyx.merge(ONYXKEYS.ACCOUNT, {delegatedAccess: null})) - .then(() => Onyx.merge(ONYXKEYS.IS_LOADING_APP, null)); + Log.info("[HybridApp] OpenApp hasn't been called yet. Calling `resetDidUserLogInDuringSession`"); + resetDidUserLogInDuringSession(); }; - return clearOnyxForNewAccount() + return clearOnyxBeforeSignIn() .then(() => { // This section controls copilot changes const currentUserEmail = getCurrentUserEmail(); - // If OD is in copilot, stash the original account data - if (oldDotOriginalAccountEmail && oldDotOriginalAccountEmail !== email) { - return Onyx.multiSet({ - [ONYXKEYS.STASHED_SESSION]: {email: oldDotOriginalAccountEmail, authToken: stashedAuthToken, accountID: Number(stashedAccountID)}, - [ONYXKEYS.STASHED_CREDENTIALS]: {autoGeneratedLogin, autoGeneratedPassword}, - }); - } - - // If OD and ND account are the same - do nothing - if (email === currentUserEmail) { + // If ND and OD account are the same - do nothing + if (hybridApp?.delegateAccessData?.oldDotCurrentUserEmail === currentUserEmail) { return; } - // If account was changed to original one on OD side - clear onyx - if (!oldDotOriginalAccountEmail) { - return Onyx.clear(KEYS_TO_PRESERVE_DELEGATE_ACCESS); - } - - // If we're already logged in - do nothing, data will be set in next step - if (currentUserEmail) { - return; - } - - // If we're not logged in - set stashed data - return Onyx.multiSet({ - [ONYXKEYS.STASHED_CREDENTIALS]: {autoGeneratedLogin, autoGeneratedPassword}, - }); + const stashedData = hybridApp?.delegateAccessData?.isDelegateAccess + ? { + [ONYXKEYS.STASHED_CREDENTIALS]: credentials, + [ONYXKEYS.STASHED_SESSION]: session, + } + : { + [ONYXKEYS.STASHED_CREDENTIALS]: {}, + [ONYXKEYS.STASHED_SESSION]: {}, + }; + + // Account was changed on OD side - clear onyx and apply data + return Onyx.clear(KEYS_TO_PRESERVE_DELEGATE_ACCESS).then(() => + Onyx.multiSet({ + ...stashedData, + [ONYXKEYS.SESSION]: { + email: hybridApp?.delegateAccessData?.oldDotCurrentUserEmail, + authToken: hybridApp?.delegateAccessData?.oldDotCurrentAuthToken, + encryptedAuthToken: decodeURIComponent(hybridApp?.delegateAccessData?.oldDotCurrentEncryptedAuthToken ?? ''), + accountID: hybridApp?.delegateAccessData?.oldDotCurrentAccountID, + }, + [ONYXKEYS.CREDENTIALS]: { + autoGeneratedLogin: credentials?.autoGeneratedLogin, + autoGeneratedPassword: credentials?.autoGeneratedPassword, + }, + }) + .then(() => Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin: hybridApp?.delegateAccessData?.oldDotCurrentUserEmail})) + .then(() => openApp()), + ); }) .then(() => - Onyx.multiSet({ - [ONYXKEYS.SESSION]: {email, authToken, encryptedAuthToken: decodeURIComponent(encryptedAuthToken), accountID: Number(accountID)}, - [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin, autoGeneratedPassword}, - [ONYXKEYS.NVP_TRY_NEW_DOT]: { - classicRedirect: {completedHybridAppOnboarding}, - nudgeMigration: nudgeMigrationTimestamp ? {timestamp: new Date(nudgeMigrationTimestamp)} : undefined, - }, - [ONYXKEYS.ACCOUNT]: {shouldUseStagingServer: isStaging}, - }) - .then(() => Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin, requiresTwoFactorAuth, needsTwoFactorAuthSetup})) - .then(() => Onyx.merge(ONYXKEYS.HYBRID_APP, {isSingleNewDotEntry, closingReactNativeApp: false})), + HybridAppActions.prepareHybridAppAfterTransitionToNewDot({ + ...hybridApp, + closingReactNativeApp: false, + }), ) + .then(resetDidUserLoginDuringSessionIfNeeded) + .then(() => Promise.all(Object.entries(newDotOnyxValues).map(([key, value]) => Onyx.merge(key as OnyxKey, value ?? {})))) .then(() => { - if (clearOnyxOnStart) { - return openApp(); - } - return getLastUpdateIDAppliedToClient().then((lastUpdateId) => { - return reconnectApp(lastUpdateId); - }); + isHybridAppSetupFinished = true; + return Promise.resolve(); }) .catch((error) => { Log.hmmm('[HybridApp] Initialization of HybridApp has failed. Forcing transition', {error}); @@ -737,7 +695,11 @@ function signIn(validateCode: string, twoFactorAuthCode?: string) { params.validateCode = validateCode || credentials.validateCode; } - API.write(WRITE_COMMANDS.SIGN_IN_USER, params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.SIGN_IN_USER, params, { + optimisticData, + successData, + failureData, + }); }); } @@ -1477,7 +1439,7 @@ export { isSupportAuthToken, hasStashedSession, signUpUser, - signInAfterTransitionFromOldDot, + setupNewDotAfterTransitionFromOldDot, AddWorkEmail, MergeIntoAccountAndLogin, resetSMSDeliveryFailureStatus, diff --git a/src/libs/actions/SignInRedirect.ts b/src/libs/actions/SignInRedirect.ts index 626641927b687..da39079dc5123 100644 --- a/src/libs/actions/SignInRedirect.ts +++ b/src/libs/actions/SignInRedirect.ts @@ -1,8 +1,10 @@ import Onyx from 'react-native-onyx'; import {getMicroSecondOnyxErrorWithMessage} from '@libs/ErrorUtils'; import {clearSessionStorage} from '@libs/Navigation/helpers/lastVisitedTabPathUtils'; +import CONFIG from '@src/CONFIG'; import type {OnyxKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; +import {resetSignInFlow} from './HybridApp'; import {clearAllPolicies} from './Policy/Policy'; let currentIsOffline: boolean | undefined; @@ -33,6 +35,9 @@ function clearStorageAndRedirect(errorMessage?: string): Promise { } return Onyx.clear(keysToPreserve).then(() => { + if (CONFIG.IS_HYBRID_APP) { + resetSignInFlow(); + } clearAllPolicies(); if (!errorMessage) { return; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index c0d7a21ccf26e..d486c27391a7c 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -117,6 +117,11 @@ function closeAccount(reason: string) { optimisticData, failureData, }); + + // On HybridApp, we need to sign out from the oldDot app as well to keep state of both apps in sync + if (CONFIG.IS_HYBRID_APP) { + HybridAppModule.signOutFromOldDot(); + } } /** diff --git a/src/pages/ErrorPage/SessionExpiredPage.tsx b/src/pages/ErrorPage/SessionExpiredPage.tsx index 4972b8e6459e6..0ef53fe11ecf1 100644 --- a/src/pages/ErrorPage/SessionExpiredPage.tsx +++ b/src/pages/ErrorPage/SessionExpiredPage.tsx @@ -9,7 +9,7 @@ import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import closeReactNativeApp from '@userActions/HybridApp'; +import {closeReactNativeApp} from '@userActions/HybridApp'; import {clearSignInData} from '@userActions/Session'; import CONFIG from '@src/CONFIG'; diff --git a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx index 0680533b7d146..c99db4de674dd 100644 --- a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx +++ b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx @@ -21,7 +21,6 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import closeReactNativeApp from '@libs/actions/HybridApp'; import {openOldDotLink} from '@libs/actions/Link'; import {setOnboardingAdminsChatReportID, setOnboardingPolicyID, setOnboardingUserReportedIntegration} from '@libs/actions/Welcome'; import Navigation from '@libs/Navigation/Navigation'; @@ -29,6 +28,7 @@ import {waitForIdle} from '@libs/Network/SequentialQueue'; import {shouldOnboardingRedirectToOldDot} from '@libs/OnboardingUtils'; import {isPaidGroupPolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import variables from '@styles/variables'; +import {closeReactNativeApp} from '@userActions/HybridApp'; import CONFIG from '@src/CONFIG'; import type {OnboardingAccounting} from '@src/CONST'; import CONST from '@src/CONST'; diff --git a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx index c5e6a5d32ed0e..6236aeb2c9a08 100644 --- a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx +++ b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx @@ -31,7 +31,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {waitForIdle} from '@libs/Network/SequentialQueue'; import {shouldOnboardingRedirectToOldDot} from '@libs/OnboardingUtils'; import {isPaidGroupPolicy, isPolicyAdmin} from '@libs/PolicyUtils'; -import closeReactNativeApp from '@userActions/HybridApp'; +import {closeReactNativeApp} from '@userActions/HybridApp'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index 0cf834958bfc4..ccf75fe922fda 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -46,7 +46,7 @@ import {getQuickActionIcon, getQuickActionTitle, isQuickActionAllowed} from '@li import {generateReportID, getDisplayNameForParticipant, getIcons, getReportName, getWorkspaceChats, isPolicyExpenseChat} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import variables from '@styles/variables'; -import closeReactNativeApp from '@userActions/HybridApp'; +import {closeReactNativeApp} from '@userActions/HybridApp'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index f35fcb559a57f..2f2c581f81072 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -29,6 +29,7 @@ import useSingleExecution from '@hooks/useSingleExecution'; import useSubscriptionPlan from '@hooks/useSubscriptionPlan'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import '@libs/actions/Delegate'; import {resetExitSurveyForm} from '@libs/actions/ExitSurvey'; import {checkIfFeedConnectionIsBroken} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; @@ -40,7 +41,7 @@ import type SETTINGS_TO_RHP from '@navigation/linkingConfig/RELATIONS/SETTINGS_T import {showContextMenu} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import variables from '@styles/variables'; import {confirmReadyToOpenApp} from '@userActions/App'; -import closeReactNativeApp from '@userActions/HybridApp'; +import {closeReactNativeApp} from '@userActions/HybridApp'; import {openExternalLink} from '@userActions/Link'; import {hasPaymentMethodError} from '@userActions/PaymentMethods'; import {isSupportAuthToken, signOutAndRedirectToSignIn} from '@userActions/Session'; diff --git a/src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx b/src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx index 233811f8cf8be..9a0e29bf933a1 100644 --- a/src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx +++ b/src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx @@ -17,7 +17,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import closeReactNativeApp from '@userActions/HybridApp'; +import {closeReactNativeApp} from '@userActions/HybridApp'; import {openOldDotLink} from '@userActions/Link'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; diff --git a/src/pages/signin/SignUpWelcomeForm.tsx b/src/pages/signin/SignUpWelcomeForm.tsx index 203e799770f05..20ae01b52d7c4 100644 --- a/src/pages/signin/SignUpWelcomeForm.tsx +++ b/src/pages/signin/SignUpWelcomeForm.tsx @@ -6,8 +6,9 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ErrorUtils from '@libs/ErrorUtils'; -import * as Session from '@userActions/Session'; +import {getLatestErrorMessage} from '@libs/ErrorUtils'; +import {setReadyToShowAuthScreens} from '@userActions/HybridApp'; +import {clearSignInData, signUpUser} from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; import ChangeExpensifyLoginLink from './ChangeExpensifyLoginLink'; import Terms from './Terms'; @@ -16,8 +17,8 @@ function SignUpWelcomeForm() { const network = useNetwork(); const styles = useThemeStyles(); const {translate} = useLocalize(); - const [account] = useOnyx(ONYXKEYS.ACCOUNT); - const serverErrorText = useMemo(() => (account ? ErrorUtils.getLatestErrorMessage(account) : ''), [account]); + const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: false}); + const serverErrorText = useMemo(() => (account ? getLatestErrorMessage(account) : ''), [account]); return ( <> @@ -28,7 +29,10 @@ function SignUpWelcomeForm() { large text={translate('welcomeSignUpForm.join')} isLoading={account?.isLoading} - onPress={() => Session.signUpUser()} + onPress={() => { + signUpUser(); + setReadyToShowAuthScreens(true); + }} pressOnEnter style={[styles.mb2]} /> @@ -38,7 +42,7 @@ function SignUpWelcomeForm() { message={serverErrorText} /> )} - Session.clearSignInData()} /> + clearSignInData()} /> diff --git a/src/types/onyx/HybridApp.ts b/src/types/onyx/HybridApp.ts index 1cdac26938d11..46a86e701c50a 100644 --- a/src/types/onyx/HybridApp.ts +++ b/src/types/onyx/HybridApp.ts @@ -1,8 +1,41 @@ +/** Data structure holding user's OldDot access information */ +type HybridAppDelegateAccessData = { + /** Indicates if OldDot is accessed in a delegate mode */ + isDelegateAccess?: boolean; + + /** Email address through which the user is currently authenticated in OldDot */ + oldDotCurrentUserEmail?: string; + + /** Authentication token used in OldDot */ + oldDotCurrentAuthToken?: string; + + /** Encrypted authentication token used in OldDot */ + oldDotCurrentEncryptedAuthToken?: string; + + /** Account ID for the user in OldDot */ + oldDotCurrentAccountID?: number; +}; + /** State and configuration of a HybridApp */ type HybridApp = { + /** Stores the information if HybridApp uses NewDot's sign-in flow */ + useNewDotSignInPage?: boolean; + + /** Determines if the AuthScreens are ready to be displayed */ + readyToShowAuthScreens?: boolean; + /** Specifies if the transition from OldDot was made to display a specific subset of screens in NewDot */ isSingleNewDotEntry?: boolean; + /** Indicates if the last sign out action was performed from OldDot */ + loggedOutFromOldDot?: boolean; + + /** Determines whether to remove delegated access */ + shouldRemoveDelegatedAccess?: boolean; + + /** Holds delegate access information */ + delegateAccessData?: HybridAppDelegateAccessData; + /** Indicates if NewDot is being closed */ closingReactNativeApp?: boolean; }; diff --git a/src/types/onyx/TryNewDot.ts b/src/types/onyx/TryNewDot.ts index 8d34b2efa18c2..4d79b0f36c49b 100644 --- a/src/types/onyx/TryNewDot.ts +++ b/src/types/onyx/TryNewDot.ts @@ -5,11 +5,11 @@ type TryNewDot = { /** * This key is mostly used on OldDot. In NewDot, we only use `completedHybridAppOnboarding`. */ - classicRedirect: { + classicRedirect?: { /** * Indicates if transition from OldDot to NewDot should happen in HybridApp. */ - dismissed: boolean | string; + dismissed: boolean; /** * Indicates timestamp of an action. */ @@ -23,7 +23,7 @@ type TryNewDot = { /** * This key is added when user is migrated from OldDot to NewDot with nudge migration as part of a cohort. */ - nudgeMigration: { + nudgeMigration?: { /** Indicates timestamp of an action. */ timestamp: Date; };