From bf87d42365a18ead5d858d4d1877faf0872750e9 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Mon, 30 Jun 2025 13:06:55 +0200 Subject: [PATCH 01/29] Send hybridAppSettings via native method --- .../ReactNativeHybridApp.kt | 5 ++++ .../hybrid-app/ios/ReactNativeHybridApp.mm | 5 ++++ .../src/NativeReactNativeHybridApp.ts | 1 + modules/hybrid-app/src/index.native.ts | 4 +++ modules/hybrid-app/src/index.ts | 5 ++++ modules/hybrid-app/src/types.ts | 1 + src/App.tsx | 6 ++-- src/HybridAppHandler.tsx | 30 ++++++++++++------- 8 files changed, 43 insertions(+), 14 deletions(-) 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 46c7968d6cca5..0255a1530c13e 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 @@ -3,6 +3,7 @@ package com.expensify.reactnativehybridapp import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.module.annotations.ReactModule import android.util.Log +import com.facebook.react.bridge.Promise @ReactModule(name = NativeReactNativeHybridAppSpec.NAME) class ReactNativeHybridApp(reactContext: ReactApplicationContext) : @@ -54,4 +55,8 @@ class ReactNativeHybridApp(reactContext: ReactApplicationContext) : override fun clearOldDotAfterSignOut() { Log.d(NAME, "`clearOldDotAfterSignOut` should never be called in standalone `New Expensify` app") } + + override fun getHybridAppSettings(promise: Promise) { + promise.reject("NOT_IMPLEMENTED", "getHybridAppSettings is not implemented in standalone New Expensify app") + } } diff --git a/modules/hybrid-app/ios/ReactNativeHybridApp.mm b/modules/hybrid-app/ios/ReactNativeHybridApp.mm index f94bccf5d5605..866562d4842be 100644 --- a/modules/hybrid-app/ios/ReactNativeHybridApp.mm +++ b/modules/hybrid-app/ios/ReactNativeHybridApp.mm @@ -39,6 +39,11 @@ - (void)clearOldDotAfterSignOut { NSLog(@"[ReactNativeHybridApp] `clearOldDotAfterSignOut` should never be called in standalone `New Expensify` app"); } +- (void)getHybridAppSettings:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + reject(@"NOT_IMPLEMENTED", @"This method is not available in standalone New Expensify app", nil); +} + - (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 4001adacff494..ccf045b21ac5e 100644 --- a/modules/hybrid-app/src/NativeReactNativeHybridApp.ts +++ b/modules/hybrid-app/src/NativeReactNativeHybridApp.ts @@ -12,6 +12,7 @@ export interface Spec extends TurboModule { signInToOldDot: (autoGeneratedLogin: string, autoGeneratedPassword: string, authToken: string, email: string, policyID: string) => void; signOutFromOldDot: () => void; clearOldDotAfterSignOut: () => void; + getHybridAppSettings: () => Promise; } export default TurboModuleRegistry.getEnforcing('ReactNativeHybridApp'); diff --git a/modules/hybrid-app/src/index.native.ts b/modules/hybrid-app/src/index.native.ts index 83d342cf922f4..b93fb42dfa4fd 100644 --- a/modules/hybrid-app/src/index.native.ts +++ b/modules/hybrid-app/src/index.native.ts @@ -29,6 +29,10 @@ const HybridAppModule: HybridAppModuleType = { clearOldDotAfterSignOut() { ReactNativeHybridApp.clearOldDotAfterSignOut(); }, + + getHybridAppSettings() { + return ReactNativeHybridApp.getHybridAppSettings(); + }, }; export default HybridAppModule; diff --git a/modules/hybrid-app/src/index.ts b/modules/hybrid-app/src/index.ts index f9c8bb22ec7d5..790f781af1590 100644 --- a/modules/hybrid-app/src/index.ts +++ b/modules/hybrid-app/src/index.ts @@ -36,6 +36,11 @@ const HybridAppModule: HybridAppModuleType = { // eslint-disable-next-line no-console console.warn('HybridAppModule: `clearOldDotAfterSignOut` should never be called on web'); }, + getHybridAppSettings() { + // eslint-disable-next-line no-console + console.warn('HybridAppModule: `getHybridAppSettings` should never be called on web'); + return Promise.resolve(null); + }, }; export default HybridAppModule; diff --git a/modules/hybrid-app/src/types.ts b/modules/hybrid-app/src/types.ts index 7d33f31446f06..b168868b990aa 100644 --- a/modules/hybrid-app/src/types.ts +++ b/modules/hybrid-app/src/types.ts @@ -8,6 +8,7 @@ type HybridAppModuleType = { signInToOldDot: (args: {autoGeneratedLogin: string; autoGeneratedPassword: string; authToken: string; email: string; policyID: string}) => void; signOutFromOldDot: () => void; clearOldDotAfterSignOut: () => void; + getHybridAppSettings: () => Promise; }; export default HybridAppModuleType; diff --git a/src/App.tsx b/src/App.tsx index 35251f2a55641..f49532c2459f2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -55,8 +55,6 @@ import {SplashScreenStateContextProvider} from './SplashScreenStateContext'; type AppProps = { /** The URL specifying the initial navigation destination when the app opens */ url?: Route; - /** Serialized configuration data required to initialize the React Native app (e.g. authentication details) */ - hybridAppSettings?: string; }; LogBox.ignoreLogs([ @@ -72,7 +70,7 @@ const fill = {flex: 1}; const StrictModeWrapper = CONFIG.USE_REACT_STRICT_MODE_IN_DEV ? React.StrictMode : ({children}: {children: React.ReactElement}) => children; -function App({url, hybridAppSettings}: AppProps) { +function App({url}: AppProps) { useDefaultDragAndDrop(); OnyxUpdateManager(); @@ -80,7 +78,7 @@ function App({url, hybridAppSettings}: AppProps) { - + { - if (parsedHybridAppSettings.hybridApp?.loggedOutFromOldDot) { - setSplashScreenState(CONST.BOOT_SPLASH_STATE.HIDDEN); - } else if (splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE) { - setSplashScreenState(CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN); + HybridAppModule.getHybridAppSettings().then((hybridAppSettings: string | null) => { + if(!hybridAppSettings) { + // Native method can send non-null value only once per NewDot lifecycle. It prevents issues with multiple initializations during reloads on debug builds. + Log.info('[HybridApp] `getHybridAppSettings` called more than once during single NewDot lifecycle. Skipping initialization.'); + return; } - setSignInHandled(true); + + const parsedHybridAppSettings = parseHybridAppSettings(hybridAppSettings); + + setupNewDotAfterTransitionFromOldDot(parsedHybridAppSettings, tryNewDot).then(() => { + if (parsedHybridAppSettings.hybridApp?.loggedOutFromOldDot) { + setSplashScreenState(CONST.BOOT_SPLASH_STATE.HIDDEN); + } else if (splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE) { + setSplashScreenState(CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN); + } + setSignInHandled(true); + }); }); return null; From 15eed0eadb3874e580566300d1eb7ffd237f644c Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 2 Jul 2025 09:00:26 +0200 Subject: [PATCH 02/29] Add log --- .../com/expensify/reactnativehybridapp/ReactNativeHybridApp.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 0255a1530c13e..655ab6ac75ee0 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 @@ -56,7 +56,8 @@ class ReactNativeHybridApp(reactContext: ReactApplicationContext) : Log.d(NAME, "`clearOldDotAfterSignOut` should never be called in standalone `New Expensify` app") } - override fun getHybridAppSettings(promise: Promise) { + override fun getHybridAppSettings(promise: Promise) { + Log.d(NAME, "`getHybridAppSettings` should never be called in standalone `New Expensify` app") promise.reject("NOT_IMPLEMENTED", "getHybridAppSettings is not implemented in standalone New Expensify app") } } From 296cb7e853c09758af3250cd292eaea47eaf6900 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 2 Jul 2025 09:05:34 +0200 Subject: [PATCH 03/29] Fix typo --- src/HybridAppHandler.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HybridAppHandler.tsx b/src/HybridAppHandler.tsx index 2cf9b9a9d6c7d..3a00570a977d5 100644 --- a/src/HybridAppHandler.tsx +++ b/src/HybridAppHandler.tsx @@ -1,7 +1,7 @@ import {useContext, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import HybridAppModule from "@expensify/react-native-hybrid-app/src/index.native"; -import Log from './libs/__mocks__/Log'; +import Log from './libs/Log'; import CONFIG from './CONFIG'; import CONST from './CONST'; import {parseHybridAppSettings} from './libs/actions/HybridApp'; From 259a5149f840027da7c2f3ef7112d668ab0f86a8 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 2 Jul 2025 09:49:06 +0200 Subject: [PATCH 04/29] Apply prettier --- src/App.tsx | 2 +- src/HybridAppHandler.tsx | 42 ++++++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 089ccafe2822b..02c512a427497 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -78,7 +78,7 @@ function App({url}: AppProps) { - + { - if(!hybridAppSettings) { - // Native method can send non-null value only once per NewDot lifecycle. It prevents issues with multiple initializations during reloads on debug builds. - Log.info('[HybridApp] `getHybridAppSettings` called more than once during single NewDot lifecycle. Skipping initialization.'); + useEffect(() => { + if (!CONFIG.IS_HYBRID_APP || isLoading) { return; } - const parsedHybridAppSettings = parseHybridAppSettings(hybridAppSettings); - - setupNewDotAfterTransitionFromOldDot(parsedHybridAppSettings, tryNewDot).then(() => { - if (parsedHybridAppSettings.hybridApp?.loggedOutFromOldDot) { - setSplashScreenState(CONST.BOOT_SPLASH_STATE.HIDDEN); - } else if (splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE) { - setSplashScreenState(CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN); + HybridAppModule.getHybridAppSettings().then((hybridAppSettings: string | null) => { + if (!hybridAppSettings) { + // Native method can send non-null value only once per NewDot lifecycle. It prevents issues with multiple initializations during reloads on debug builds. + Log.info('[HybridApp] `getHybridAppSettings` called more than once during single NewDot lifecycle. Skipping initialization.'); + return; } - setSignInHandled(true); + + const parsedHybridAppSettings = parseHybridAppSettings(hybridAppSettings); + + setupNewDotAfterTransitionFromOldDot(parsedHybridAppSettings, tryNewDot).then(() => { + if (parsedHybridAppSettings.hybridApp?.loggedOutFromOldDot) { + setSplashScreenState(CONST.BOOT_SPLASH_STATE.HIDDEN); + } else if (splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE) { + setSplashScreenState(CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN); + } + }); }); - }); + }, [isLoading, setSplashScreenState, splashScreenState, tryNewDot]); return null; } From 503fc0ea6cdfd6e3e8b448183a4e05e129347553 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 2 Jul 2025 10:00:49 +0200 Subject: [PATCH 05/29] Add missing log in iOS turbo module --- modules/hybrid-app/ios/ReactNativeHybridApp.mm | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/hybrid-app/ios/ReactNativeHybridApp.mm b/modules/hybrid-app/ios/ReactNativeHybridApp.mm index 866562d4842be..cc00253079752 100644 --- a/modules/hybrid-app/ios/ReactNativeHybridApp.mm +++ b/modules/hybrid-app/ios/ReactNativeHybridApp.mm @@ -41,6 +41,7 @@ - (void)clearOldDotAfterSignOut { - (void)getHybridAppSettings:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + NSLog(@"[ReactNativeHybridApp] `getHybridAppSettings` should never be called in standalone `New Expensify` app"); reject(@"NOT_IMPLEMENTED", @"This method is not available in standalone New Expensify app", nil); } From c07874f6a7e50ff0d6115122175ca0c275888c3b Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Thu, 3 Jul 2025 12:03:06 +0200 Subject: [PATCH 06/29] Revert "Revert "Bring back new sign-in page"" This reverts commit def8188dc8b6754059394867fb091c0d01a603a7. --- __mocks__/@ua/react-native-airship.ts | 9 +- src/App.tsx | 87 ++++---- src/CONFIG.ts | 5 + src/CONST/index.ts | 6 + src/Expensify.tsx | 11 +- src/HybridAppHandler.tsx | 1 + src/ONYXKEYS.ts | 8 +- src/components/BookTravelButton.tsx | 17 +- .../CustomStatusBarAndBackground/index.tsx | 5 +- src/components/ScreenWrapper/index.tsx | 11 +- .../AppleSignIn/index.android.tsx | 8 +- .../SignInButtons/AppleSignIn/index.ios.tsx | 8 +- .../GoogleSignIn/index.native.tsx | 12 +- src/hooks/useOnboardingFlow.ts | 2 +- src/libs/HybridApp.ts | 100 ++++++++++ .../Navigation/AppNavigator/AuthScreens.tsx | 33 ++-- .../Navigation/AppNavigator/PublicScreens.tsx | 4 +- .../Navigation/AppNavigator/index.native.tsx | 17 +- .../PushNotification/index.native.ts | 23 ++- .../subscribeToPushNotifications.ts | 4 +- src/libs/actions/App.ts | 1 + src/libs/actions/Delegate.ts | 1 + src/libs/actions/HybridApp/index.ts | 89 +++++++++ src/libs/actions/HybridApp/types.ts | 11 ++ src/libs/actions/QueuedOnyxUpdates.ts | 1 + src/libs/actions/Report.ts | 5 + src/libs/actions/Session/index.ts | 186 ++++++++---------- src/libs/actions/SignInRedirect.ts | 5 + src/libs/actions/User.ts | 5 + src/pages/ErrorPage/SessionExpiredPage.tsx | 4 +- .../BaseOnboardingAccounting.tsx | 11 +- src/pages/ValidateLoginPage/index.tsx | 15 +- .../FloatingActionButtonAndPopover.tsx | 11 +- src/pages/settings/InitialSettingsPage.tsx | 12 +- .../MergeAccounts/MergeResultPage.tsx | 11 +- src/pages/signin/ChooseSSOOrMagicCode.tsx | 51 +++-- .../signin/SAMLSignInPage/index.native.tsx | 7 +- src/pages/signin/SignInPage.tsx | 11 ++ src/pages/signin/SignUpWelcomeForm.tsx | 18 +- .../ValidateCodeForm/BaseValidateCodeForm.tsx | 31 ++- src/types/onyx/HybridApp.ts | 49 +++++ src/types/onyx/TryNewDot.ts | 6 +- src/types/onyx/index.ts | 2 + 43 files changed, 620 insertions(+), 294 deletions(-) create mode 100644 src/libs/HybridApp.ts create mode 100644 src/libs/actions/HybridApp/index.ts create mode 100644 src/libs/actions/HybridApp/types.ts create mode 100644 src/types/onyx/HybridApp.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/src/App.tsx b/src/App.tsx index 6eac37c6f2db2..02c512a427497 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 type {Route} from './ROUTES'; import './setup/backgroundTask'; @@ -79,47 +80,53 @@ function App({url}: AppProps) { - - - - - - - - - + + + + + + + + + + diff --git a/src/CONFIG.ts b/src/CONFIG.ts index aa6e3a325d499..569995f679c99 100644 --- a/src/CONFIG.ts +++ b/src/CONFIG.ts @@ -95,7 +95,12 @@ 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', + // cspell:disable-next-line + HYBRID_APP_WEB_CLIENT_ID: '1008697809946-5e095eqem3o6ugtpc2rjf7v880tcp28p.apps.googleusercontent.com', + // cspell:disable-next-line + HYBRID_APP_IOS_CLIENT_ID: '1008697809946-sh04nqq0hea396s1qdqqbj6ia649odb2.apps.googleusercontent.com', }, GCP_GEOLOCATION_API_KEY: googleGeolocationAPIKey, FIREBASE_WEB_CONFIG: { diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 353d210eaf7d4..6cc855cbe6d3f 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6718,6 +6718,12 @@ const CONST = { HIDDEN: `hidden`, }, + HYBRID_APP_SIGN_IN_STATE: { + NOT_STARTED: 'notStarted', + STARTED: 'started', + FINISHED: 'finished', + }, + CSV_IMPORT_COLUMNS: { EMAIL: 'email', NAME: 'name', diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 587e529ec8a4a..65c72ffd0ceb3 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -99,6 +99,7 @@ function Expensify() { const [isSidebarLoaded] = useOnyx(ONYXKEYS.IS_SIDEBAR_LOADED, {canBeMissing: true}); const [screenShareRequest] = useOnyx(ONYXKEYS.SCREEN_SHARE_REQUEST, {canBeMissing: true}); const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH, {canBeMissing: true}); + const [hybridApp] = useOnyx(ONYXKEYS.HYBRID_APP, {canBeMissing: true}); useDebugShortcut(); @@ -114,10 +115,12 @@ function Expensify() { const isAuthenticated = useIsAuthenticated(); const autoAuthState = useMemo(() => session?.autoAuthState ?? '', [session]); - const shouldInit = isNavigationReady && hasAttemptedToOpenPublicRoom && !!preferredLocale; - 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 && (CONFIG.IS_HYBRID_APP ? !hybridApp?.loggedOutFromOldDot : true); + const shouldHideSplash = + shouldInit && + (CONFIG.IS_HYBRID_APP + ? splashScreenState === CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN && (isAuthenticated || !!hybridApp?.useNewDotSignInPage) + : splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE); const initializeClient = () => { if (!Visibility.isVisible()) { diff --git a/src/HybridAppHandler.tsx b/src/HybridAppHandler.tsx index ad4ccb558755a..0e19acbab40b0 100644 --- a/src/HybridAppHandler.tsx +++ b/src/HybridAppHandler.tsx @@ -8,6 +8,7 @@ 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 {splashScreenState, setSplashScreenState} = useContext(SplashScreenStateContext); diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index cfb6a38be0a11..fbe2c23a47c49 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -476,9 +476,6 @@ const ONYXKEYS = { /** Stores recently used currencies */ RECENTLY_USED_CURRENCIES: 'nvp_recentlyUsedCurrencies', - /** States whether we transitioned from OldDot to show only certain group of screens. It should be undefined on pure NewDot. */ - IS_SINGLE_NEW_DOT_ENTRY: 'isSingleNewDotEntry', - /** Company cards custom names */ NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES: 'nvp_expensify_ccCustomNames', @@ -535,6 +532,9 @@ const ONYXKEYS = { /** Set this gets redirected from global reimbursements flow */ IS_COMING_FROM_GLOBAL_REIMBURSEMENTS_FLOW: 'isComingFromGlobalReimbursementsFlow', + /** Stores HybridApp specific state required to interoperate with OldDot */ + HYBRID_APP: 'hybridApp', + /** Stores information for OpenUnreportedExpensesPage API call pagination */ HAS_MORE_UNREPORTED_TRANSACTIONS_RESULTS: 'hasMoreUnreportedTransactionsResults', @@ -1172,7 +1172,6 @@ type OnyxValuesMapping = { [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx; [ONYXKEYS.IMPORTED_SPREADSHEET]: OnyxTypes.ImportedSpreadsheet; [ONYXKEYS.LAST_ROUTE]: string; - [ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY]: boolean | undefined; [ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean; [ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record; [ONYXKEYS.CONCIERGE_REPORT_ID]: string; @@ -1191,6 +1190,7 @@ type OnyxValuesMapping = { [ONYXKEYS.SCHEDULE_CALL_DRAFT]: OnyxTypes.ScheduleCallDraft; [ONYXKEYS.IS_FORCED_TO_CHANGE_CURRENCY]: boolean | undefined; [ONYXKEYS.IS_COMING_FROM_GLOBAL_REIMBURSEMENTS_FLOW]: boolean | undefined; + [ONYXKEYS.HYBRID_APP]: OnyxTypes.HybridApp; [ONYXKEYS.HAS_MORE_UNREPORTED_TRANSACTIONS_RESULTS]: boolean | undefined; [ONYXKEYS.IS_LOADING_UNREPORTED_TRANSACTIONS]: boolean | undefined; [ONYXKEYS.NVP_LAST_ECASH_IOS_LOGIN]: string; diff --git a/src/components/BookTravelButton.tsx b/src/components/BookTravelButton.tsx index 8551e67aea6b0..96ebcd2d08e2e 100644 --- a/src/components/BookTravelButton.tsx +++ b/src/components/BookTravelButton.tsx @@ -1,7 +1,6 @@ -import HybridAppModule from '@expensify/react-native-hybrid-app'; import {Str} from 'expensify-common'; import type {ReactElement} from 'react'; -import React, {useCallback, useContext, useEffect, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; @@ -22,7 +21,6 @@ import ROUTES from '@src/ROUTES'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import Button from './Button'; import ConfirmModal from './ConfirmModal'; -import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import DotIndicatorMessage from './DotIndicatorMessage'; import {RocketDude} from './Icon/Illustrations'; import Text from './Text'; @@ -62,7 +60,6 @@ function BookTravelButton({text, shouldRenderErrorMessageBelowButton = false, se const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS, {canBeMissing: false}); const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email, canBeMissing: false}); const primaryContactMethod = primaryLogin ?? sessionEmail ?? ''; - const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); const {isBlockedFromSpotnanaTravel, isBetaEnabled} = usePermissions(); const [isPreventionModalVisible, setPreventionModalVisibility] = useState(false); const [isVerificationModalVisible, setVerificationModalVisibility] = useState(false); @@ -72,7 +69,7 @@ function BookTravelButton({text, shouldRenderErrorMessageBelowButton = false, se const groupPaidPolicies = activePolicies.filter((activePolicy) => activePolicy.type !== CONST.POLICY.TYPE.PERSONAL && isPaidGroupPolicy(activePolicy)); // Flag indicating whether NewDot was launched exclusively for Travel, // e.g., when the user selects "Trips" from the Expensify Classic menu in HybridApp. - const [wasNewDotLaunchedJustForTravel] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY, {canBeMissing: false}); + const [hybridApp] = useOnyx(ONYXKEYS.HYBRID_APP, {canBeMissing: true}); const hidePreventionModal = () => setPreventionModalVisibility(false); const hideVerificationModal = () => setVerificationModalVisibility(false); @@ -130,15 +127,14 @@ function BookTravelButton({text, shouldRenderErrorMessageBelowButton = false, se openTravelDotLink(policy?.id) ?.then(() => { // When a user selects "Trips" in the Expensify Classic menu, the HybridApp opens the ManageTrips page in NewDot. - // The wasNewDotLaunchedJustForTravel flag indicates if NewDot was launched solely for this purpose. - if (!CONFIG.IS_HYBRID_APP || !wasNewDotLaunchedJustForTravel) { + // The isSingleNewDotEntry flag indicates if NewDot was launched solely for this purpose. + if (!CONFIG.IS_HYBRID_APP || !hybridApp?.isSingleNewDotEntry) { return; } // Close NewDot if it was opened only for Travel, as its purpose is now fulfilled. Log.info('[HybridApp] Returning to OldDot after opening TravelDot'); - HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false}); - setRootStatusBarEnabled(false); + closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false}); }) ?.catch(() => { setErrorMessage(translate('travel.errorMessage')); @@ -175,8 +171,7 @@ function BookTravelButton({text, shouldRenderErrorMessageBelowButton = false, se styles.link, StyleUtils, translate, - wasNewDotLaunchedJustForTravel, - setRootStatusBarEnabled, + hybridApp?.isSingleNewDotEntry, isUserValidated, groupPaidPolicies.length, isBetaEnabled, diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx index 95ad1fe0f3a80..6f5c3b0e9c2e3 100644 --- a/src/components/CustomStatusBarAndBackground/index.tsx +++ b/src/components/CustomStatusBarAndBackground/index.tsx @@ -5,6 +5,8 @@ import useTheme from '@hooks/useTheme'; import {navigationRef} from '@libs/Navigation/Navigation'; import StatusBar from '@libs/StatusBar'; import type {StatusBarStyle} from '@styles/index'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {useOnyx} from '../../../__mocks__/react-native-onyx'; import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackgroundContext'; import updateGlobalBackgroundColor from './updateGlobalBackgroundColor'; import updateStatusBarAppearance from './updateStatusBarAppearance'; @@ -19,8 +21,9 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack const {isRootStatusBarEnabled, setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); const theme = useTheme(); const [statusBarStyle, setStatusBarStyle] = useState(); + const [hybridApp] = useOnyx(ONYXKEYS.HYBRID_APP, {canBeMissing: true}); - const isDisabled = !isNested && !isRootStatusBarEnabled; + const isDisabled = (!isNested && !isRootStatusBarEnabled) || (hybridApp?.closingReactNativeApp ?? false); // Disable the root status bar when a nested status bar is rendered useEffect(() => { diff --git a/src/components/ScreenWrapper/index.tsx b/src/components/ScreenWrapper/index.tsx index c12b133451cbd..87236fdc67106 100644 --- a/src/components/ScreenWrapper/index.tsx +++ b/src/components/ScreenWrapper/index.tsx @@ -1,4 +1,3 @@ -import HybridAppModule from '@expensify/react-native-hybrid-app'; import {useIsFocused, useNavigation, usePreventRemove} from '@react-navigation/native'; import type {ForwardedRef, ReactNode} from 'react'; import React, {forwardRef, useContext, useEffect, useMemo, useState} from 'react'; @@ -7,7 +6,6 @@ import {Keyboard} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {EdgeInsets} from 'react-native-safe-area-context'; import CustomDevMenu from '@components/CustomDevMenu'; -import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import FocusTrapForScreen from '@components/FocusTrap/FocusTrapForScreen'; import type FocusTrapForScreenProps from '@components/FocusTrap/FocusTrapForScreen/FocusTrapProps'; import HeaderGap from '@components/HeaderGap'; @@ -18,6 +16,7 @@ import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useThemeStyles from '@hooks/useThemeStyles'; +import {closeReactNativeApp} from '@libs/actions/Session'; import NarrowPaneContext from '@libs/Navigation/AppNavigator/Navigators/NarrowPaneContext'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -170,15 +169,13 @@ function ScreenWrapper( const shouldOffsetMobileOfflineIndicator = displaySmallScreenOfflineIndicator && addSmallScreenOfflineIndicatorBottomSafeAreaPadding && isOffline; const {initialURL} = useContext(InitialURLContext); - const [isSingleNewDotEntry] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY, {canBeMissing: true}); - const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); + const [hybridApp] = useOnyx(ONYXKEYS.HYBRID_APP, {canBeMissing: true}); - usePreventRemove((isSingleNewDotEntry ?? false) && initialURL === Navigation.getActiveRouteWithoutParams(), () => { + usePreventRemove((hybridApp?.isSingleNewDotEntry ?? false) && initialURL === Navigation.getActiveRouteWithoutParams(), () => { if (!CONFIG.IS_HYBRID_APP) { return; } - HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false}); - setRootStatusBarEnabled(false); + closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false}); }); useEffect(() => { diff --git a/src/components/SignInButtons/AppleSignIn/index.android.tsx b/src/components/SignInButtons/AppleSignIn/index.android.tsx index a528fe7c5a10c..46cf0e0ab46de 100644 --- a/src/components/SignInButtons/AppleSignIn/index.android.tsx +++ b/src/components/SignInButtons/AppleSignIn/index.android.tsx @@ -1,8 +1,9 @@ import {appleAuthAndroid} from '@invertase/react-native-apple-authentication'; import React from 'react'; import IconButton from '@components/SignInButtons/IconButton'; +import {setNewDotSignInState} from '@libs/actions/HybridApp'; import Log from '@libs/Log'; -import * as Session from '@userActions/Session'; +import {beginAppleSignIn} from '@userActions/Session'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import type {AppleSignInProps} from '.'; @@ -37,7 +38,10 @@ function appleSignInRequest(): Promise { function AppleSignIn({onPress = () => {}}: AppleSignInProps) { const handleSignIn = () => { appleSignInRequest() - .then((token) => Session.beginAppleSignIn(token)) + .then((token) => { + setNewDotSignInState(CONST.HYBRID_APP_SIGN_IN_STATE.STARTED); + beginAppleSignIn(token); + }) .catch((error: Record) => { if (error.message === appleAuthAndroid.Error.SIGNIN_CANCELLED) { return null; diff --git a/src/components/SignInButtons/AppleSignIn/index.ios.tsx b/src/components/SignInButtons/AppleSignIn/index.ios.tsx index 57aae97b9c48f..de808ecd49cb7 100644 --- a/src/components/SignInButtons/AppleSignIn/index.ios.tsx +++ b/src/components/SignInButtons/AppleSignIn/index.ios.tsx @@ -2,8 +2,9 @@ import appleAuth from '@invertase/react-native-apple-authentication'; import type {AppleError} from '@invertase/react-native-apple-authentication'; import React from 'react'; import IconButton from '@components/SignInButtons/IconButton'; +import {setNewDotSignInState} from '@libs/actions/HybridApp'; import Log from '@libs/Log'; -import * as Session from '@userActions/Session'; +import {beginAppleSignIn} from '@userActions/Session'; import CONST from '@src/CONST'; import type {AppleSignInProps} from '.'; @@ -36,7 +37,10 @@ function appleSignInRequest(): Promise { function AppleSignIn({onPress = () => {}}: AppleSignInProps) { const handleSignIn = () => { appleSignInRequest() - .then((token) => Session.beginAppleSignIn(token)) + .then((token) => { + setNewDotSignInState(CONST.HYBRID_APP_SIGN_IN_STATE.STARTED); + beginAppleSignIn(token); + }) .catch((error: {code: AppleError}) => { if (error.code === appleAuth.Error.CANCELED) { return null; diff --git a/src/components/SignInButtons/GoogleSignIn/index.native.tsx b/src/components/SignInButtons/GoogleSignIn/index.native.tsx index 4a70a10ef0171..afdc9edb0bd36 100644 --- a/src/components/SignInButtons/GoogleSignIn/index.native.tsx +++ b/src/components/SignInButtons/GoogleSignIn/index.native.tsx @@ -1,8 +1,9 @@ import {GoogleSignin, statusCodes} from '@react-native-google-signin/google-signin'; import React from 'react'; import IconButton from '@components/SignInButtons/IconButton'; +import {setNewDotSignInState} from '@libs/actions/HybridApp'; 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 '.'; @@ -13,8 +14,8 @@ import type GoogleError from './types'; */ function googleSignInRequest() { GoogleSignin.configure({ - webClientId: CONFIG.GOOGLE_SIGN_IN.WEB_CLIENT_ID, - iosClientId: CONFIG.GOOGLE_SIGN_IN.IOS_CLIENT_ID, + webClientId: CONFIG.IS_HYBRID_APP ? CONFIG.GOOGLE_SIGN_IN.HYBRID_APP_WEB_CLIENT_ID : CONFIG.GOOGLE_SIGN_IN.WEB_CLIENT_ID, + 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 +26,10 @@ function googleSignInRequest() { GoogleSignin.signIn() .then((response) => response.idToken) - .then((token) => Session.beginGoogleSignIn(token)) + .then((token) => { + setNewDotSignInState(CONST.HYBRID_APP_SIGN_IN_STATE.STARTED); + beginGoogleSignIn(token); + }) .catch((error: GoogleError | undefined) => { // Handle unexpected error shape if (error?.code === undefined) { diff --git a/src/hooks/useOnboardingFlow.ts b/src/hooks/useOnboardingFlow.ts index bf229b62ea289..3462ff52f0233 100644 --- a/src/hooks/useOnboardingFlow.ts +++ b/src/hooks/useOnboardingFlow.ts @@ -32,7 +32,7 @@ function useOnboardingFlowRouter() { const [dismissedProductTraining, dismissedProductTrainingMetadata] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true}); - const [isSingleNewDotEntry, isSingleNewDotEntryMetadata] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY, {canBeMissing: true}); + const [isSingleNewDotEntry, isSingleNewDotEntryMetadata] = useOnyx(ONYXKEYS.HYBRID_APP, {selector: (data) => data?.isSingleNewDotEntry, canBeMissing: true}); useEffect(() => { // This should delay opening the onboarding modal so it does not interfere with the ongoing ReportScreen params changes diff --git a/src/libs/HybridApp.ts b/src/libs/HybridApp.ts new file mode 100644 index 0000000000000..d37d919ccb6b8 --- /dev/null +++ b/src/libs/HybridApp.ts @@ -0,0 +1,100 @@ +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 CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Credentials, HybridApp, Session, TryNewDot} from '@src/types/onyx'; +import {setNewDotSignInState, setReadyToShowAuthScreens, setUseNewDotSignInPage} from './actions/HybridApp'; +import {closeReactNativeApp} from './actions/Session'; +import Log from './Log'; +import {getCurrentUserEmail} from './Network/NetworkStore'; + +let currentHybridApp: OnyxEntry; +let currentTryNewDot: OnyxEntry; +let currentCredentials: OnyxEntry; + +Onyx.connect({ + key: ONYXKEYS.HYBRID_APP, + callback: (hybridApp) => { + handleChangeInHybridAppSignInFlow(hybridApp, currentTryNewDot, currentCredentials); + }, +}); + +Onyx.connect({ + key: ONYXKEYS.NVP_TRY_NEW_DOT, + callback: (tryNewDot) => { + handleChangeInHybridAppSignInFlow(currentHybridApp, tryNewDot, currentCredentials); + }, +}); + +Onyx.connect({ + key: ONYXKEYS.CREDENTIALS, + callback: (credentials) => { + currentCredentials = credentials; + handleChangeInHybridAppSignInFlow(currentHybridApp, currentTryNewDot, credentials); + }, +}); + +let currentSession: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (session: OnyxEntry) => { + if (!currentSession?.authToken && session?.authToken && currentHybridApp?.newDotSignInState === CONST.HYBRID_APP_SIGN_IN_STATE.STARTED) { + setNewDotSignInState(CONST.HYBRID_APP_SIGN_IN_STATE.FINISHED); + } + currentSession = session; + }, +}); + +let activePolicyID: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, + callback: (newActivePolicyID) => { + activePolicyID = newActivePolicyID; + }, +}); + +function shouldUseOldApp(tryNewDot?: TryNewDot) { + if (!!tryNewDot && !tryNewDot.classicRedirect) { + return true; + } + return tryNewDot?.classicRedirect?.dismissed === true; +} + +function handleChangeInHybridAppSignInFlow(hybridApp: OnyxEntry, tryNewDot: OnyxEntry, credentials: OnyxEntry) { + if (!CONFIG.IS_HYBRID_APP) { + return; + } + + if (!hybridApp?.useNewDotSignInPage) { + currentHybridApp = hybridApp; + currentTryNewDot = tryNewDot; + return; + } + + if (hybridApp?.newDotSignInState === CONST.HYBRID_APP_SIGN_IN_STATE.FINISHED && 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: currentSession?.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); + } + }); + } + + currentHybridApp = hybridApp; + currentTryNewDot = tryNewDot; +} diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index d40102010bc1b..d4b3409b0865a 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -307,26 +307,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 9204ba8b9563c..0c66afec9bf3c 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/Notification/PushNotification/subscribeToPushNotifications.ts b/src/libs/Notification/PushNotification/subscribeToPushNotifications.ts index 88acb4687054b..95b308b1f8c06 100644 --- a/src/libs/Notification/PushNotification/subscribeToPushNotifications.ts +++ b/src/libs/Notification/PushNotification/subscribeToPushNotifications.ts @@ -41,12 +41,12 @@ Onyx.connect({ let isSingleNewDotEntry: boolean | undefined; Onyx.connect({ - key: ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY, + key: ONYXKEYS.HYBRID_APP, callback: (value) => { if (!value) { return; } - isSingleNewDotEntry = value; + isSingleNewDotEntry = value?.isSingleNewDotEntry; }, }); diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 017d10ded4989..f1298c4866f9e 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -152,6 +152,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 new file mode 100644 index 0000000000000..6f655b83a4bb4 --- /dev/null +++ b/src/libs/actions/HybridApp/index.ts @@ -0,0 +1,89 @@ +import Onyx from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import CONFIG from '@src/CONFIG'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {HybridApp} from '@src/types/onyx'; +import type HybridAppSettings from './types'; + +/* + * 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}); +} + +/* + * Changes NewDot sign-in state + */ +function setNewDotSignInState(newDotSignInState: ValueOf) { + // 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, {newDotSignInState}); +} + +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, + newDotSignInState: CONST.HYBRID_APP_SIGN_IN_STATE.NOT_STARTED, + 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), + newDotSignInState: CONST.HYBRID_APP_SIGN_IN_STATE.NOT_STARTED, + }); + } + + // 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, setNewDotSignInState, resetSignInFlow, prepareHybridAppAfterTransitionToNewDot, setUseNewDotSignInPage, setClosingReactNativeApp}; 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/Report.ts b/src/libs/actions/Report.ts index 91f5655ef8f28..312de5f7c9260 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3388,6 +3388,11 @@ function openReportFromDeepLink(url: string) { return; } + if (CONST.REGEX.ROUTES.VALIDATE_LOGIN.test(`${route}`)) { + Navigation.navigate(route as Route); + return; + } + // Navigate to the report after sign-in/sign-up. InteractionManager.runAfterInteractions(() => { waitForUserSignIn().then(() => { diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index d3fe1afedb53b..33f86174d3a9d 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,12 +41,16 @@ 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 * as HybridAppActions from '@userActions/HybridApp'; +import {setClosingReactNativeApp} 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'; @@ -56,6 +60,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {HybridAppRoute, 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'; @@ -68,6 +73,7 @@ const INVALID_TOKEN = 'pizza'; let session: Session = {}; let authPromiseResolver: ((value: boolean) => void) | null = null; +let isSetUpReady = false; let hasSwitchedAccountInHybridMode = false; @@ -82,7 +88,7 @@ Onyx.connect({ authPromiseResolver(true); authPromiseResolver = null; } - if (CONFIG.IS_HYBRID_APP && session.authToken && session.authToken !== INVALID_TOKEN) { + if (CONFIG.IS_HYBRID_APP && session.authToken && session.authToken !== INVALID_TOKEN && isSetUpReady) { HybridAppModule.sendAuthToken({authToken: session.authToken}); } }, @@ -241,7 +247,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); @@ -261,10 +267,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) { - HybridAppModule.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(); @@ -514,127 +519,89 @@ 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.IS_SINGLE_NEW_DOT_ENTRY]: isSingleNewDotEntry, - [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})), + 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); - }); + isSetUpReady = true; + return Promise.resolve(); }) .catch((error) => { Log.hmmm('[HybridApp] Initialization of HybridApp has failed. Forcing transition', {error}); }); } +function closeReactNativeApp({shouldSignOut, shouldSetNVP}: {shouldSignOut: boolean; shouldSetNVP: boolean}) { + if (CONFIG.IS_HYBRID_APP) { + setClosingReactNativeApp(true); + } + HybridAppModule.closeReactNativeApp({shouldSignOut, shouldSetNVP}); +} + /** * Given an idToken from Sign in with Apple, checks the API to see if an account * exists for that email address and signs the user in if so. @@ -733,7 +700,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, + }); }); } @@ -1469,9 +1440,10 @@ export { isSupportAuthToken, hasStashedSession, signUpUser, - signInAfterTransitionFromOldDot, + setupNewDotAfterTransitionFromOldDot, AddWorkEmail, MergeIntoAccountAndLogin, resetSMSDeliveryFailureStatus, clearDisableTwoFactorAuthErrors, + closeReactNativeApp, }; 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 65c2bb51c6883..8212f2f4f616d 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 85dd6724a3bdd..b2a6200e2c709 100644 --- a/src/pages/ErrorPage/SessionExpiredPage.tsx +++ b/src/pages/ErrorPage/SessionExpiredPage.tsx @@ -1,4 +1,3 @@ -import HybridAppModule from '@expensify/react-native-hybrid-app'; import React from 'react'; import {View} from 'react-native'; import Icon from '@components/Icon'; @@ -9,6 +8,7 @@ import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {closeReactNativeApp} from '@libs/actions/Session'; import Navigation from '@libs/Navigation/Navigation'; import {clearSignInData} from '@userActions/Session'; import CONFIG from '@src/CONFIG'; @@ -39,7 +39,7 @@ function SessionExpiredPage() { Navigation.goBack(); return; } - HybridAppModule.closeReactNativeApp({shouldSignOut: true, shouldSetNVP: false}); + closeReactNativeApp({shouldSignOut: true, shouldSetNVP: false}); }} > {translate('deeplinkWrapper.signIn')} diff --git a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx index e3f32bd02c76f..fe113f33060ac 100644 --- a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx +++ b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx @@ -1,10 +1,8 @@ -import HybridAppModule from '@expensify/react-native-hybrid-app'; -import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg'; import Button from '@components/Button'; -import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import FixedFooter from '@components/FixedFooter'; import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -28,6 +26,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {openOldDotLink} from '@libs/actions/Link'; import {createWorkspace, generatePolicyID} from '@libs/actions/Policy/Policy'; import {completeOnboarding} from '@libs/actions/Report'; +import {closeReactNativeApp} from '@libs/actions/Session'; import {setOnboardingAdminsChatReportID, setOnboardingPolicyID} from '@libs/actions/Welcome'; import navigateAfterOnboarding from '@libs/navigateAfterOnboarding'; import Navigation from '@libs/Navigation/Navigation'; @@ -101,7 +100,6 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const {onboardingMessages} = useOnboardingMessages(); - const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); // We need to use isSmallScreenWidth, see navigateAfterOnboarding function comment // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth @@ -138,14 +136,13 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount } if (CONFIG.IS_HYBRID_APP) { - HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: true}); - setRootStatusBarEnabled(false); + closeReactNativeApp({shouldSignOut: false, shouldSetNVP: true}); return; } waitForIdle().then(() => { openOldDotLink(CONST.OLDDOT_URLS.INBOX, true); }); - }, [isLoading, prevIsLoading, setRootStatusBarEnabled]); + }, [isLoading, prevIsLoading]); const accountingOptions: OnboardingListItem[] = useMemo(() => { const createAccountingOption = (integration: Integration): OnboardingListItem => ({ diff --git a/src/pages/ValidateLoginPage/index.tsx b/src/pages/ValidateLoginPage/index.tsx index dc38916fdd109..b3d24a4f84332 100644 --- a/src/pages/ValidateLoginPage/index.tsx +++ b/src/pages/ValidateLoginPage/index.tsx @@ -2,7 +2,9 @@ import React, {useEffect} from 'react'; import {useOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Navigation from '@libs/Navigation/Navigation'; -import * as Session from '@userActions/Session'; +import {setNewDotSignInState} from '@userActions/HybridApp'; +import {handleExitToNavigation, signInWithValidateCodeAndNavigate} from '@userActions/Session'; +import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type ValidateLoginPageProps from './types'; @@ -12,7 +14,7 @@ function ValidateLoginPage({ params: {accountID, validateCode, exitTo}, }, }: ValidateLoginPageProps) { - const [session] = useOnyx(ONYXKEYS.SESSION); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true}); useEffect(() => { // Wait till navigation becomes available @@ -21,12 +23,17 @@ function ValidateLoginPage({ // If already signed in, do not show the validate code if not on web, // because we don't want to block the user with the interstitial page. if (exitTo) { - Session.handleExitToNavigation(exitTo); + handleExitToNavigation(exitTo); return; } Navigation.goBack(); } else { - Session.signInWithValidateCodeAndNavigate(Number(accountID), validateCode, '', exitTo); + // On HybridApp we need to orchestrate the sign-in flow of both apps so we need to set the state to STARTED here + if (CONFIG.IS_HYBRID_APP) { + setNewDotSignInState(CONST.HYBRID_APP_SIGN_IN_STATE.STARTED); + } + + signInWithValidateCodeAndNavigate(Number(accountID), validateCode, '', exitTo); } }); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index 1f2108e9adecc..1033baa0221e8 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -1,14 +1,12 @@ -import HybridAppModule from '@expensify/react-native-hybrid-app'; import {useIsFocused} from '@react-navigation/native'; import {Str} from 'expensify-common'; import type {ImageContentFit} from 'expo-image'; import type {ForwardedRef} from 'react'; -import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; -import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import FloatingActionButton from '@components/FloatingActionButton'; import * as Expensicons from '@components/Icon/Expensicons'; import type {PopoverMenuItem} from '@components/PopoverMenu'; @@ -26,7 +24,7 @@ import {startMoneyRequest} from '@libs/actions/IOU'; import {openOldDotLink, openTravelDotLink} from '@libs/actions/Link'; import {navigateToQuickAction} from '@libs/actions/QuickActionNavigation'; import {createNewReport, startNewChat} from '@libs/actions/Report'; -import {isAnonymousUser} from '@libs/actions/Session'; +import {closeReactNativeApp, isAnonymousUser} from '@libs/actions/Session'; import {completeTestDriveTask} from '@libs/actions/Task'; import getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; @@ -135,8 +133,6 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT const viewTourReportID = introSelected?.viewTour; const [viewTourReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${viewTourReportID}`, {canBeMissing: true}); - const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); - const groupPoliciesWithChatEnabled = getGroupPaidPoliciesWithExpenseChatEnabled(); /** @@ -578,8 +574,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT onConfirm={() => { setModalVisible(false); if (CONFIG.IS_HYBRID_APP) { - HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: true}); - setRootStatusBarEnabled(false); + closeReactNativeApp({shouldSignOut: false, shouldSetNVP: true}); return; } openOldDotLink(CONST.OLDDOT_URLS.INBOX); diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index bf8614a4daaca..e26a273f682f5 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -1,4 +1,3 @@ -import HybridAppModule from '@expensify/react-native-hybrid-app/src'; import {findFocusedRoute, useNavigationState, useRoute} from '@react-navigation/native'; import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; // eslint-disable-next-line no-restricted-imports @@ -9,7 +8,6 @@ import type {ValueOf} from 'type-fest'; import AccountSwitcher from '@components/AccountSwitcher'; import AccountSwitcherSkeletonView from '@components/AccountSwitcherSkeletonView'; import ConfirmModal from '@components/ConfirmModal'; -import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; @@ -30,7 +28,9 @@ 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 {closeReactNativeApp} from '@libs/actions/Session'; import {checkIfFeedConnectionIsBroken} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import useIsSidebarRouteActive from '@libs/Navigation/helpers/useIsSidebarRouteActive'; @@ -102,7 +102,6 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr const {translate} = useLocalize(); const focusedRouteName = useNavigationState((state) => findFocusedRoute(state)?.name); const emojiCode = currentUserPersonalDetails?.status?.emojiCode ?? ''; - const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); const isScreenFocused = useIsSidebarRouteActive(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR, shouldUseNarrowLayout); const hasActivatedWallet = ([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM] as string[]).includes(userWallet?.tierName ?? ''); @@ -238,10 +237,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr icon: Expensicons.ExpensifyLogoNew, ...(CONFIG.IS_HYBRID_APP ? { - action: () => { - HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: true}); - setRootStatusBarEnabled(false); - }, + action: () => closeReactNativeApp({shouldSignOut: false, shouldSetNVP: true}), } : { action() { @@ -282,7 +278,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr }, ], }; - }, [styles.pt4, setRootStatusBarEnabled, shouldOpenSurveyReasonPage, signOut]); + }, [styles.pt4, shouldOpenSurveyReasonPage, signOut]); /** * Return JSX.Element with menu items diff --git a/src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx b/src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx index 1f623d61a56d3..dd6ebbf608927 100644 --- a/src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx +++ b/src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx @@ -1,12 +1,10 @@ -import HybridAppModule from '@expensify/react-native-hybrid-app'; import {useRoute} from '@react-navigation/native'; -import React, {useContext, useEffect, useMemo} from 'react'; +import React, {useEffect, useMemo} from 'react'; import {InteractionManager} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import ConfirmationPage from '@components/ConfirmationPage'; import type {ConfirmationPageProps} from '@components/ConfirmationPage'; -import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; import LottieAnimations from '@components/LottieAnimations'; @@ -15,6 +13,7 @@ import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import {closeReactNativeApp} from '@libs/actions/Session'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; @@ -29,7 +28,6 @@ import SCREENS from '@src/SCREENS'; function MergeResultPage() { const styles = useThemeStyles(); const {translate} = useLocalize(); - const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); const [userEmailOrPhone] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email, canBeMissing: true}); const {params} = useRoute>(); const {result, login} = params; @@ -153,8 +151,7 @@ function MergeResultPage() { secondaryButtonText: translate('mergeAccountsPage.mergePendingSAML.goToExpensifyClassic'), onSecondaryButtonPress: () => { if (CONFIG.IS_HYBRID_APP) { - HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: true}); - setRootStatusBarEnabled(false); + closeReactNativeApp({shouldSignOut: false, shouldSetNVP: true}); return; } openOldDotLink(CONST.OLDDOT_URLS.INBOX, false); @@ -234,7 +231,7 @@ function MergeResultPage() { illustration: Illustrations.LockClosedOrange, }, }; - }, [setRootStatusBarEnabled, login, translate, userEmailOrPhone, styles]); + }, [login, translate, userEmailOrPhone, styles]); useEffect(() => { /** diff --git a/src/pages/signin/ChooseSSOOrMagicCode.tsx b/src/pages/signin/ChooseSSOOrMagicCode.tsx index 2551fe34a21b1..fab3cd258008e 100644 --- a/src/pages/signin/ChooseSSOOrMagicCode.tsx +++ b/src/pages/signin/ChooseSSOOrMagicCode.tsx @@ -1,7 +1,7 @@ -import React, {useEffect} from 'react'; +import {useFocusEffect} from '@react-navigation/native'; +import React, {useCallback} from 'react'; import {Keyboard, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; import Text from '@components/Text'; @@ -10,44 +10,40 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ErrorUtils from '@libs/ErrorUtils'; +import {getLatestErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as Session from '@userActions/Session'; +import {clearSignInData, resendValidateCode} from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Account, Credentials} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import ChangeExpensifyLoginLink from './ChangeExpensifyLoginLink'; import Terms from './Terms'; -type ChooseSSOOrMagicCodeOnyxProps = { - /** The credentials of the logged in person */ - credentials: OnyxEntry; - - /** The details about the account that the user is signing in with */ - account: OnyxEntry; -}; - -type ChooseSSOOrMagicCodeProps = ChooseSSOOrMagicCodeOnyxProps & { +type ChooseSSOOrMagicCodeProps = { /** Function that returns whether the user is using SAML or magic codes to log in */ setIsUsingMagicCode: (value: boolean) => void; }; -function ChooseSSOOrMagicCode({credentials, account, setIsUsingMagicCode}: ChooseSSOOrMagicCodeProps) { +function ChooseSSOOrMagicCode({setIsUsingMagicCode}: ChooseSSOOrMagicCodeProps) { const styles = useThemeStyles(); const {isKeyboardShown} = useKeyboardState(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: false}); + const [credentials] = useOnyx(ONYXKEYS.CREDENTIALS, {canBeMissing: true}); + // This view doesn't have a field for user input, so dismiss the device keyboard if shown - useEffect(() => { - if (!isKeyboardShown) { - return; - } - Keyboard.dismiss(); - }, [isKeyboardShown]); + useFocusEffect( + useCallback(() => { + if (!isKeyboardShown) { + return; + } + Keyboard.dismiss(); + }, [isKeyboardShown]), + ); return ( <> @@ -78,12 +74,12 @@ function ChooseSSOOrMagicCode({credentials, account, setIsUsingMagicCode}: Choos text={translate('samlSignIn.useMagicCode')} isLoading={account?.isLoading && account?.loadingForm === (account?.requiresTwoFactorAuth ? CONST.FORMS.VALIDATE_TFA_CODE_FORM : CONST.FORMS.VALIDATE_CODE_FORM)} onPress={() => { - Session.resendValidateCode(credentials?.login); + resendValidateCode(credentials?.login); setIsUsingMagicCode(true); }} /> - {!!account && !isEmptyObject(account.errors) && } - Session.clearSignInData()} /> + {!!account && !isEmptyObject(account.errors) && } + clearSignInData()} /> @@ -94,7 +90,4 @@ function ChooseSSOOrMagicCode({credentials, account, setIsUsingMagicCode}: Choos ChooseSSOOrMagicCode.displayName = 'ChooseSSOOrMagicCode'; -export default withOnyx({ - credentials: {key: ONYXKEYS.CREDENTIALS}, - account: {key: ONYXKEYS.ACCOUNT}, -})(ChooseSSOOrMagicCode); +export default ChooseSSOOrMagicCode; diff --git a/src/pages/signin/SAMLSignInPage/index.native.tsx b/src/pages/signin/SAMLSignInPage/index.native.tsx index 2daa8ebce16ba..3d37272549afe 100644 --- a/src/pages/signin/SAMLSignInPage/index.native.tsx +++ b/src/pages/signin/SAMLSignInPage/index.native.tsx @@ -7,6 +7,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import SAMLLoadingIndicator from '@components/SAMLLoadingIndicator'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; +import {setNewDotSignInState} from '@libs/actions/HybridApp'; import getPlatform from '@libs/getPlatform'; import getUAForWebView from '@libs/getUAForWebView'; import Log from '@libs/Log'; @@ -14,12 +15,13 @@ import {handleSAMLLoginError, postSAMLLogin} from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import {clearSignInData, setAccountError, signInWithShortLivedAuthToken} from '@userActions/Session'; import CONFIG from '@src/CONFIG'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; function SAMLSignInPage() { - const [account] = useOnyx(ONYXKEYS.ACCOUNT); - const [credentials] = useOnyx(ONYXKEYS.CREDENTIALS); + const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: false}); + const [credentials] = useOnyx(ONYXKEYS.CREDENTIALS, {canBeMissing: true}); const [showNavigation, shouldShowNavigation] = useState(true); const [SAMLUrl, setSAMLUrl] = useState(''); const webViewRef = useRef(null); @@ -69,6 +71,7 @@ function SAMLSignInPage() { if (!account?.isLoading && credentials?.login && !!shortLivedAuthToken) { Log.info('SAMLSignInPage - Successfully received shortLivedAuthToken. Signing in...'); signInWithShortLivedAuthToken(shortLivedAuthToken); + setNewDotSignInState(CONST.HYBRID_APP_SIGN_IN_STATE.STARTED); } // If the login attempt is unsuccessful, set the error message for the account and redirect to sign in page diff --git a/src/pages/signin/SignInPage.tsx b/src/pages/signin/SignInPage.tsx index 1cd6b2c04ef63..8ad0b93fc44e8 100644 --- a/src/pages/signin/SignInPage.tsx +++ b/src/pages/signin/SignInPage.tsx @@ -1,3 +1,4 @@ +import HybridAppModule from '@expensify/react-native-hybrid-app'; import {Str} from 'expensify-common'; import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; import type {ForwardedRef} from 'react'; @@ -21,6 +22,7 @@ import Performance from '@libs/Performance'; import Visibility from '@libs/Visibility'; import {setLocale} from '@userActions/App'; import {clearSignInData} from '@userActions/Session'; +import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -182,6 +184,7 @@ function SignInPage({shouldEnableMaxHeight = true}: SignInPageInnerProps, ref: F // We need to show "Another login page is opened" message if the page isn't active and visible // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowAnotherLoginPageOpenedMessage = Visibility.isVisible() && !isClientTheLeader; + const [hybridApp] = useOnyx(ONYXKEYS.HYBRID_APP, {canBeMissing: true}); useEffect(() => Performance.measureTTI(), []); useEffect(() => { @@ -299,6 +302,14 @@ function SignInPage({shouldEnableMaxHeight = true}: SignInPageInnerProps, ref: F useImperativeHandle(ref, () => ({ navigateBack, })); + + useEffect(() => { + if (!CONFIG.IS_HYBRID_APP || !hybridApp?.loggedOutFromOldDot) { + return; + } + HybridAppModule.clearOldDotAfterSignOut(); + }, [hybridApp?.loggedOutFromOldDot]); + return ( // Bottom SafeAreaView is removed so that login screen svg displays correctly on mobile. // The SVG should flow under the Home Indicator on iOS. diff --git a/src/pages/signin/SignUpWelcomeForm.tsx b/src/pages/signin/SignUpWelcomeForm.tsx index 1f8687c218b71..a52efa042e8d1 100644 --- a/src/pages/signin/SignUpWelcomeForm.tsx +++ b/src/pages/signin/SignUpWelcomeForm.tsx @@ -6,8 +6,10 @@ import FormHelpMessage from '@components/FormHelpMessage'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ErrorUtils from '@libs/ErrorUtils'; -import * as Session from '@userActions/Session'; +import {getLatestErrorMessage} from '@libs/ErrorUtils'; +import {setNewDotSignInState, setReadyToShowAuthScreens} from '@userActions/HybridApp'; +import {clearSignInData, signUpUser} from '@userActions/Session'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ChangeExpensifyLoginLink from './ChangeExpensifyLoginLink'; import Terms from './Terms'; @@ -16,8 +18,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 +30,11 @@ function SignUpWelcomeForm() { large text={translate('welcomeSignUpForm.join')} isLoading={account?.isLoading} - onPress={() => Session.signUpUser()} + onPress={() => { + setNewDotSignInState(CONST.HYBRID_APP_SIGN_IN_STATE.STARTED); + signUpUser(); + setReadyToShowAuthScreens(true); + }} pressOnEnter style={[styles.mb2]} /> @@ -38,7 +44,7 @@ function SignUpWelcomeForm() { message={serverErrorText} /> )} - Session.clearSignInData()} /> + clearSignInData()} /> diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx index 063ca96f48f46..5448995d36fb1 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -24,8 +24,10 @@ import {getLatestErrorMessage} from '@libs/ErrorUtils'; import {isValidRecoveryCode, isValidTwoFactorCode, isValidValidateCode} from '@libs/ValidationUtils'; import ChangeExpensifyLoginLink from '@pages/signin/ChangeExpensifyLoginLink'; import Terms from '@pages/signin/Terms'; -import {clearAccountMessages, clearSignInData as sessionActionsClearSignInData, signIn, signInWithValidateCode} from '@userActions/Session'; +import {resetSignInFlow, setNewDotSignInState} from '@userActions/HybridApp'; +import {clearAccountMessages, isAnonymousUser as isAnonymousUserUtil, clearSignInData as sessionActionsClearSignInData, signIn, signInWithValidateCode} from '@userActions/Session'; import {resendValidateCode as userActionsResendValidateCode} from '@userActions/User'; +import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -50,6 +52,7 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); const [credentials] = useOnyx(ONYXKEYS.CREDENTIALS, {canBeMissing: true}); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); + const [hybridApp] = useOnyx(ONYXKEYS.HYBRID_APP, {canBeMissing: false}); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); @@ -153,6 +156,10 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco * Trigger the reset validate code flow and ensure the 2FA input field is reset to avoid it being permanently hidden */ const resendValidateCode = () => { + if (CONFIG.IS_HYBRID_APP) { + resetSignInFlow(); + } + userActionsResendValidateCode(credentials?.login ?? ''); inputValidateCodeRef.current?.clear(); // Give feedback to the user to let them know the email was sent so that they don't spam the button. @@ -232,9 +239,15 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco * Check that all the form fields are valid, then trigger the submit callback */ const validateAndSubmitForm = useCallback(() => { - if (account?.isLoading) { + const isAnonymousUser = isAnonymousUserUtil(session); + + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (account?.isLoading || hybridApp?.readyToShowAuthScreens || (session?.authToken && !isAnonymousUser)) { return; } + if (CONFIG.IS_HYBRID_APP) { + resetSignInFlow(); + } if (account?.errors) { clearAccountMessages(); } @@ -282,13 +295,25 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco const recoveryCodeOr2faCode = isUsingRecoveryCode ? recoveryCode : twoFactorAuthCode; + setNewDotSignInState(CONST.HYBRID_APP_SIGN_IN_STATE.STARTED); const accountID = credentials?.accountID; if (accountID) { signInWithValidateCode(accountID, validateCode, recoveryCodeOr2faCode); } else { signIn(validateCode, recoveryCodeOr2faCode); } - }, [account?.isLoading, account?.errors, account?.requiresTwoFactorAuth, isUsingRecoveryCode, recoveryCode, twoFactorAuthCode, credentials?.accountID, validateCode]); + }, [ + account?.isLoading, + account?.errors, + account?.requiresTwoFactorAuth, + hybridApp?.readyToShowAuthScreens, + session, + isUsingRecoveryCode, + recoveryCode, + twoFactorAuthCode, + credentials?.accountID, + validateCode, + ]); return ( diff --git a/src/types/onyx/HybridApp.ts b/src/types/onyx/HybridApp.ts new file mode 100644 index 0000000000000..b6fee12920cf0 --- /dev/null +++ b/src/types/onyx/HybridApp.ts @@ -0,0 +1,49 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +/** 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; + + /** Describes the current state of NewDot sign-in process */ + newDotSignInState?: ValueOf; + + /** Holds delegate access information */ + delegateAccessData?: HybridAppDelegateAccessData; + + /** Indicates if the NewDot is being closed */ + closingReactNativeApp?: boolean; +}; + +export default HybridApp; 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; }; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 24a921c1c9e5c..e61b5fdac6537 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -32,6 +32,7 @@ import type ExpensifyCardSettings from './ExpensifyCardSettings'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; import type Fund from './Fund'; +import type HybridApp from './HybridApp'; import type ImportedSpreadsheet from './ImportedSpreadsheet'; import type IntroSelected from './IntroSelected'; import type InvitedEmailsToAccountIDs from './InvitedEmailsToAccountIDs'; @@ -263,4 +264,5 @@ export type { ScheduleCallDraft, ValidateUserAndGetAccessiblePolicies, BillingReceiptDetails, + HybridApp, }; From cd42183fccf0cc81c42bc1bde8fc0a7281291cc5 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Thu, 3 Jul 2025 12:26:35 +0200 Subject: [PATCH 07/29] Add missing import --- src/components/BookTravelButton.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/BookTravelButton.tsx b/src/components/BookTravelButton.tsx index 96ebcd2d08e2e..d5253b5c95fa4 100644 --- a/src/components/BookTravelButton.tsx +++ b/src/components/BookTravelButton.tsx @@ -9,6 +9,7 @@ import usePolicy from '@hooks/usePolicy'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {openTravelDotLink} from '@libs/actions/Link'; +import {closeReactNativeApp} from '@libs/actions/Session'; import {cleanupTravelProvisioningSession, requestTravelAccess} from '@libs/actions/Travel'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; From 6f91ef18a80394c98f8dd73de8de8c6a115c95b6 Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 9 Jul 2025 13:04:18 +0200 Subject: [PATCH 08/29] remove newDotSignInState --- src/CONST/index.ts | 6 ------ .../AppleSignIn/index.android.tsx | 8 ++----- .../SignInButtons/AppleSignIn/index.ios.tsx | 8 ++----- .../GoogleSignIn/index.native.tsx | 6 +----- src/libs/HybridApp.ts | 21 +++++++++---------- src/libs/actions/HybridApp/index.ts | 17 +-------------- src/pages/ValidateLoginPage/index.tsx | 15 ++++--------- .../signin/SAMLSignInPage/index.native.tsx | 7 ++----- src/pages/signin/SignUpWelcomeForm.tsx | 4 +--- .../ValidateCodeForm/BaseValidateCodeForm.tsx | 3 +-- src/types/onyx/HybridApp.ts | 6 ------ 11 files changed, 24 insertions(+), 77 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 46d25e0951509..ae37de14951bc 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6609,12 +6609,6 @@ const CONST = { HIDDEN: `hidden`, }, - HYBRID_APP_SIGN_IN_STATE: { - NOT_STARTED: 'notStarted', - STARTED: 'started', - FINISHED: 'finished', - }, - CSV_IMPORT_COLUMNS: { EMAIL: 'email', NAME: 'name', diff --git a/src/components/SignInButtons/AppleSignIn/index.android.tsx b/src/components/SignInButtons/AppleSignIn/index.android.tsx index 46cf0e0ab46de..a528fe7c5a10c 100644 --- a/src/components/SignInButtons/AppleSignIn/index.android.tsx +++ b/src/components/SignInButtons/AppleSignIn/index.android.tsx @@ -1,9 +1,8 @@ import {appleAuthAndroid} from '@invertase/react-native-apple-authentication'; import React from 'react'; import IconButton from '@components/SignInButtons/IconButton'; -import {setNewDotSignInState} from '@libs/actions/HybridApp'; import Log from '@libs/Log'; -import {beginAppleSignIn} from '@userActions/Session'; +import * as Session from '@userActions/Session'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import type {AppleSignInProps} from '.'; @@ -38,10 +37,7 @@ function appleSignInRequest(): Promise { function AppleSignIn({onPress = () => {}}: AppleSignInProps) { const handleSignIn = () => { appleSignInRequest() - .then((token) => { - setNewDotSignInState(CONST.HYBRID_APP_SIGN_IN_STATE.STARTED); - beginAppleSignIn(token); - }) + .then((token) => Session.beginAppleSignIn(token)) .catch((error: Record) => { if (error.message === appleAuthAndroid.Error.SIGNIN_CANCELLED) { return null; diff --git a/src/components/SignInButtons/AppleSignIn/index.ios.tsx b/src/components/SignInButtons/AppleSignIn/index.ios.tsx index de808ecd49cb7..57aae97b9c48f 100644 --- a/src/components/SignInButtons/AppleSignIn/index.ios.tsx +++ b/src/components/SignInButtons/AppleSignIn/index.ios.tsx @@ -2,9 +2,8 @@ import appleAuth from '@invertase/react-native-apple-authentication'; import type {AppleError} from '@invertase/react-native-apple-authentication'; import React from 'react'; import IconButton from '@components/SignInButtons/IconButton'; -import {setNewDotSignInState} from '@libs/actions/HybridApp'; import Log from '@libs/Log'; -import {beginAppleSignIn} from '@userActions/Session'; +import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import type {AppleSignInProps} from '.'; @@ -37,10 +36,7 @@ function appleSignInRequest(): Promise { function AppleSignIn({onPress = () => {}}: AppleSignInProps) { const handleSignIn = () => { appleSignInRequest() - .then((token) => { - setNewDotSignInState(CONST.HYBRID_APP_SIGN_IN_STATE.STARTED); - beginAppleSignIn(token); - }) + .then((token) => Session.beginAppleSignIn(token)) .catch((error: {code: AppleError}) => { if (error.code === appleAuth.Error.CANCELED) { return null; diff --git a/src/components/SignInButtons/GoogleSignIn/index.native.tsx b/src/components/SignInButtons/GoogleSignIn/index.native.tsx index afdc9edb0bd36..9d636d47fc672 100644 --- a/src/components/SignInButtons/GoogleSignIn/index.native.tsx +++ b/src/components/SignInButtons/GoogleSignIn/index.native.tsx @@ -1,7 +1,6 @@ import {GoogleSignin, statusCodes} from '@react-native-google-signin/google-signin'; import React from 'react'; import IconButton from '@components/SignInButtons/IconButton'; -import {setNewDotSignInState} from '@libs/actions/HybridApp'; import Log from '@libs/Log'; import {beginGoogleSignIn} from '@userActions/Session'; import CONFIG from '@src/CONFIG'; @@ -26,10 +25,7 @@ function googleSignInRequest() { GoogleSignin.signIn() .then((response) => response.idToken) - .then((token) => { - setNewDotSignInState(CONST.HYBRID_APP_SIGN_IN_STATE.STARTED); - 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 index d37d919ccb6b8..a1adba9f1c6fc 100644 --- a/src/libs/HybridApp.ts +++ b/src/libs/HybridApp.ts @@ -2,10 +2,9 @@ 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 CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Credentials, HybridApp, Session, TryNewDot} from '@src/types/onyx'; -import {setNewDotSignInState, setReadyToShowAuthScreens, setUseNewDotSignInPage} from './actions/HybridApp'; +import {setReadyToShowAuthScreens, setUseNewDotSignInPage} from './actions/HybridApp'; import {closeReactNativeApp} from './actions/Session'; import Log from './Log'; import {getCurrentUserEmail} from './Network/NetworkStore'; @@ -13,18 +12,19 @@ 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) => { - handleChangeInHybridAppSignInFlow(hybridApp, currentTryNewDot, currentCredentials); + handleChangeInHybridAppSignInFlow(hybridApp, currentTryNewDot, currentCredentials, currentSession); }, }); Onyx.connect({ key: ONYXKEYS.NVP_TRY_NEW_DOT, callback: (tryNewDot) => { - handleChangeInHybridAppSignInFlow(currentHybridApp, tryNewDot, currentCredentials); + handleChangeInHybridAppSignInFlow(currentHybridApp, tryNewDot, currentCredentials, currentSession); }, }); @@ -32,16 +32,15 @@ Onyx.connect({ key: ONYXKEYS.CREDENTIALS, callback: (credentials) => { currentCredentials = credentials; - handleChangeInHybridAppSignInFlow(currentHybridApp, currentTryNewDot, credentials); + handleChangeInHybridAppSignInFlow(currentHybridApp, currentTryNewDot, credentials, currentSession); }, }); -let currentSession: OnyxEntry; Onyx.connect({ key: ONYXKEYS.SESSION, callback: (session: OnyxEntry) => { - if (!currentSession?.authToken && session?.authToken && currentHybridApp?.newDotSignInState === CONST.HYBRID_APP_SIGN_IN_STATE.STARTED) { - setNewDotSignInState(CONST.HYBRID_APP_SIGN_IN_STATE.FINISHED); + if (!currentSession?.authToken && session?.authToken) { + handleChangeInHybridAppSignInFlow(currentHybridApp, currentTryNewDot, currentCredentials, session); } currentSession = session; }, @@ -62,7 +61,7 @@ function shouldUseOldApp(tryNewDot?: TryNewDot) { return tryNewDot?.classicRedirect?.dismissed === true; } -function handleChangeInHybridAppSignInFlow(hybridApp: OnyxEntry, tryNewDot: OnyxEntry, credentials: OnyxEntry) { +function handleChangeInHybridAppSignInFlow(hybridApp: OnyxEntry, tryNewDot: OnyxEntry, credentials: OnyxEntry, session: OnyxEntry) { if (!CONFIG.IS_HYBRID_APP) { return; } @@ -73,14 +72,14 @@ function handleChangeInHybridAppSignInFlow(hybridApp: OnyxEntry, tryN return; } - if (hybridApp?.newDotSignInState === CONST.HYBRID_APP_SIGN_IN_STATE.FINISHED && tryNewDot !== undefined && !!credentials?.autoGeneratedLogin && !!credentials?.autoGeneratedPassword) { + if (!!session?.authToken && 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: currentSession?.authToken ?? '', + authToken: session.authToken, email: getCurrentUserEmail() ?? '', // eslint-disable-next-line rulesdir/no-default-id-values policyID: activePolicyID ?? '', diff --git a/src/libs/actions/HybridApp/index.ts b/src/libs/actions/HybridApp/index.ts index 6f655b83a4bb4..2d19507243ea8 100644 --- a/src/libs/actions/HybridApp/index.ts +++ b/src/libs/actions/HybridApp/index.ts @@ -1,7 +1,5 @@ import Onyx from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; import CONFIG from '@src/CONFIG'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {HybridApp} from '@src/types/onyx'; import type HybridAppSettings from './types'; @@ -24,17 +22,6 @@ function setReadyToShowAuthScreens(readyToShowAuthScreens: boolean) { Onyx.merge(ONYXKEYS.HYBRID_APP, {readyToShowAuthScreens}); } -/* - * Changes NewDot sign-in state - */ -function setNewDotSignInState(newDotSignInState: ValueOf) { - // 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, {newDotSignInState}); -} - function setUseNewDotSignInPage(useNewDotSignInPage: boolean) { // This value is only relevant for HybridApp, so we can skip it in other environments. if (!CONFIG.IS_HYBRID_APP) { @@ -62,7 +49,6 @@ function resetSignInFlow() { Onyx.merge(ONYXKEYS.HYBRID_APP, { readyToShowAuthScreens: false, - newDotSignInState: CONST.HYBRID_APP_SIGN_IN_STATE.NOT_STARTED, useNewDotSignInPage: true, }); } @@ -75,7 +61,6 @@ function prepareHybridAppAfterTransitionToNewDot(hybridApp: HybridApp) { return Onyx.merge(ONYXKEYS.HYBRID_APP, { ...hybridApp, readyToShowAuthScreens: !(hybridApp?.useNewDotSignInPage ?? false), - newDotSignInState: CONST.HYBRID_APP_SIGN_IN_STATE.NOT_STARTED, }); } @@ -86,4 +71,4 @@ function prepareHybridAppAfterTransitionToNewDot(hybridApp: HybridApp) { }); } -export {parseHybridAppSettings, setReadyToShowAuthScreens, setNewDotSignInState, resetSignInFlow, prepareHybridAppAfterTransitionToNewDot, setUseNewDotSignInPage, setClosingReactNativeApp}; +export {parseHybridAppSettings, setReadyToShowAuthScreens, resetSignInFlow, prepareHybridAppAfterTransitionToNewDot, setUseNewDotSignInPage, setClosingReactNativeApp}; diff --git a/src/pages/ValidateLoginPage/index.tsx b/src/pages/ValidateLoginPage/index.tsx index f7fc2b6d1d8f4..a26221a689dc2 100644 --- a/src/pages/ValidateLoginPage/index.tsx +++ b/src/pages/ValidateLoginPage/index.tsx @@ -2,9 +2,7 @@ import React, {useEffect} from 'react'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import useOnyx from '@hooks/useOnyx'; import Navigation from '@libs/Navigation/Navigation'; -import {setNewDotSignInState} from '@userActions/HybridApp'; -import {handleExitToNavigation, signInWithValidateCodeAndNavigate} from '@userActions/Session'; -import CONFIG from '@src/CONFIG'; +import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type ValidateLoginPageProps from './types'; @@ -14,7 +12,7 @@ function ValidateLoginPage({ params: {accountID, validateCode, exitTo}, }, }: ValidateLoginPageProps) { - const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true}); + const [session] = useOnyx(ONYXKEYS.SESSION); useEffect(() => { // Wait till navigation becomes available @@ -23,17 +21,12 @@ function ValidateLoginPage({ // If already signed in, do not show the validate code if not on web, // because we don't want to block the user with the interstitial page. if (exitTo) { - handleExitToNavigation(exitTo); + Session.handleExitToNavigation(exitTo); return; } Navigation.goBack(); } else { - // On HybridApp we need to orchestrate the sign-in flow of both apps so we need to set the state to STARTED here - if (CONFIG.IS_HYBRID_APP) { - setNewDotSignInState(CONST.HYBRID_APP_SIGN_IN_STATE.STARTED); - } - - signInWithValidateCodeAndNavigate(Number(accountID), validateCode, '', exitTo); + Session.signInWithValidateCodeAndNavigate(Number(accountID), validateCode, '', exitTo); } }); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps diff --git a/src/pages/signin/SAMLSignInPage/index.native.tsx b/src/pages/signin/SAMLSignInPage/index.native.tsx index 7bbddeb43ae59..434593d90d4ff 100644 --- a/src/pages/signin/SAMLSignInPage/index.native.tsx +++ b/src/pages/signin/SAMLSignInPage/index.native.tsx @@ -7,7 +7,6 @@ import SAMLLoadingIndicator from '@components/SAMLLoadingIndicator'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import {setNewDotSignInState} from '@libs/actions/HybridApp'; import getPlatform from '@libs/getPlatform'; import getUAForWebView from '@libs/getUAForWebView'; import Log from '@libs/Log'; @@ -15,13 +14,12 @@ import {handleSAMLLoginError, postSAMLLogin} from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import {clearSignInData, setAccountError, signInWithShortLivedAuthToken} from '@userActions/Session'; import CONFIG from '@src/CONFIG'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; function SAMLSignInPage() { - const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: false}); - const [credentials] = useOnyx(ONYXKEYS.CREDENTIALS, {canBeMissing: true}); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const [credentials] = useOnyx(ONYXKEYS.CREDENTIALS); const [showNavigation, shouldShowNavigation] = useState(true); const [SAMLUrl, setSAMLUrl] = useState(''); const webViewRef = useRef(null); @@ -71,7 +69,6 @@ function SAMLSignInPage() { if (!account?.isLoading && credentials?.login && !!shortLivedAuthToken) { Log.info('SAMLSignInPage - Successfully received shortLivedAuthToken. Signing in...'); signInWithShortLivedAuthToken(shortLivedAuthToken); - setNewDotSignInState(CONST.HYBRID_APP_SIGN_IN_STATE.STARTED); } // If the login attempt is unsuccessful, set the error message for the account and redirect to sign in page diff --git a/src/pages/signin/SignUpWelcomeForm.tsx b/src/pages/signin/SignUpWelcomeForm.tsx index 780667f70cc20..20ae01b52d7c4 100644 --- a/src/pages/signin/SignUpWelcomeForm.tsx +++ b/src/pages/signin/SignUpWelcomeForm.tsx @@ -7,9 +7,8 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {getLatestErrorMessage} from '@libs/ErrorUtils'; -import {setNewDotSignInState, setReadyToShowAuthScreens} from '@userActions/HybridApp'; +import {setReadyToShowAuthScreens} from '@userActions/HybridApp'; import {clearSignInData, signUpUser} from '@userActions/Session'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ChangeExpensifyLoginLink from './ChangeExpensifyLoginLink'; import Terms from './Terms'; @@ -31,7 +30,6 @@ function SignUpWelcomeForm() { text={translate('welcomeSignUpForm.join')} isLoading={account?.isLoading} onPress={() => { - setNewDotSignInState(CONST.HYBRID_APP_SIGN_IN_STATE.STARTED); signUpUser(); setReadyToShowAuthScreens(true); }} diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx index 5764f8f6efc10..e91ca4519e462 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -24,7 +24,7 @@ import {getLatestErrorMessage} from '@libs/ErrorUtils'; import {isValidRecoveryCode, isValidTwoFactorCode, isValidValidateCode} from '@libs/ValidationUtils'; import ChangeExpensifyLoginLink from '@pages/signin/ChangeExpensifyLoginLink'; import Terms from '@pages/signin/Terms'; -import {resetSignInFlow, setNewDotSignInState} from '@userActions/HybridApp'; +import {resetSignInFlow} from '@userActions/HybridApp'; import {clearAccountMessages, isAnonymousUser as isAnonymousUserUtil, clearSignInData as sessionActionsClearSignInData, signIn, signInWithValidateCode} from '@userActions/Session'; import {resendValidateCode as userActionsResendValidateCode} from '@userActions/User'; import CONFIG from '@src/CONFIG'; @@ -295,7 +295,6 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco const recoveryCodeOr2faCode = isUsingRecoveryCode ? recoveryCode : twoFactorAuthCode; - setNewDotSignInState(CONST.HYBRID_APP_SIGN_IN_STATE.STARTED); const accountID = credentials?.accountID; if (accountID) { signInWithValidateCode(accountID, validateCode, recoveryCodeOr2faCode); diff --git a/src/types/onyx/HybridApp.ts b/src/types/onyx/HybridApp.ts index b6fee12920cf0..bfef8decab654 100644 --- a/src/types/onyx/HybridApp.ts +++ b/src/types/onyx/HybridApp.ts @@ -1,6 +1,3 @@ -import type {ValueOf} from 'type-fest'; -import type CONST from '@src/CONST'; - /** Data structure holding user's OldDot access information */ type HybridAppDelegateAccessData = { /** Indicates if OldDot is accessed in a delegate mode */ @@ -36,9 +33,6 @@ type HybridApp = { /** Determines whether to remove delegated access */ shouldRemoveDelegatedAccess?: boolean; - /** Describes the current state of NewDot sign-in process */ - newDotSignInState?: ValueOf; - /** Holds delegate access information */ delegateAccessData?: HybridAppDelegateAccessData; From f9154287f101f1bb18fa1b9981416ea219b891fc Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 9 Jul 2025 13:04:48 +0200 Subject: [PATCH 09/29] remove unnecessary validate login check --- src/libs/actions/Report.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index dd9efa0f8f75a..687b654a668fd 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3389,11 +3389,6 @@ function openReportFromDeepLink(url: string) { return; } - if (CONST.REGEX.ROUTES.VALIDATE_LOGIN.test(`${route}`)) { - Navigation.navigate(route as Route); - return; - } - // Navigate to the report after sign-in/sign-up. InteractionManager.runAfterInteractions(() => { waitForUserSignIn().then(() => { From 5342ff5266391a78d8deef8f157dd57beb5b16cd Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 9 Jul 2025 16:04:45 +0200 Subject: [PATCH 10/29] enable public rooms for anonymus users --- src/libs/HybridApp.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/libs/HybridApp.ts b/src/libs/HybridApp.ts index a1adba9f1c6fc..2aa330a3fe4ec 100644 --- a/src/libs/HybridApp.ts +++ b/src/libs/HybridApp.ts @@ -5,7 +5,7 @@ import CONFIG from '@src/CONFIG'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Credentials, HybridApp, Session, TryNewDot} from '@src/types/onyx'; import {setReadyToShowAuthScreens, setUseNewDotSignInPage} from './actions/HybridApp'; -import {closeReactNativeApp} from './actions/Session'; +import {closeReactNativeApp, isAnonymousUser} from './actions/Session'; import Log from './Log'; import {getCurrentUserEmail} from './Network/NetworkStore'; @@ -66,13 +66,21 @@ function handleChangeInHybridAppSignInFlow(hybridApp: OnyxEntry, tryN return; } - if (!hybridApp?.useNewDotSignInPage) { - currentHybridApp = hybridApp; - currentTryNewDot = tryNewDot; + currentHybridApp = hybridApp; + currentTryNewDot = tryNewDot; + + if (!hybridApp?.useNewDotSignInPage || !session?.authToken) { return; } - if (!!session?.authToken && tryNewDot !== undefined && !!credentials?.autoGeneratedLogin && !!credentials?.autoGeneratedPassword) { + 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`); @@ -93,7 +101,4 @@ function handleChangeInHybridAppSignInFlow(hybridApp: OnyxEntry, tryN } }); } - - currentHybridApp = hybridApp; - currentTryNewDot = tryNewDot; } From a4b8f0431b94835342f618c7a4f15caede047ece Mon Sep 17 00:00:00 2001 From: war-in Date: Thu, 10 Jul 2025 14:10:50 +0200 Subject: [PATCH 11/29] fix closeReactNativeApp imports --- src/components/BookTravelButton.tsx | 2 +- src/libs/HybridApp.ts | 4 ++-- src/pages/ErrorPage/SessionExpiredPage.tsx | 3 +-- src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx | 3 +-- src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx | 4 ++-- src/pages/settings/InitialSettingsPage.tsx | 3 +-- src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx | 3 +-- 7 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/components/BookTravelButton.tsx b/src/components/BookTravelButton.tsx index 388b5896ebc8e..fe9e8c91ff864 100644 --- a/src/components/BookTravelButton.tsx +++ b/src/components/BookTravelButton.tsx @@ -14,7 +14,7 @@ import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import {getActivePolicies, getAdminsPrivateEmailDomains, isPaidGroupPolicy} from '@libs/PolicyUtils'; import colors from '@styles/theme/colors'; -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/libs/HybridApp.ts b/src/libs/HybridApp.ts index 2aa330a3fe4ec..04c4e3b598b23 100644 --- a/src/libs/HybridApp.ts +++ b/src/libs/HybridApp.ts @@ -4,8 +4,8 @@ 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 {setReadyToShowAuthScreens, setUseNewDotSignInPage} from './actions/HybridApp'; -import {closeReactNativeApp, isAnonymousUser} from './actions/Session'; +import {closeReactNativeApp, setReadyToShowAuthScreens, setUseNewDotSignInPage} from './actions/HybridApp'; +import {isAnonymousUser} from './actions/Session'; import Log from './Log'; import {getCurrentUserEmail} from './Network/NetworkStore'; diff --git a/src/pages/ErrorPage/SessionExpiredPage.tsx b/src/pages/ErrorPage/SessionExpiredPage.tsx index 31700b2aa8a7d..0ef53fe11ecf1 100644 --- a/src/pages/ErrorPage/SessionExpiredPage.tsx +++ b/src/pages/ErrorPage/SessionExpiredPage.tsx @@ -8,9 +8,8 @@ import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {closeReactNativeApp} from '@libs/actions/Session'; 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 1a008b8fed1de..3985d5256d57d 100644 --- a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx +++ b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx @@ -23,11 +23,9 @@ 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 {createWorkspace, generatePolicyID} from '@libs/actions/Policy/Policy'; import {completeOnboarding} from '@libs/actions/Report'; -import {closeReactNativeApp} from '@libs/actions/Session'; import {setOnboardingAdminsChatReportID, setOnboardingPolicyID} from '@libs/actions/Welcome'; import {navigateAfterOnboardingWithMicrotaskQueue} from '@libs/navigateAfterOnboarding'; import Navigation from '@libs/Navigation/Navigation'; @@ -41,6 +39,7 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {closeReactNativeApp} from '@userActions/HybridApp'; import type {BaseOnboardingAccountingProps} from './types'; type Integration = { diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index 35b332e9eb067..cb344d2b7bca9 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -24,7 +24,7 @@ import {startMoneyRequest} from '@libs/actions/IOU'; import {openOldDotLink, openTravelDotLink} from '@libs/actions/Link'; import {navigateToQuickAction} from '@libs/actions/QuickActionNavigation'; import {createNewReport, startNewChat} from '@libs/actions/Report'; -import {closeReactNativeApp, isAnonymousUser} from '@libs/actions/Session'; +import {isAnonymousUser} from '@libs/actions/Session'; import {completeTestDriveTask} from '@libs/actions/Task'; import getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; @@ -43,7 +43,7 @@ import {getQuickActionIcon, getQuickActionTitle, isQuickActionAllowed} from '@li import {generateReportID, getDisplayNameForParticipant, getIcons, getReportName, getWorkspaceChats, isArchivedReport, 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 b46e8ebb589ad..d66021d57a545 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -30,7 +30,6 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import '@libs/actions/Delegate'; import {resetExitSurveyForm} from '@libs/actions/ExitSurvey'; -import {closeReactNativeApp} from '@libs/actions/Session'; import {checkIfFeedConnectionIsBroken} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import useIsSidebarRouteActive from '@libs/Navigation/helpers/useIsSidebarRouteActive'; @@ -41,7 +40,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 a19e2f7d6f9ff..55f9bdd8c2384 100644 --- a/src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx +++ b/src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx @@ -13,11 +13,10 @@ import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {closeReactNativeApp} from '@libs/actions/Session'; 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 {navigateToConciergeChat} from '@userActions/Report'; import CONFIG from '@src/CONFIG'; From 4d0ac15475a861f152d98e2a08971dc170164f89 Mon Sep 17 00:00:00 2001 From: war-in Date: Thu, 10 Jul 2025 15:27:34 +0200 Subject: [PATCH 12/29] fix anonymus user sign in --- src/libs/HybridApp.ts | 11 ++++++----- .../signin/ValidateCodeForm/BaseValidateCodeForm.tsx | 12 ++---------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/libs/HybridApp.ts b/src/libs/HybridApp.ts index 04c4e3b598b23..5e21b38520c6b 100644 --- a/src/libs/HybridApp.ts +++ b/src/libs/HybridApp.ts @@ -17,6 +17,7 @@ let currentSession: OnyxEntry; Onyx.connect({ key: ONYXKEYS.HYBRID_APP, callback: (hybridApp) => { + currentHybridApp = hybridApp; handleChangeInHybridAppSignInFlow(hybridApp, currentTryNewDot, currentCredentials, currentSession); }, }); @@ -24,6 +25,7 @@ Onyx.connect({ Onyx.connect({ key: ONYXKEYS.NVP_TRY_NEW_DOT, callback: (tryNewDot) => { + currentTryNewDot = tryNewDot; handleChangeInHybridAppSignInFlow(currentHybridApp, tryNewDot, currentCredentials, currentSession); }, }); @@ -41,6 +43,8 @@ Onyx.connect({ 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; }, @@ -61,15 +65,12 @@ function shouldUseOldApp(tryNewDot?: TryNewDot) { return tryNewDot?.classicRedirect?.dismissed === true; } -function handleChangeInHybridAppSignInFlow(hybridApp: OnyxEntry, tryNewDot: OnyxEntry, credentials: OnyxEntry, session: OnyxEntry) { +function handleChangeInHybridAppSignInFlow(hybridApp: OnyxEntry, tryNewDot: OnyxEntry, credentials: OnyxEntry, session: OnyxEntry, usingSignInModal = false) { if (!CONFIG.IS_HYBRID_APP) { return; } - currentHybridApp = hybridApp; - currentTryNewDot = tryNewDot; - - if (!hybridApp?.useNewDotSignInPage || !session?.authToken) { + if (!session?.authToken || (!hybridApp?.useNewDotSignInPage && !usingSignInModal)) { return; } diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx index e91ca4519e462..db992d6182524 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -156,10 +156,6 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco * Trigger the reset validate code flow and ensure the 2FA input field is reset to avoid it being permanently hidden */ const resendValidateCode = () => { - if (CONFIG.IS_HYBRID_APP) { - resetSignInFlow(); - } - userActionsResendValidateCode(credentials?.login ?? ''); inputValidateCodeRef.current?.clear(); // Give feedback to the user to let them know the email was sent so that they don't spam the button. @@ -239,15 +235,11 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco * Check that all the form fields are valid, then trigger the submit callback */ const validateAndSubmitForm = useCallback(() => { - const isAnonymousUser = isAnonymousUserUtil(session); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (account?.isLoading || hybridApp?.readyToShowAuthScreens || (session?.authToken && !isAnonymousUser)) { + if (account?.isLoading) { return; } - if (CONFIG.IS_HYBRID_APP) { - resetSignInFlow(); - } + if (account?.errors) { clearAccountMessages(); } From 5fcf9adcc3bbbd3ff361cc4d2ac86dce40f6c46b Mon Sep 17 00:00:00 2001 From: war-in Date: Thu, 10 Jul 2025 17:17:21 +0200 Subject: [PATCH 13/29] fix lint & prettier --- src/App.tsx | 13 ++----------- src/HybridAppHandler.tsx | 2 +- src/components/InitialURLContextProvider.tsx | 3 +-- src/libs/HybridApp.ts | 8 +++++++- .../Navigation/AppNavigator/index.native.tsx | 2 +- .../BaseOnboardingAccounting.tsx | 2 +- src/pages/signin/ChooseSSOOrMagicCode.tsx | 2 +- .../ValidateCodeForm/BaseValidateCodeForm.tsx | 17 ++--------------- 8 files changed, 16 insertions(+), 33 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 6c8403909ee03..7b5ec99a1e2df 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,13 +47,6 @@ import './setup/backgroundTask'; import './setup/hybridApp'; import {SplashScreenStateContextProvider} from './SplashScreenStateContext'; -/** - * Properties passed to the top-level React Native component by HybridApp. - * These will always be `undefined` in "pure" NewDot builds. - */ -type AppProps = { -}; - LogBox.ignoreLogs([ // Basically it means that if the app goes in the background and back to foreground on Android, // the timer is lost. Currently Expensify is using a 30 minutes interval to refresh personal details. @@ -67,7 +60,7 @@ const fill = {flex: 1}; const StrictModeWrapper = CONFIG.USE_REACT_STRICT_MODE_IN_DEV ? React.StrictMode : ({children}: {children: React.ReactElement}) => children; -function App({}: AppProps) { +function App() { useDefaultDragAndDrop(); OnyxUpdateManager(); @@ -75,7 +68,7 @@ function App({}: AppProps) { - + ({ setInitialURL: () => {}, }); -type InitialURLContextProviderProps = AppProps & { +type InitialURLContextProviderProps = { /** Children passed to the context provider */ children: ReactNode; }; diff --git a/src/libs/HybridApp.ts b/src/libs/HybridApp.ts index 5e21b38520c6b..711614731a35d 100644 --- a/src/libs/HybridApp.ts +++ b/src/libs/HybridApp.ts @@ -65,7 +65,13 @@ function shouldUseOldApp(tryNewDot?: TryNewDot) { return tryNewDot?.classicRedirect?.dismissed === true; } -function handleChangeInHybridAppSignInFlow(hybridApp: OnyxEntry, tryNewDot: OnyxEntry, credentials: OnyxEntry, session: OnyxEntry, usingSignInModal = false) { +function handleChangeInHybridAppSignInFlow( + hybridApp: OnyxEntry, + tryNewDot: OnyxEntry, + credentials: OnyxEntry, + session: OnyxEntry, + usingSignInModal = false, +) { if (!CONFIG.IS_HYBRID_APP) { return; } diff --git a/src/libs/Navigation/AppNavigator/index.native.tsx b/src/libs/Navigation/AppNavigator/index.native.tsx index 285f6ddc830ff..474a71416b02e 100644 --- a/src/libs/Navigation/AppNavigator/index.native.tsx +++ b/src/libs/Navigation/AppNavigator/index.native.tsx @@ -1,5 +1,5 @@ import React, {memo, useMemo} from 'react'; -import {useOnyx} from 'react-native-onyx'; +import useOnyx from '@hooks/useOnyx'; import CONFIG from '@src/CONFIG'; import ONYXKEYS from '@src/ONYXKEYS'; import type ReactComponentModule from '@src/types/utils/ReactComponentModule'; diff --git a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx index 3985d5256d57d..c22aa72c56d29 100644 --- a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx +++ b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx @@ -33,13 +33,13 @@ 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'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {closeReactNativeApp} from '@userActions/HybridApp'; import type {BaseOnboardingAccountingProps} from './types'; type Integration = { diff --git a/src/pages/signin/ChooseSSOOrMagicCode.tsx b/src/pages/signin/ChooseSSOOrMagicCode.tsx index fab3cd258008e..20a5f11d01150 100644 --- a/src/pages/signin/ChooseSSOOrMagicCode.tsx +++ b/src/pages/signin/ChooseSSOOrMagicCode.tsx @@ -1,13 +1,13 @@ import {useFocusEffect} from '@react-navigation/native'; import React, {useCallback} from 'react'; import {Keyboard, View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; import Text from '@components/Text'; import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {getLatestErrorMessage} from '@libs/ErrorUtils'; diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx index db992d6182524..a2bef3532e666 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -24,10 +24,8 @@ import {getLatestErrorMessage} from '@libs/ErrorUtils'; import {isValidRecoveryCode, isValidTwoFactorCode, isValidValidateCode} from '@libs/ValidationUtils'; import ChangeExpensifyLoginLink from '@pages/signin/ChangeExpensifyLoginLink'; import Terms from '@pages/signin/Terms'; -import {resetSignInFlow} from '@userActions/HybridApp'; -import {clearAccountMessages, isAnonymousUser as isAnonymousUserUtil, clearSignInData as sessionActionsClearSignInData, signIn, signInWithValidateCode} from '@userActions/Session'; +import {clearAccountMessages, clearSignInData as sessionActionsClearSignInData, signIn, signInWithValidateCode} from '@userActions/Session'; import {resendValidateCode as userActionsResendValidateCode} from '@userActions/User'; -import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -293,18 +291,7 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco } else { signIn(validateCode, recoveryCodeOr2faCode); } - }, [ - account?.isLoading, - account?.errors, - account?.requiresTwoFactorAuth, - hybridApp?.readyToShowAuthScreens, - session, - isUsingRecoveryCode, - recoveryCode, - twoFactorAuthCode, - credentials?.accountID, - validateCode, - ]); + }, [account?.isLoading, account?.errors, account?.requiresTwoFactorAuth, isUsingRecoveryCode, recoveryCode, twoFactorAuthCode, credentials?.accountID, validateCode]); return ( From 53b196f9c059b204831876d025dc65d86b1a48ad Mon Sep 17 00:00:00 2001 From: war-in Date: Thu, 10 Jul 2025 17:21:54 +0200 Subject: [PATCH 14/29] fix typecheck --- src/ONYXKEYS.ts | 3 --- src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx | 1 - 2 files changed, 4 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 6e8e4cadfccd4..e34653001b2a0 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -532,9 +532,6 @@ const ONYXKEYS = { /** Set this gets redirected from global reimbursements flow */ IS_COMING_FROM_GLOBAL_REIMBURSEMENTS_FLOW: 'isComingFromGlobalReimbursementsFlow', - /** Stores HybridApp specific state required to interoperate with OldDot */ - HYBRID_APP: 'hybridApp', - /** Stores information for OpenUnreportedExpensesPage API call pagination */ HAS_MORE_UNREPORTED_TRANSACTIONS_RESULTS: 'hasMoreUnreportedTransactionsResults', diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx index a2bef3532e666..d52563761d60f 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -50,7 +50,6 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); const [credentials] = useOnyx(ONYXKEYS.CREDENTIALS, {canBeMissing: true}); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); - const [hybridApp] = useOnyx(ONYXKEYS.HYBRID_APP, {canBeMissing: false}); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); From ffb0d694054dbc44be4128ed0271d7a902eab8a6 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 15 Jul 2025 16:46:15 +0200 Subject: [PATCH 15/29] cleanup shouldHideSplash logic --- src/App.tsx | 2 +- src/Expensify.tsx | 10 ++++------ src/ONYXKEYS.ts | 1 - src/components/BookTravelButton.tsx | 2 -- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7b5ec99a1e2df..ab26e156ec872 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -82,10 +82,10 @@ function App() { ThemeProvider, ThemeStylesProvider, ThemeIllustrationsProvider, + HTMLEngineProvider, PortalProvider, SafeArea, LocaleContextProvider, - HTMLEngineProvider, PopoverContextProvider, CurrentReportIDContextProvider, ScrollOffsetContextProvider, diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 2d9470f5f0495..5952144f99672 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -117,12 +117,10 @@ function Expensify() { const isAuthenticated = useIsAuthenticated(); const autoAuthState = useMemo(() => session?.autoAuthState ?? '', [session]); - const shouldInit = isNavigationReady && hasAttemptedToOpenPublicRoom && !!preferredLocale && (CONFIG.IS_HYBRID_APP ? !hybridApp?.loggedOutFromOldDot : true); - const shouldHideSplash = - shouldInit && - (CONFIG.IS_HYBRID_APP - ? splashScreenState === CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN && (isAuthenticated || !!hybridApp?.useNewDotSignInPage) - : splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE); + const shouldInit = isNavigationReady && hasAttemptedToOpenPublicRoom && !!preferredLocale; + const isSplashVisible = splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE; + const isHybridAppReady = splashScreenState === CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN && (isAuthenticated || !!hybridApp?.useNewDotSignInPage); + const shouldHideSplash = shouldInit && (CONFIG.IS_HYBRID_APP ? isHybridAppReady : isSplashVisible); const initializeClient = () => { if (!Visibility.isVisible()) { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index e34653001b2a0..a982a7d2d0b95 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1191,7 +1191,6 @@ type OnyxValuesMapping = { [ONYXKEYS.SCHEDULE_CALL_DRAFT]: OnyxTypes.ScheduleCallDraft; [ONYXKEYS.IS_FORCED_TO_CHANGE_CURRENCY]: boolean | undefined; [ONYXKEYS.IS_COMING_FROM_GLOBAL_REIMBURSEMENTS_FLOW]: boolean | undefined; - [ONYXKEYS.HYBRID_APP]: OnyxTypes.HybridApp; [ONYXKEYS.HAS_MORE_UNREPORTED_TRANSACTIONS_RESULTS]: boolean | undefined; [ONYXKEYS.IS_LOADING_UNREPORTED_TRANSACTIONS]: boolean | undefined; [ONYXKEYS.NVP_LAST_ECASH_IOS_LOGIN]: string; diff --git a/src/components/BookTravelButton.tsx b/src/components/BookTravelButton.tsx index 8991616431a33..fbf456e6deb45 100644 --- a/src/components/BookTravelButton.tsx +++ b/src/components/BookTravelButton.tsx @@ -13,8 +13,6 @@ import Navigation from '@libs/Navigation/Navigation'; import {openTravelDotLink} from '@libs/openTravelDotLink'; import {getActivePolicies, getAdminsPrivateEmailDomains, isPaidGroupPolicy} from '@libs/PolicyUtils'; import colors from '@styles/theme/colors'; -import {closeReactNativeApp} from '@userActions/HybridApp'; -import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; From 6d38ec7184425fc7a84eafa9869e00c27fd456ec Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 15 Jul 2025 16:58:43 +0200 Subject: [PATCH 16/29] fix degraded performance when signing out from OD by bumping react-native-performance --- package-lock.json | 6 ++-- package.json | 2 +- ...ive-performance+5.1.0+001+bridgeless.patch | 30 ------------------- ...y-checking-for-active-react-instance.patch | 28 +++++++++++++++++ 4 files changed, 33 insertions(+), 33 deletions(-) delete mode 100644 patches/react-native-performance+5.1.0+001+bridgeless.patch create mode 100644 patches/react-native-performance+5.1.4+001+fix-soft-crash-by-checking-for-active-react-instance.patch diff --git a/package-lock.json b/package-lock.json index ba7d8ec322a4a..7143a2c3f1cb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -112,7 +112,7 @@ "react-native-onyx": "^2.0.117", "react-native-pager-view": "6.5.3", "react-native-pdf": "6.7.3", - "react-native-performance": "^5.1.0", + "react-native-performance": "^5.1.4", "react-native-permissions": "^5.4.0", "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#07d60d78d4772d47afd7a744940fc6b6d1881806", "react-native-plaid-link-sdk": "11.11.0", @@ -33069,7 +33069,9 @@ } }, "node_modules/react-native-performance": { - "version": "5.1.0", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/react-native-performance/-/react-native-performance-5.1.4.tgz", + "integrity": "sha512-ydqDe8EFlA9iuhA/DBYAnt4Hs3lpmLKcaXPsDfQJwowT4dXPgLgUDqJUuDIYEVsEsjZybloSmUj0fvUuUogqpw==", "license": "MIT", "peerDependencies": { "react-native": "*" diff --git a/package.json b/package.json index b9aea029b8997..f91c5d6385dc0 100644 --- a/package.json +++ b/package.json @@ -181,7 +181,7 @@ "react-native-onyx": "^2.0.117", "react-native-pager-view": "6.5.3", "react-native-pdf": "6.7.3", - "react-native-performance": "^5.1.0", + "react-native-performance": "^5.1.4", "react-native-permissions": "^5.4.0", "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#07d60d78d4772d47afd7a744940fc6b6d1881806", "react-native-plaid-link-sdk": "11.11.0", diff --git a/patches/react-native-performance+5.1.0+001+bridgeless.patch b/patches/react-native-performance+5.1.0+001+bridgeless.patch deleted file mode 100644 index 7aed8cf57487e..0000000000000 --- a/patches/react-native-performance+5.1.0+001+bridgeless.patch +++ /dev/null @@ -1,30 +0,0 @@ -diff --git a/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java b/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java -index 2fa7d5d..10e1ba6 100644 ---- a/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java -+++ b/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java -@@ -17,7 +17,7 @@ import java.util.Queue; - import java.util.concurrent.ConcurrentLinkedQueue; - - // Should extend NativeRNPerformanceManagerSpec when codegen for old architecture is solved --public class PerformanceModule extends ReactContextBaseJavaModule implements TurboModule, RNPerformance.MarkerListener { -+public class PerformanceModule extends NativeRNPerformanceManagerSpec implements RNPerformance.MarkerListener { - public static final String PERFORMANCE_MODULE = "RNPerformanceManager"; - public static final String BRIDGE_SETUP_START = "bridgeSetupStart"; - -@@ -118,6 +118,16 @@ public class PerformanceModule extends ReactContextBaseJavaModule implements Tur - return PERFORMANCE_MODULE; - } - -+ @Override -+ public void addListener(String eventName) { -+ // needed for spec -+ } -+ -+ @Override -+ public void removeListeners(double count) { -+ // needed for spec -+ } -+ - private void emitNativeStartupTime() { - safelyEmitMark(new PerformanceMark("nativeLaunchStart", StartTimeProvider.getStartTime())); - safelyEmitMark(new PerformanceMark("nativeLaunchEnd", StartTimeProvider.getEndTime())); diff --git a/patches/react-native-performance+5.1.4+001+fix-soft-crash-by-checking-for-active-react-instance.patch b/patches/react-native-performance+5.1.4+001+fix-soft-crash-by-checking-for-active-react-instance.patch new file mode 100644 index 0000000000000..3d40972c8e2ca --- /dev/null +++ b/patches/react-native-performance+5.1.4+001+fix-soft-crash-by-checking-for-active-react-instance.patch @@ -0,0 +1,28 @@ +diff --git a/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java b/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java +index 4a187bb..66c94ee 100644 +--- a/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java ++++ b/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java +@@ -187,7 +187,7 @@ public class PerformanceModule extends ReactContextBaseJavaModule implements Tur + WritableMap map = Arguments.fromBundle(metric.getDetail()); + params.putMap("detail", map); + } +- if (getReactApplicationContext().hasActiveCatalystInstance()) { ++ if (getReactApplicationContext().hasActiveReactInstance()) { + getReactApplicationContext() + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("metric", params); +@@ -202,9 +202,11 @@ public class PerformanceModule extends ReactContextBaseJavaModule implements Tur + WritableMap map = Arguments.fromBundle(mark.getDetail()); + params.putMap("detail", map); + } +- getReactApplicationContext() +- .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) +- .emit("mark", params); ++ if (getReactApplicationContext().hasActiveReactInstance()) { ++ getReactApplicationContext() ++ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) ++ .emit("mark", params); ++ } + } + + @Override From d461b59d402abe8f8b7977625cc414426cb6a5ad Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 16 Jul 2025 12:31:14 +0200 Subject: [PATCH 17/29] improve signing out from OD --- src/libs/actions/Session/index.ts | 6 +++--- src/pages/signin/SignInPage.tsx | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 4d3c4e2344233..e0003e470214e 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -72,8 +72,8 @@ const INVALID_TOKEN = 'pizza'; let session: Session = {}; let authPromiseResolver: ((value: boolean) => void) | null = null; -let isSetUpReady = false; +let isHybridAppSetupFinished = false; let hasSwitchedAccountInHybridMode = false; Onyx.connect({ @@ -87,7 +87,7 @@ Onyx.connect({ authPromiseResolver(true); authPromiseResolver = null; } - if (CONFIG.IS_HYBRID_APP && session.authToken && session.authToken !== INVALID_TOKEN && isSetUpReady) { + if (CONFIG.IS_HYBRID_APP && isHybridAppSetupFinished && session.authToken && session.authToken !== INVALID_TOKEN && !isAnonymousUser(value)) { HybridAppModule.sendAuthToken({authToken: session.authToken}); } }, @@ -588,7 +588,7 @@ function setupNewDotAfterTransitionFromOldDot(hybridAppSettings: HybridAppSettin .then(resetDidUserLoginDuringSessionIfNeeded) .then(() => Promise.all(Object.entries(newDotOnyxValues).map(([key, value]) => Onyx.merge(key as OnyxKey, value ?? {})))) .then(() => { - isSetUpReady = true; + isHybridAppSetupFinished = true; return Promise.resolve(); }) .catch((error) => { diff --git a/src/pages/signin/SignInPage.tsx b/src/pages/signin/SignInPage.tsx index b46de34e83197..7257b762088d9 100644 --- a/src/pages/signin/SignInPage.tsx +++ b/src/pages/signin/SignInPage.tsx @@ -306,12 +306,15 @@ function SignInPage({shouldEnableMaxHeight = true}: SignInPageInnerProps, ref: F navigateBack, })); + // This should be executed when SignInPage appears for the first time + // It's closing `Signing out` modal from OldDot useEffect(() => { if (!CONFIG.IS_HYBRID_APP || !hybridApp?.loggedOutFromOldDot) { return; } HybridAppModule.clearOldDotAfterSignOut(); - }, [hybridApp?.loggedOutFromOldDot]); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, []); return ( // Bottom SafeAreaView is removed so that login screen svg displays correctly on mobile. From 607190b3c57aafb43f18ccee64b37840ae8963dc Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 16 Jul 2025 13:00:11 +0200 Subject: [PATCH 18/29] explain why we clear OD in SignInPage --- src/pages/signin/SignInPage.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/signin/SignInPage.tsx b/src/pages/signin/SignInPage.tsx index 7257b762088d9..a340127a1bce0 100644 --- a/src/pages/signin/SignInPage.tsx +++ b/src/pages/signin/SignInPage.tsx @@ -306,15 +306,14 @@ function SignInPage({shouldEnableMaxHeight = true}: SignInPageInnerProps, ref: F navigateBack, })); - // This should be executed when SignInPage appears for the first time - // It's closing `Signing out` modal from OldDot + // This effect is closing OldDot sign out modal when SignInPage appears + // It can't be performed in any other place because there would be an empty screen for a while useEffect(() => { if (!CONFIG.IS_HYBRID_APP || !hybridApp?.loggedOutFromOldDot) { return; } HybridAppModule.clearOldDotAfterSignOut(); - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, []); + }, [hybridApp?.loggedOutFromOldDot]); return ( // Bottom SafeAreaView is removed so that login screen svg displays correctly on mobile. From cd9ff6cf0d469cc986d622fce0615c2008961af6 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 18 Jul 2025 10:38:14 +0200 Subject: [PATCH 19/29] post-merge fixes --- ...y-checking-for-active-react-instance.patch | 28 ------------------- src/pages/signin/ChooseSSOOrMagicCode.tsx | 3 -- .../ValidateCodeForm/BaseValidateCodeForm.tsx | 2 -- 3 files changed, 33 deletions(-) delete mode 100644 patches/react-native-performance+5.1.4+001+fix-soft-crash-by-checking-for-active-react-instance.patch diff --git a/patches/react-native-performance+5.1.4+001+fix-soft-crash-by-checking-for-active-react-instance.patch b/patches/react-native-performance+5.1.4+001+fix-soft-crash-by-checking-for-active-react-instance.patch deleted file mode 100644 index 3d40972c8e2ca..0000000000000 --- a/patches/react-native-performance+5.1.4+001+fix-soft-crash-by-checking-for-active-react-instance.patch +++ /dev/null @@ -1,28 +0,0 @@ -diff --git a/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java b/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java -index 4a187bb..66c94ee 100644 ---- a/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java -+++ b/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java -@@ -187,7 +187,7 @@ public class PerformanceModule extends ReactContextBaseJavaModule implements Tur - WritableMap map = Arguments.fromBundle(metric.getDetail()); - params.putMap("detail", map); - } -- if (getReactApplicationContext().hasActiveCatalystInstance()) { -+ if (getReactApplicationContext().hasActiveReactInstance()) { - getReactApplicationContext() - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit("metric", params); -@@ -202,9 +202,11 @@ public class PerformanceModule extends ReactContextBaseJavaModule implements Tur - WritableMap map = Arguments.fromBundle(mark.getDetail()); - params.putMap("detail", map); - } -- getReactApplicationContext() -- .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) -- .emit("mark", params); -+ if (getReactApplicationContext().hasActiveReactInstance()) { -+ getReactApplicationContext() -+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) -+ .emit("mark", params); -+ } - } - - @Override diff --git a/src/pages/signin/ChooseSSOOrMagicCode.tsx b/src/pages/signin/ChooseSSOOrMagicCode.tsx index d62a13ba227a9..c93a7f895ed44 100644 --- a/src/pages/signin/ChooseSSOOrMagicCode.tsx +++ b/src/pages/signin/ChooseSSOOrMagicCode.tsx @@ -34,9 +34,6 @@ function ChooseSSOOrMagicCode({setIsUsingMagicCode}: ChooseSSOOrMagicCodeProps) const [credentials] = useOnyx(ONYXKEYS.CREDENTIALS, {canBeMissing: true}); const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); - const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: false}); - const [credentials] = useOnyx(ONYXKEYS.CREDENTIALS, {canBeMissing: true}); - // This view doesn't have a field for user input, so dismiss the device keyboard if shown useFocusEffect( useCallback(() => { diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx index d52563761d60f..5ac19c9afaf86 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -232,11 +232,9 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco * Check that all the form fields are valid, then trigger the submit callback */ const validateAndSubmitForm = useCallback(() => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (account?.isLoading) { return; } - if (account?.errors) { clearAccountMessages(); } From 1b7f447d6330de70f24f27c60ace686cb9dc4af2 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 18 Jul 2025 15:53:37 +0200 Subject: [PATCH 20/29] fix sign-in flow --- src/libs/HybridApp.ts | 7 ++++--- src/libs/actions/Session/index.ts | 6 +++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/libs/HybridApp.ts b/src/libs/HybridApp.ts index 711614731a35d..e1579b673fb37 100644 --- a/src/libs/HybridApp.ts +++ b/src/libs/HybridApp.ts @@ -4,6 +4,7 @@ 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'; @@ -58,11 +59,11 @@ Onyx.connect({ }, }); -function shouldUseOldApp(tryNewDot?: TryNewDot) { - if (!!tryNewDot && !tryNewDot.classicRedirect) { +function shouldUseOldApp(tryNewDot: TryNewDot) { + if (isEmptyObject(tryNewDot) || isEmptyObject(tryNewDot.classicRedirect)) { return true; } - return tryNewDot?.classicRedirect?.dismissed === true; + return tryNewDot.classicRedirect.dismissed; } function handleChangeInHybridAppSignInFlow( diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index e0003e470214e..c90c14b3c80e0 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -458,7 +458,11 @@ function beginSignIn(email: string) { const params: BeginSignInParams = {email}; - API.read(READ_COMMANDS.BEGIN_SIGNIN, params, {optimisticData, successData, failureData}); + // eslint-disable-next-line rulesdir/no-api-side-effects-method + API.makeRequestWithSideEffects(READ_COMMANDS.BEGIN_SIGNIN, params, {optimisticData, successData, failureData}).then(() => { + // TODO: this is only a temporary change until we fix things on backend side + Onyx.merge(ONYXKEYS.NVP_TRY_NEW_DOT, null); + }); } /** From 6eb0c4475886b58727e3eee412c53dbc0df8f123 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 22 Jul 2025 16:36:05 +0200 Subject: [PATCH 21/29] remove temporary change --- src/libs/actions/Session/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index c90c14b3c80e0..1df837bbc4f6d 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -459,10 +459,7 @@ function beginSignIn(email: string) { const params: BeginSignInParams = {email}; // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects(READ_COMMANDS.BEGIN_SIGNIN, params, {optimisticData, successData, failureData}).then(() => { - // TODO: this is only a temporary change until we fix things on backend side - Onyx.merge(ONYXKEYS.NVP_TRY_NEW_DOT, null); - }); + API.read(READ_COMMANDS.BEGIN_SIGNIN, params, {optimisticData, successData, failureData}); } /** From 5e21bd53efd726aff3733db0b42c30fca0a11fe0 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 22 Jul 2025 18:16:06 +0200 Subject: [PATCH 22/29] improve splash screen hiding logic --- src/Expensify.tsx | 17 ++++++++++++++--- src/HybridAppHandler.tsx | 8 +++----- src/pages/signin/SignInPage.tsx | 12 ------------ 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 5952144f99672..cc0e0a73d6a86 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -117,10 +117,21 @@ 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 || !!hybridApp?.useNewDotSignInPage); - 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 || !shouldInit || !hybridApp?.loggedOutFromOldDot) { + return; + } + + setSplashScreenState(CONST.BOOT_SPLASH_STATE.HIDDEN); + HybridAppModule.clearOldDotAfterSignOut(); + }, [hybridApp?.loggedOutFromOldDot, isSplashReadyToBeHidden, setSplashScreenState, shouldInit, splashScreenState]); const initializeClient = () => { if (!Visibility.isVisible()) { diff --git a/src/HybridAppHandler.tsx b/src/HybridAppHandler.tsx index 7184638d55ffe..3535a5c5283f1 100644 --- a/src/HybridAppHandler.tsx +++ b/src/HybridAppHandler.tsx @@ -29,13 +29,11 @@ function HybridAppHandler() { } const parsedHybridAppSettings = parseHybridAppSettings(hybridAppSettings); - setupNewDotAfterTransitionFromOldDot(parsedHybridAppSettings, tryNewDot).then(() => { - if (parsedHybridAppSettings.hybridApp?.loggedOutFromOldDot) { - setSplashScreenState(CONST.BOOT_SPLASH_STATE.HIDDEN); - } else if (splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE) { - setSplashScreenState(CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN); + if (splashScreenState !== CONST.BOOT_SPLASH_STATE.VISIBLE) { + return; } + setSplashScreenState(CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN); }); }); }, [isLoading, setSplashScreenState, splashScreenState, tryNewDot]); diff --git a/src/pages/signin/SignInPage.tsx b/src/pages/signin/SignInPage.tsx index a340127a1bce0..99293ff9c2512 100644 --- a/src/pages/signin/SignInPage.tsx +++ b/src/pages/signin/SignInPage.tsx @@ -1,4 +1,3 @@ -import HybridAppModule from '@expensify/react-native-hybrid-app'; import {Str} from 'expensify-common'; import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; import type {ForwardedRef} from 'react'; @@ -22,7 +21,6 @@ import Performance from '@libs/Performance'; import Visibility from '@libs/Visibility'; import {setLocale} from '@userActions/App'; import {clearSignInData} from '@userActions/Session'; -import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -185,7 +183,6 @@ function SignInPage({shouldEnableMaxHeight = true}: SignInPageInnerProps, ref: F // We need to show "Another login page is opened" message if the page isn't active and visible // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowAnotherLoginPageOpenedMessage = Visibility.isVisible() && !isClientTheLeader; - const [hybridApp] = useOnyx(ONYXKEYS.HYBRID_APP, {canBeMissing: true}); useEffect(() => Performance.measureTTI(), []); @@ -306,15 +303,6 @@ function SignInPage({shouldEnableMaxHeight = true}: SignInPageInnerProps, ref: F navigateBack, })); - // This effect is closing OldDot sign out modal when SignInPage appears - // It can't be performed in any other place because there would be an empty screen for a while - useEffect(() => { - if (!CONFIG.IS_HYBRID_APP || !hybridApp?.loggedOutFromOldDot) { - return; - } - HybridAppModule.clearOldDotAfterSignOut(); - }, [hybridApp?.loggedOutFromOldDot]); - return ( // Bottom SafeAreaView is removed so that login screen svg displays correctly on mobile. // The SVG should flow under the Home Indicator on iOS. From 857ea6d172a21c29822161ecc5f64364acdf0c3a Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 22 Jul 2025 18:30:21 +0200 Subject: [PATCH 23/29] revert unnecessary changes --- src/HybridAppHandler.tsx | 3 +-- src/pages/signin/ChooseSSOOrMagicCode.tsx | 17 +++++++---------- src/pages/signin/SignInPage.tsx | 1 - 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/HybridAppHandler.tsx b/src/HybridAppHandler.tsx index 3535a5c5283f1..dd4b08cc79d08 100644 --- a/src/HybridAppHandler.tsx +++ b/src/HybridAppHandler.tsx @@ -28,8 +28,7 @@ function HybridAppHandler() { return; } - const parsedHybridAppSettings = parseHybridAppSettings(hybridAppSettings); - setupNewDotAfterTransitionFromOldDot(parsedHybridAppSettings, tryNewDot).then(() => { + setupNewDotAfterTransitionFromOldDot(parseHybridAppSettings(hybridAppSettings), tryNewDot).then(() => { if (splashScreenState !== CONST.BOOT_SPLASH_STATE.VISIBLE) { return; } diff --git a/src/pages/signin/ChooseSSOOrMagicCode.tsx b/src/pages/signin/ChooseSSOOrMagicCode.tsx index c93a7f895ed44..0e626390e5e83 100644 --- a/src/pages/signin/ChooseSSOOrMagicCode.tsx +++ b/src/pages/signin/ChooseSSOOrMagicCode.tsx @@ -1,5 +1,4 @@ -import {useFocusEffect} from '@react-navigation/native'; -import React, {useCallback} from 'react'; +import React, {useEffect} from 'react'; import {Keyboard, View} from 'react-native'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -35,14 +34,12 @@ function ChooseSSOOrMagicCode({setIsUsingMagicCode}: ChooseSSOOrMagicCodeProps) const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); // This view doesn't have a field for user input, so dismiss the device keyboard if shown - useFocusEffect( - useCallback(() => { - if (!isKeyboardShown) { - return; - } - Keyboard.dismiss(); - }, [isKeyboardShown]), - ); + useEffect(() => { + if (!isKeyboardShown) { + return; + } + Keyboard.dismiss(); + }, [isKeyboardShown]); return ( <> diff --git a/src/pages/signin/SignInPage.tsx b/src/pages/signin/SignInPage.tsx index 99293ff9c2512..ccddac958a1b4 100644 --- a/src/pages/signin/SignInPage.tsx +++ b/src/pages/signin/SignInPage.tsx @@ -302,7 +302,6 @@ function SignInPage({shouldEnableMaxHeight = true}: SignInPageInnerProps, ref: F useImperativeHandle(ref, () => ({ navigateBack, })); - return ( // Bottom SafeAreaView is removed so that login screen svg displays correctly on mobile. // The SVG should flow under the Home Indicator on iOS. From a819ec6f6b296119fd9489485d2deac3d15e3e35 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 22 Jul 2025 18:35:09 +0200 Subject: [PATCH 24/29] fix typescript issue --- .../BaseOnboardingInterestedFeatures.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx index c1e49acb1b9a7..1f5e349d05df9 100644 --- a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx +++ b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx @@ -30,7 +30,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'; From 7f7064cc06a56064af60361e741974907ca9dfac Mon Sep 17 00:00:00 2001 From: war-in Date: Thu, 24 Jul 2025 11:49:27 +0200 Subject: [PATCH 25/29] set correct google sign-in webClientId --- src/CONFIG.ts | 14 ++++++++++---- .../GoogleSignIn/index.native.tsx | 18 ++++++++++++++++-- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/CONFIG.ts b/src/CONFIG.ts index 569995f679c99..2ccf86585e7fe 100644 --- a/src/CONFIG.ts +++ b/src/CONFIG.ts @@ -97,10 +97,16 @@ export default { WEB_CLIENT_ID: '921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com', // cspell:disable-next-line IOS_CLIENT_ID: '921154746561-s3uqn2oe4m85tufi6mqflbfbuajrm2i3.apps.googleusercontent.com', - // cspell:disable-next-line - HYBRID_APP_WEB_CLIENT_ID: '1008697809946-5e095eqem3o6ugtpc2rjf7v880tcp28p.apps.googleusercontent.com', - // cspell:disable-next-line - HYBRID_APP_IOS_CLIENT_ID: '1008697809946-sh04nqq0hea396s1qdqqbj6ia649odb2.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/components/SignInButtons/GoogleSignIn/index.native.tsx b/src/components/SignInButtons/GoogleSignIn/index.native.tsx index 9d636d47fc672..41d04620e3e77 100644 --- a/src/components/SignInButtons/GoogleSignIn/index.native.tsx +++ b/src/components/SignInButtons/GoogleSignIn/index.native.tsx @@ -5,16 +5,30 @@ import Log from '@libs/Log'; import {beginGoogleSignIn} from '@userActions/Session'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; +import getPlatform from '@libs/getPlatform'; 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.IS_HYBRID_APP ? CONFIG.GOOGLE_SIGN_IN.HYBRID_APP_WEB_CLIENT_ID : CONFIG.GOOGLE_SIGN_IN.WEB_CLIENT_ID, - iosClientId: CONFIG.IS_HYBRID_APP ? CONFIG.GOOGLE_SIGN_IN.HYBRID_APP_IOS_CLIENT_ID : 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, }); From cf18531aab2cd1ebc9abf8856f4147c6aa639d90 Mon Sep 17 00:00:00 2001 From: war-in Date: Thu, 24 Jul 2025 13:01:15 +0200 Subject: [PATCH 26/29] revert SafeAreaProvider change --- src/App.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 2defc5ac81ba9..ed19d7a59989f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -70,18 +70,13 @@ function App() { - - From fc4952f446463f07b16c5712b1a72e619c3eaeb1 Mon Sep 17 00:00:00 2001 From: war-in Date: Mon, 28 Jul 2025 18:43:11 +0200 Subject: [PATCH 27/29] fix prettier --- src/App.tsx | 82 +++++++++---------- src/CONFIG.ts | 2 +- .../GoogleSignIn/index.native.tsx | 6 +- 3 files changed, 44 insertions(+), 46 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index ed19d7a59989f..cc3e3d61a4cfa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -70,47 +70,47 @@ function App() { - - - - - - - - - + + + + + + + + + diff --git a/src/CONFIG.ts b/src/CONFIG.ts index 2ccf86585e7fe..aa7b3d3963a13 100644 --- a/src/CONFIG.ts +++ b/src/CONFIG.ts @@ -105,7 +105,7 @@ export default { IOS: '1008697809946-5e095eqem3o6ugtpc2rjf7v880tcp28p.apps.googleusercontent.com', // cspell:disable-next-line ANDROID: '240677659774-86pov3adub93cv4b8uj13g7varolmk2l.apps.googleusercontent.com', - } + }, }, }, GCP_GEOLOCATION_API_KEY: googleGeolocationAPIKey, diff --git a/src/components/SignInButtons/GoogleSignIn/index.native.tsx b/src/components/SignInButtons/GoogleSignIn/index.native.tsx index 41d04620e3e77..70144444d282f 100644 --- a/src/components/SignInButtons/GoogleSignIn/index.native.tsx +++ b/src/components/SignInButtons/GoogleSignIn/index.native.tsx @@ -1,11 +1,11 @@ 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 {beginGoogleSignIn} from '@userActions/Session'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; -import getPlatform from '@libs/getPlatform'; import type {GoogleSignInProps} from '.'; import type GoogleError from './types'; @@ -17,9 +17,7 @@ function getWebClientId() { 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; + 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; } /** From 8641d50f77705a12ba6671ecdf29a9878204215a Mon Sep 17 00:00:00 2001 From: war-in Date: Thu, 31 Jul 2025 11:08:57 +0200 Subject: [PATCH 28/29] fix broken import in HybridAppHandler.tsx --- src/HybridAppHandler.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HybridAppHandler.tsx b/src/HybridAppHandler.tsx index dd4b08cc79d08..a73c93e3c868a 100644 --- a/src/HybridAppHandler.tsx +++ b/src/HybridAppHandler.tsx @@ -1,4 +1,4 @@ -import HybridAppModule from '@expensify/react-native-hybrid-app/src/index.native'; +import HybridAppModule from '@expensify/react-native-hybrid-app'; import {useContext, useEffect} from 'react'; import CONFIG from './CONFIG'; import CONST from './CONST'; From 18baf7e0342c54a91b210e677218a7930500c50d Mon Sep 17 00:00:00 2001 From: war-in Date: Thu, 31 Jul 2025 13:26:11 +0200 Subject: [PATCH 29/29] fix infinite `Signing out...` iOS dialog --- src/App.tsx | 90 +++++++++++++++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index cc3e3d61a4cfa..f42ac14638825 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -70,47 +70,57 @@ function App() { - - - - - - - - - + + + + + + + + + +