diff --git a/__mocks__/@ua/react-native-airship.ts b/__mocks__/@ua/react-native-airship.ts index a60053df475e0..08ec1b781fc21 100644 --- a/__mocks__/@ua/react-native-airship.ts +++ b/__mocks__/@ua/react-native-airship.ts @@ -6,6 +6,13 @@ enum EventType { PushReceived = 'com.airship.push_received', } +// eslint-disable-next-line no-restricted-syntax +enum PermissionStatus { + Granted = 'granted', + Denied = 'denied', + NotDetermined = 'not_determined', +} + // eslint-disable-next-line @typescript-eslint/no-namespace namespace iOS { /** @@ -71,4 +78,4 @@ const Airship: Partial = { export default Airship; -export {EventType, iOS}; +export {EventType, iOS, PermissionStatus}; diff --git a/modules/hybrid-app/android/src/main/java/com/expensify/reactnativehybridapp/ReactNativeHybridApp.kt b/modules/hybrid-app/android/src/main/java/com/expensify/reactnativehybridapp/ReactNativeHybridApp.kt index 9a743490ffa3f..46c7968d6cca5 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 @@ -36,4 +36,22 @@ class ReactNativeHybridApp(reactContext: ReactApplicationContext) : override fun sendAuthToken(authToken: String?) { Log.d(NAME, "`sendAuthToken` should never be called in standalone `New Expensify` app") } + + override fun signInToOldDot( + autoGeneratedLogin: String, + autoGeneratedPassword: String, + authToken: String, + email: String, + policyID: String + ) { + Log.d(NAME, "`signInToOldDot` should never be called in standalone `New Expensify` app") + } + + override fun signOutFromOldDot() { + Log.d(NAME, "`signOutFromOldDot` should never be called in standalone `New Expensify` app") + } + + override fun clearOldDotAfterSignOut() { + Log.d(NAME, "`clearOldDotAfterSignOut` should never be called in standalone `New Expensify` app") + } } diff --git a/modules/hybrid-app/ios/ReactNativeHybridApp.mm b/modules/hybrid-app/ios/ReactNativeHybridApp.mm index 6b093b3f26b5b..f94bccf5d5605 100644 --- a/modules/hybrid-app/ios/ReactNativeHybridApp.mm +++ b/modules/hybrid-app/ios/ReactNativeHybridApp.mm @@ -27,6 +27,18 @@ - (void)sendAuthToken:(NSString *)authToken { NSLog(@"[ReactNativeHybridApp] `sendAuthToken` should never be called in standalone `New Expensify` app"); } +- (void)signInToOldDot:(NSString *)autoGeneratedLogin autoGeneratedPassword:(NSString *)autoGeneratedPassword authToken:(NSString *)authToken email:(NSString *)email policyID:(NSString *)policyID { + NSLog(@"[ReactNativeHybridApp] `signInToOldDot` should never be called in standalone `New Expensify` app"); +} + +- (void)signOutFromOldDot { + NSLog(@"[ReactNativeHybridApp] `signOutFromOldDot` should never be called in standalone `New Expensify` app"); +} + +- (void)clearOldDotAfterSignOut { + NSLog(@"[ReactNativeHybridApp] `clearOldDotAfterSignOut` should never be called in standalone `New Expensify` app"); +} + - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { diff --git a/modules/hybrid-app/src/NativeReactNativeHybridApp.ts b/modules/hybrid-app/src/NativeReactNativeHybridApp.ts index a18f44f87d509..4001adacff494 100644 --- a/modules/hybrid-app/src/NativeReactNativeHybridApp.ts +++ b/modules/hybrid-app/src/NativeReactNativeHybridApp.ts @@ -9,6 +9,9 @@ export interface Spec extends TurboModule { completeOnboarding: (status: boolean) => void; switchAccount: (newDotCurrentAccountEmail: string, authToken: string, policyID: string, accountID: string) => void; sendAuthToken: (authToken: string) => void; + signInToOldDot: (autoGeneratedLogin: string, autoGeneratedPassword: string, authToken: string, email: string, policyID: string) => void; + signOutFromOldDot: () => void; + clearOldDotAfterSignOut: () => void; } export default TurboModuleRegistry.getEnforcing('ReactNativeHybridApp'); diff --git a/modules/hybrid-app/src/index.native.ts b/modules/hybrid-app/src/index.native.ts index 50dfd719449a9..83d342cf922f4 100644 --- a/modules/hybrid-app/src/index.native.ts +++ b/modules/hybrid-app/src/index.native.ts @@ -20,6 +20,15 @@ const HybridAppModule: HybridAppModuleType = { sendAuthToken({authToken}) { ReactNativeHybridApp.sendAuthToken(authToken); }, + signInToOldDot({autoGeneratedLogin, autoGeneratedPassword, authToken, email, policyID}) { + ReactNativeHybridApp.signInToOldDot(autoGeneratedLogin, autoGeneratedPassword, authToken, email, policyID); + }, + signOutFromOldDot() { + ReactNativeHybridApp.signOutFromOldDot(); + }, + clearOldDotAfterSignOut() { + ReactNativeHybridApp.clearOldDotAfterSignOut(); + }, }; export default HybridAppModule; diff --git a/modules/hybrid-app/src/index.ts b/modules/hybrid-app/src/index.ts index ca45aeeb20757..f9c8bb22ec7d5 100644 --- a/modules/hybrid-app/src/index.ts +++ b/modules/hybrid-app/src/index.ts @@ -24,6 +24,18 @@ const HybridAppModule: HybridAppModuleType = { // eslint-disable-next-line no-console console.warn('HybridAppModule: `sendAuthToken` should never be called on web'); }, + signInToOldDot() { + // eslint-disable-next-line no-console + console.warn('HybridAppModule: `signInToOldDot` should never be called on web'); + }, + signOutFromOldDot() { + // eslint-disable-next-line no-console + console.warn('HybridAppModule: `signOutFromOldDot` should never be called on web'); + }, + clearOldDotAfterSignOut() { + // eslint-disable-next-line no-console + console.warn('HybridAppModule: `clearOldDotAfterSignOut` should never be called on web'); + }, }; export default HybridAppModule; diff --git a/modules/hybrid-app/src/types.ts b/modules/hybrid-app/src/types.ts index 8a40a1fc5767b..7d33f31446f06 100644 --- a/modules/hybrid-app/src/types.ts +++ b/modules/hybrid-app/src/types.ts @@ -5,6 +5,9 @@ type HybridAppModuleType = { completeOnboarding: (args: {status: boolean}) => void; switchAccount: (args: {newDotCurrentAccountEmail: string; authToken: string; policyID: string; accountID: string}) => void; sendAuthToken: (args: {authToken: string}) => void; + signInToOldDot: (args: {autoGeneratedLogin: string; autoGeneratedPassword: string; authToken: string; email: string; policyID: string}) => void; + signOutFromOldDot: () => void; + clearOldDotAfterSignOut: () => void; }; export default HybridAppModuleType; diff --git a/src/App.tsx b/src/App.tsx index dc17fe242a550..06f05de7a5018 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'; @@ -81,47 +82,53 @@ function App({url, hybridAppSettings}: 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 4b9943598c95b..46deed015020d 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6722,6 +6722,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 59ec56bc1bcfc..011e8998a43cc 100644 --- a/src/HybridAppHandler.tsx +++ b/src/HybridAppHandler.tsx @@ -1,20 +1,32 @@ import {useContext, useState} from 'react'; +import {useOnyx} from 'react-native-onyx'; import type {AppProps} from './App'; import CONFIG from './CONFIG'; import CONST from './CONST'; -import {signInAfterTransitionFromOldDot} from './libs/actions/Session'; +import {parseHybridAppSettings} from './libs/actions/HybridApp'; +import {setupNewDotAfterTransitionFromOldDot} from './libs/actions/Session'; +import ONYXKEYS from './ONYXKEYS'; import SplashScreenStateContext from './SplashScreenStateContext'; +import isLoadingOnyxValue from './types/utils/isLoadingOnyxValue'; function HybridAppHandler({hybridAppSettings}: AppProps) { const [signInHandled, setSignInHandled] = useState(false); - const {setSplashScreenState} = useContext(SplashScreenStateContext); + const {splashScreenState, setSplashScreenState} = useContext(SplashScreenStateContext); + const [tryNewDot, tryNewDotMetadata] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {canBeMissing: true}); - if (!CONFIG.IS_HYBRID_APP || !hybridAppSettings || signInHandled) { + const isLoading = isLoadingOnyxValue(tryNewDotMetadata); + + if (!CONFIG.IS_HYBRID_APP || !hybridAppSettings || signInHandled || isLoading) { return null; } - signInAfterTransitionFromOldDot(hybridAppSettings).then(() => { - setSplashScreenState(CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN); + 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); }); diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index bce349022a860..fd4b64451f793 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 15927fda953e9..34cedb85eec04 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'; @@ -10,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} from '@libs/actions/Travel'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; @@ -22,7 +22,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 +61,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 +70,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 +128,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')); @@ -172,8 +169,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 9a6d52f968fff..1a8ecb1832b8f 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 91ab2bc2e7983..3e28fdcfb2f2a 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; @@ -41,6 +43,9 @@ function clearStorageAndRedirect(errorMessage?: string): Promise { keysToPreserve.push(ONYXKEYS.ACCOUNT); return Onyx.clear(keysToPreserve).then(() => { + if (CONFIG.IS_HYBRID_APP) { + resetSignInFlow(); + } clearAllPolicies(); if (!errorMessage) { diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 3697bc1b6242b..b2b6f67d246d4 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 cfd009a63f825..7854a74162919 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'; @@ -134,8 +132,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(); /** @@ -573,8 +569,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 11db1b00c5f41..e074f2f802758 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'; @@ -183,6 +185,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(() => { @@ -300,6 +303,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, };