diff --git a/src/App.tsx b/src/App.tsx index 3513cb23953b..70adbfb7efa2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,6 +47,7 @@ import {SplashScreenStateContextProvider} from './SplashScreenStateContext'; type AppProps = { /** URL containing all necessary data to run React Native app (e.g. login data) */ url?: Route; + hybridAppSettings?: string; }; LogBox.ignoreLogs([ @@ -62,14 +63,17 @@ const fill = {flex: 1}; const StrictModeWrapper = CONFIG.USE_REACT_STRICT_MODE_IN_DEV ? React.StrictMode : ({children}: {children: React.ReactElement}) => children; -function App({url}: AppProps) { +function App({url, hybridAppSettings}: AppProps) { useDefaultDragAndDrop(); OnyxUpdateManager(); return ( - + { if (!account?.needsTwoFactorAuthSetup || account.requiresTwoFactorAuth) { @@ -115,10 +116,14 @@ function Expensify() { const isAuthenticated = useMemo(() => !!(session?.authToken ?? null), [session]); const autoAuthState = useMemo(() => session?.autoAuthState ?? '', [session]); - const shouldInit = isNavigationReady && hasAttemptedToOpenPublicRoom; + const shouldInit = NativeModules.HybridAppModule + ? !hybridApp?.loggedOutFromOldDot && isNavigationReady && hasAttemptedToOpenPublicRoom + : isNavigationReady && hasAttemptedToOpenPublicRoom; const shouldHideSplash = shouldInit && - (NativeModules.HybridAppModule ? splashScreenState === CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN && isAuthenticated : splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE); + (NativeModules.HybridAppModule + ? 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/ONYXKEYS.ts b/src/ONYXKEYS.ts index 1fb84c3dd9cf..2b25f458f5fa 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -452,9 +452,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', @@ -476,6 +473,8 @@ const ONYXKEYS = { /** Information about travel provisioning process */ TRAVEL_PROVISIONING: 'travelProvisioning', + HYBRID_APP: 'hybridApp', + /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -1064,7 +1063,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; @@ -1074,6 +1072,7 @@ type OnyxValuesMapping = { [ONYXKEYS.CORPAY_ONBOARDING_FIELDS]: OnyxTypes.CorpayOnboardingFields; [ONYXKEYS.LAST_FULL_RECONNECT_TIME]: string; [ONYXKEYS.TRAVEL_PROVISIONING]: OnyxTypes.TravelProvisioning; + [ONYXKEYS.HYBRID_APP]: OnyxTypes.HybridApp; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/components/BookTravelButton.tsx b/src/components/BookTravelButton.tsx index 1953acde8ad4..68cb96eea165 100644 --- a/src/components/BookTravelButton.tsx +++ b/src/components/BookTravelButton.tsx @@ -1,5 +1,5 @@ import {Str} from 'expensify-common'; -import React, {useCallback, useContext, useState} from 'react'; +import React, {useCallback, useState} from 'react'; import {NativeModules} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; @@ -10,12 +10,12 @@ import {cleanupTravelProvisioningSession} from '@libs/actions/Travel'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import {getAdminsPrivateEmailDomains} from '@libs/PolicyUtils'; +import {setIsRootStatusBarEnabled} from '@userActions/HybridApp'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import Button from './Button'; -import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import DotIndicatorMessage from './DotIndicatorMessage'; type BookTravelButtonProps = { @@ -37,11 +37,10 @@ function BookTravelButton({text}: BookTravelButtonProps) { const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const primaryLogin = account?.primaryLogin; - const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); // 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); + const [hybridApp] = useOnyx(ONYXKEYS.HYBRID_APP); const bookATrip = useCallback(() => { setErrorMessage(''); @@ -63,15 +62,15 @@ function BookTravelButton({text}: BookTravelButtonProps) { 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 (!NativeModules.HybridAppModule || !wasNewDotLaunchedJustForTravel) { + // The isSingleNewDotEntry flag indicates if NewDot was launched solely for this purpose. + if (!NativeModules.HybridAppModule || !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'); - NativeModules.HybridAppModule.closeReactNativeApp(false, false); - setRootStatusBarEnabled(false); + NativeModules.HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false}); + setIsRootStatusBarEnabled(false); }) ?.catch(() => { setErrorMessage(translate('travel.errorMessage')); @@ -92,7 +91,7 @@ function BookTravelButton({text}: BookTravelButtonProps) { Navigation.navigate(ROUTES.TRAVEL_DOMAIN_SELECTOR); } } - }, [policy, wasNewDotLaunchedJustForTravel, travelSettings, translate, primaryLogin, setRootStatusBarEnabled]); + }, [primaryLogin, policy, travelSettings?.hasAcceptedTerms, translate, hybridApp?.isSingleNewDotEntry]); return ( <> diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx index ac1fc77dff96..2e92019d2f61 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); - const isDisabled = !isNested && !isRootStatusBarEnabled; + const isDisabled = (!hybridApp?.isRootStatusBarEnabled ?? false) || (!isNested && !isRootStatusBarEnabled); // Disable the root status bar when a nested status bar is rendered useEffect(() => { diff --git a/src/components/InitialURLContextProvider.tsx b/src/components/InitialURLContextProvider.tsx index c84379af36d8..c61a3c42edb6 100644 --- a/src/components/InitialURLContextProvider.tsx +++ b/src/components/InitialURLContextProvider.tsx @@ -1,14 +1,16 @@ -import React, {createContext, useEffect, useMemo, useState} from 'react'; +import React, {createContext, useEffect, useMemo, useRef, useState} from 'react'; import type {ReactNode} from 'react'; import {Linking} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import {signInAfterTransitionFromOldDot} from '@libs/actions/Session'; +import {setupNewDotAfterTransitionFromOldDot} from '@libs/actions/Session'; import Navigation from '@navigation/Navigation'; +import {setIsRootStatusBarEnabled} from '@userActions/HybridApp'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import {useSplashScreenStateContext} from '@src/SplashScreenStateContext'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; type InitialUrlContextType = { initialURL: Route | undefined; @@ -25,14 +27,19 @@ type InitialURLContextProviderProps = { /** URL passed to our top-level React Native component by HybridApp. Will always be undefined in "pure" NewDot builds. */ url?: Route | ValueOf; + hybridAppSettings?: string; + /** Children passed to the context provider */ children: ReactNode; }; -function InitialURLContextProvider({children, url}: InitialURLContextProviderProps) { - const [initialURL, setInitialURL] = useState(); +function InitialURLContextProvider({children, url, hybridAppSettings}: InitialURLContextProviderProps) { + const [initialURL, setInitialURL] = useState | undefined>(url); const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH); const {splashScreenState, setSplashScreenState} = useSplashScreenStateContext(); + const [tryNewDot, tryNewDotMetadata] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT); + // We use `setupCalled` ref to guarantee that `signInAfterTransitionFromOldDot` is called once. + const setupCalled = useRef(false); useEffect(() => { if (url !== CONST.HYBRID_APP.REORDERING_REACT_NATIVE_ACTIVITY_TO_FRONT) { @@ -50,22 +57,26 @@ function InitialURLContextProvider({children, url}: InitialURLContextProviderPro return; } - if (url) { - signInAfterTransitionFromOldDot(url).then((route) => { - setInitialURL(route); - setSplashScreenState(CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN); - }); + setIsRootStatusBarEnabled(true); + if (url && hybridAppSettings) { + if (!isLoadingOnyxValue(tryNewDotMetadata) && !setupCalled.current) { + setupCalled.current = true; + setupNewDotAfterTransitionFromOldDot(url, hybridAppSettings, tryNewDot).then((route) => { + setInitialURL(route); + setSplashScreenState(CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN); + }); + } return; } Linking.getInitialURL().then((initURL) => { setInitialURL(initURL as Route); }); - }, [setSplashScreenState, url]); + }, [hybridAppSettings, setSplashScreenState, tryNewDot, tryNewDotMetadata, url]); const initialUrlContext = useMemo( () => ({ - initialURL, - setInitialURL, + initialURL: initialURL === CONST.HYBRID_APP.REORDERING_REACT_NATIVE_ACTIVITY_TO_FRONT ? undefined : initialURL, + setInitialURL: setInitialURL as React.Dispatch>, }), [initialURL], ); diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 638ef0737ed5..ee57ab66c41b 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -16,10 +16,10 @@ import {isMobile, isMobileWebKit, isSafari} from '@libs/Browser'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {AuthScreensParamList, RootStackParamList} from '@libs/Navigation/types'; import addViewportResizeListener from '@libs/VisualViewport'; +import {setIsRootStatusBarEnabled} from '@userActions/HybridApp'; import toggleTestToolsModal from '@userActions/TestTool'; import CONST from '@src/CONST'; import CustomDevMenu from './CustomDevMenu'; -import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import FocusTrapForScreens from './FocusTrap/FocusTrapForScreen'; import type FocusTrapForScreenProps from './FocusTrap/FocusTrapForScreen/FocusTrapProps'; import HeaderGap from './HeaderGap'; @@ -153,7 +153,6 @@ function ScreenWrapper( const {windowHeight} = useWindowDimensions(shouldUseCachedViewportHeight); // since Modals are drawn in separate native view hierarchy we should always add paddings const ignoreInsetsConsumption = !useContext(ModalContext).default; - const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout for a case where we want to show the offline indicator only on small screens // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth @@ -172,8 +171,8 @@ function ScreenWrapper( const {isBlurred, setIsBlurred} = useInputBlurContext(); UNSTABLE_usePreventRemove(shouldReturnToOldDot, () => { - NativeModules.HybridAppModule?.closeReactNativeApp(false, false); - setRootStatusBarEnabled(false); + NativeModules.HybridAppModule?.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false}); + setIsRootStatusBarEnabled(false); }); const panResponder = useRef( diff --git a/src/hooks/useOnboardingFlow.ts b/src/hooks/useOnboardingFlow.ts index a626606bf3e0..68681e3cdc57 100644 --- a/src/hooks/useOnboardingFlow.ts +++ b/src/hooks/useOnboardingFlow.ts @@ -29,7 +29,7 @@ function useOnboardingFlowRouter() { const isPrivateDomain = Session.isUserOnPrivateDomain(); - const [isSingleNewDotEntry, isSingleNewDotEntryMetadata] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY); + const [isSingleNewDotEntry, isSingleNewDotEntryMetadata] = useOnyx(ONYXKEYS.HYBRID_APP, {selector: (data) => data?.isSingleNewDotEntry}); useEffect(() => { // This should delay opening the onboarding modal so it does not interfere with the ongoing ReportScreen params changes InteractionManager.runAfterInteractions(() => { diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index ec9da6201db5..970a3f067371 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -93,7 +93,6 @@ const WRITE_COMMANDS = { REQUEST_NEW_VALIDATE_CODE: 'RequestNewValidateCode', SIGN_IN_WITH_APPLE: 'SignInWithApple', SIGN_IN_WITH_GOOGLE: 'SignInWithGoogle', - SIGN_IN_USER: 'SigninUser', SIGN_IN_USER_WITH_LINK: 'SigninUserWithLink', SEARCH: 'Search', REQUEST_UNLINK_VALIDATION_LINK: 'RequestUnlinkValidationLink', @@ -312,7 +311,6 @@ const WRITE_COMMANDS = { TRANSACTION_MERGE: 'Transaction_Merge', RESOLVE_DUPLICATES: 'ResolveDuplicates', UPDATE_SUBSCRIPTION_TYPE: 'UpdateSubscriptionType', - SIGN_UP_USER: 'SignUpUser', UPDATE_SUBSCRIPTION_AUTO_RENEW: 'UpdateSubscriptionAutoRenew', UPDATE_SUBSCRIPTION_ADD_NEW_USERS_AUTOMATICALLY: 'UpdateSubscriptionAddNewUsersAutomatically', UPDATE_SUBSCRIPTION_SIZE: 'UpdateSubscriptionSize', @@ -537,7 +535,6 @@ type WriteCommandParameters = { [WRITE_COMMANDS.REQUEST_NEW_VALIDATE_CODE]: Parameters.RequestNewValidateCodeParams; [WRITE_COMMANDS.SIGN_IN_WITH_APPLE]: Parameters.BeginAppleSignInParams; [WRITE_COMMANDS.SIGN_IN_WITH_GOOGLE]: Parameters.BeginGoogleSignInParams; - [WRITE_COMMANDS.SIGN_IN_USER]: SignInUserParams; [WRITE_COMMANDS.SIGN_IN_USER_WITH_LINK]: Parameters.SignInUserWithLinkParams; [WRITE_COMMANDS.REQUEST_UNLINK_VALIDATION_LINK]: Parameters.RequestUnlinkValidationLinkParams; [WRITE_COMMANDS.UNLINK_LOGIN]: Parameters.UnlinkLoginParams; @@ -781,7 +778,6 @@ type WriteCommandParameters = { [WRITE_COMMANDS.TRANSACTION_MERGE]: Parameters.TransactionMergeParams; [WRITE_COMMANDS.RESOLVE_DUPLICATES]: Parameters.ResolveDuplicatesParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_TYPE]: Parameters.UpdateSubscriptionTypeParams; - [WRITE_COMMANDS.SIGN_UP_USER]: Parameters.SignUpUserParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_AUTO_RENEW]: Parameters.UpdateSubscriptionAutoRenewParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_ADD_NEW_USERS_AUTOMATICALLY]: Parameters.UpdateSubscriptionAddNewUsersAutomaticallyParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_SIZE]: Parameters.UpdateSubscriptionSizeParams; @@ -1081,6 +1077,8 @@ const SIDE_EFFECT_REQUEST_COMMANDS = { COMPLETE_HYBRID_APP_ONBOARDING: 'CompleteHybridAppOnboarding', CONNECT_POLICY_TO_QUICKBOOKS_DESKTOP: 'ConnectPolicyToQuickbooksDesktop', BANK_ACCOUNT_CREATE_CORPAY: 'BankAccount_CreateCorpay', + SIGN_IN_USER: 'SigninUser', + SIGN_UP_USER: 'SignUpUser', // PayMoneyRequestOnSearch only works online (pattern C) and we need to play the success sound only when the request is successful PAY_MONEY_REQUEST_ON_SEARCH: 'PayMoneyRequestOnSearch', @@ -1104,6 +1102,8 @@ type SideEffectRequestCommandParameters = { [SIDE_EFFECT_REQUEST_COMMANDS.COMPLETE_HYBRID_APP_ONBOARDING]: EmptyObject; [SIDE_EFFECT_REQUEST_COMMANDS.CONNECT_POLICY_TO_QUICKBOOKS_DESKTOP]: Parameters.ConnectPolicyToQuickBooksDesktopParams; [SIDE_EFFECT_REQUEST_COMMANDS.BANK_ACCOUNT_CREATE_CORPAY]: Parameters.BankAccountCreateCorpayParams; + [SIDE_EFFECT_REQUEST_COMMANDS.SIGN_IN_USER]: SignInUserParams; + [SIDE_EFFECT_REQUEST_COMMANDS.SIGN_UP_USER]: Parameters.SignUpUserParams; [SIDE_EFFECT_REQUEST_COMMANDS.PAY_MONEY_REQUEST_ON_SEARCH]: Parameters.PayMoneyRequestOnSearchParams; }; diff --git a/src/libs/HybridApp.ts b/src/libs/HybridApp.ts new file mode 100644 index 000000000000..177762ba34b8 --- /dev/null +++ b/src/libs/HybridApp.ts @@ -0,0 +1,96 @@ +import {NativeModules} from 'react-native'; +import Onyx from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Credentials, HybridApp, Session, TryNewDot} from '@src/types/onyx'; +import * as HybridAppActions from './actions/HybridApp'; +import {setIsRootStatusBarEnabled} from './actions/HybridApp'; +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_TRYNEWDOT, + 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) { + HybridAppActions.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) { + return tryNewDot?.classicRedirect.dismissed === true; +} + +function handleChangeInHybridAppSignInFlow(hybridApp: OnyxEntry, tryNewDot: OnyxEntry, credentials: OnyxEntry) { + if (!NativeModules.HybridAppModule) { + 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) { + Log.info(`[HybridApp] Performing sign-in${shouldUseOldApp(tryNewDot) ? '' : ' (in background)'} on OldDot side`); + NativeModules.HybridAppModule.signInToOldDot({ + autoGeneratedLogin: credentials.autoGeneratedLogin, + autoGeneratedPassword: credentials.autoGeneratedPassword, + authToken: currentSession?.authToken ?? '', + email: getCurrentUserEmail() ?? '', + policyID: activePolicyID ?? '', + }); + HybridAppActions.setUseNewDotSignInPage(false).then(() => { + if (shouldUseOldApp(tryNewDot)) { + NativeModules.HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false}); + setIsRootStatusBarEnabled(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.'); + HybridAppActions.setReadyToShowAuthScreens(true); + } + }); + } + + currentHybridApp = hybridApp; + currentTryNewDot = tryNewDot; +} + +export default {shouldUseOldApp}; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 9968251e226a..d582615b59a7 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -305,16 +305,13 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie PusherConnectionManager.init(); initializePusher(); - // In Hybrid App we decide to call one of those method when booting ND and we don't want to duplicate calls - if (!NativeModules.HybridAppModule) { - // 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(). - if (SessionUtils.didUserLogInDuringSession()) { - App.openApp(); - } else { - Log.info('[AuthScreens] Sending ReconnectApp'); - App.reconnectApp(initialLastUpdateIDAppliedToClient); - } + // 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(). + if (SessionUtils.didUserLogInDuringSession()) { + App.openApp(); + } else { + Log.info('[AuthScreens] Sending ReconnectApp'); + App.reconnectApp(initialLastUpdateIDAppliedToClient); } PriorityMode.autoSwitchToFocusMode(); diff --git a/src/libs/Navigation/AppNavigator/PublicScreens.tsx b/src/libs/Navigation/AppNavigator/PublicScreens.tsx index 00b9a98e78bd..6ac89d0882f5 100644 --- a/src/libs/Navigation/AppNavigator/PublicScreens.tsx +++ b/src/libs/Navigation/AppNavigator/PublicScreens.tsx @@ -1,9 +1,7 @@ import React from 'react'; -import {NativeModules} from 'react-native'; import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator'; 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'; @@ -23,7 +21,7 @@ function PublicScreens() { {/* The structure for the HOME route has to be the same in public and auth screens. That's why the name for SignInPage is BOTTOM_TAB_NAVIGATOR. */} { + if (!NativeModules.HybridAppModule) { + return authenticated; + } + if (HybridApp.shouldUseOldApp(tryNewDot) && !hybridApp?.isSingleNewDotEntry) { + return false; + } + + return authenticated && (!hybridApp?.useNewDotSignInPage || hybridApp?.readyToShowAuthScreens); + }, [tryNewDot, hybridApp?.isSingleNewDotEntry, hybridApp?.useNewDotSignInPage, hybridApp?.readyToShowAuthScreens, authenticated]); useEffect(() => { - if (!NativeModules.HybridAppModule || !initialURL) { + if (!NativeModules.HybridAppModule || !initialURL || !shouldShowAuthScreens) { return; } @@ -21,9 +38,9 @@ function AppNavigator({authenticated}: AppNavigatorProps) { Navigation.navigate(Navigation.parseHybridAppUrl(initialURL)); setInitialURL(undefined); }); - }, [initialURL, setInitialURL]); + }, [initialURL, setInitialURL, shouldShowAuthScreens]); - if (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/actions/Delegate.ts b/src/libs/actions/Delegate.ts index dd4dfb24a908..f5cdd404dd84 100644 --- a/src/libs/actions/Delegate.ts +++ b/src/libs/actions/Delegate.ts @@ -156,7 +156,9 @@ function connect(email: string) { NetworkStore.setAuthToken(response?.restrictedToken ?? null); confirmReadyToOpenApp(); - openApp().then(() => NativeModules.HybridAppModule?.switchAccount(email, restrictedToken, policyID, String(previousAccountID))); + openApp().then(() => + NativeModules.HybridAppModule?.switchAccount({newDotCurrentAccountEmail: email, authToken: restrictedToken, policyID, accountID: String(previousAccountID)}), + ); }); }) .catch((error) => { @@ -238,7 +240,7 @@ function disconnect() { NetworkStore.setAuthToken(response?.authToken ?? null); confirmReadyToOpenApp(); - openApp().then(() => NativeModules.HybridAppModule?.switchAccount(requesterEmail, authToken, '', '')); + openApp().then(() => NativeModules.HybridAppModule?.switchAccount({newDotCurrentAccountEmail: requesterEmail, authToken, policyID: '', accountID: ''})); }); }) .catch((error) => { diff --git a/src/libs/actions/HybridApp/index.ts b/src/libs/actions/HybridApp/index.ts new file mode 100644 index 000000000000..271b91aa99ee --- /dev/null +++ b/src/libs/actions/HybridApp/index.ts @@ -0,0 +1,67 @@ +import Onyx from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +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) { + Onyx.merge(ONYXKEYS.HYBRID_APP, {readyToShowAuthScreens}); +} + +/* + * Changes NewDot sign-in state + */ +function setNewDotSignInState(newDotSignInState: ValueOf) { + Onyx.merge(ONYXKEYS.HYBRID_APP, {newDotSignInState}); +} + +function setUseNewDotSignInPage(useNewDotSignInPage: boolean) { + return Onyx.merge(ONYXKEYS.HYBRID_APP, {useNewDotSignInPage}); +} + +function setIsRootStatusBarEnabled(isRootStatusBarEnabled: boolean) { + return Onyx.merge(ONYXKEYS.HYBRID_APP, {isRootStatusBarEnabled}); +} + +/* + * Starts HybridApp sign-in flow from the beginning. + */ +function resetSignInFlow() { + 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: 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, setIsRootStatusBarEnabled}; diff --git a/src/libs/actions/HybridApp/types.ts b/src/libs/actions/HybridApp/types.ts new file mode 100644 index 000000000000..dc99313344c8 --- /dev/null +++ b/src/libs/actions/HybridApp/types.ts @@ -0,0 +1,12 @@ +import type ONYXKEYS from '@src/ONYXKEYS'; +import type {TryNewDot} from '@src/types/onyx'; +import type HybridApp from '@src/types/onyx/HybridApp'; + +type HybridAppSettings = { + initialOnyxValues: { + [ONYXKEYS.HYBRID_APP]: HybridApp; + [ONYXKEYS.NVP_TRYNEWDOT]?: TryNewDot; + }; +}; + +export default HybridAppSettings; diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 512019b6db6e..feb72261f7f4 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -39,12 +39,14 @@ import NetworkConnection from '@libs/NetworkConnection'; import * as Pusher from '@libs/Pusher/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 * as PriorityMode from '@userActions/PriorityMode'; import redirectToSignIn from '@userActions/SignInRedirect'; import Timing from '@userActions/Timing'; @@ -55,6 +57,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 Session from '@src/types/onyx/Session'; import type {AutoAuthState} from '@src/types/onyx/Session'; @@ -225,14 +228,13 @@ function isExpiredSession(sessionCreationDate: number): boolean { return new Date().getTime() - sessionCreationDate >= CONST.SESSION_EXPIRATION_TIME_MS; } -function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean, killHybridApp = true) { +function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean, shouldSignOutFromOldDot = true) { Log.info('Redirecting to Sign In because signOut() was called'); hideContextMenu(false); if (!isAnonymousUser()) { // In the HybridApp, we want the Old Dot to handle the sign out process - if (NativeModules.HybridAppModule && killHybridApp) { - NativeModules.HybridAppModule.closeReactNativeApp(true, false); - return; + if (NativeModules.HybridAppModule && shouldSignOutFromOldDot) { + NativeModules.HybridAppModule.signOutFromOldDot(); } // We'll only call signOut if we're not stashing the session and this is not a supportal session, // otherwise we'll call the API to invalidate the autogenerated credentials used for infinite @@ -502,103 +504,84 @@ function signUpUser() { const params: SignUpUserParams = {email: credentials.login, preferredLocale}; - 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), - }); + // eslint-disable-next-line rulesdir/no-api-side-effects-method + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.SIGN_UP_USER, params, {optimisticData, successData, failureData}).then((response) => { + if (!response) { + return; + } + Onyx.merge(ONYXKEYS.NVP_TRYNEWDOT, {classicRedirect: {dismissed: !response.tryNewDot}}); }); } -function signInAfterTransitionFromOldDot(transitionURL: string) { - const [route, queryParams] = transitionURL.split('?'); +function setupNewDotAfterTransitionFromOldDot(route: Route, hybridAppSettings: string, tryNewDot: TryNewDot | undefined) { + const parsedHybridAppSettings = HybridAppActions.parseHybridAppSettings(hybridAppSettings); + const {initialOnyxValues} = parsedHybridAppSettings; + const {hybridApp, ...newDotOnyxValues} = initialOnyxValues; - const { - email, - authToken, - encryptedAuthToken, - accountID, - autoGeneratedLogin, - autoGeneratedPassword, - clearOnyxOnStart, - completedHybridAppOnboarding, - nudgeMigrationTimestamp, - isSingleNewDotEntry, - primaryLogin, - oldDotOriginalAccountEmail, - } = Object.fromEntries( - queryParams.split('&').map((param) => { - const [key, value] = param.split('='); - return [key, value]; - }), - ); + const clearOnyxBeforeSignIn = () => { + if (!hybridApp.useNewDotSignInPage) { + return Promise.resolve(); + } + + return redirectToSignIn(); + }; - const clearOnyxForNewAccount = () => { - if (clearOnyxOnStart !== 'true') { + const resetDidUserLoginDuringSessionIfNeeded = () => { + if (newDotOnyxValues.nvp_tryNewDot === undefined || tryNewDot?.classicRedirect.dismissed !== true) { return Promise.resolve(); } - return Onyx.clear(KEYS_TO_PRESERVE); + Log.info("[HybridApp] OpenApp hasn't been called yet. Calling `resetDidUserLogInDuringSession`"); + resetDidUserLogInDuringSession(); }; - const setSessionDataAndOpenApp = new Promise((resolve) => { - clearOnyxForNewAccount() + return new Promise((resolve) => { + clearOnyxBeforeSignIn() .then(() => { // This section controls copilot changes const currentUserEmail = getCurrentUserEmail(); // If ND and OD account are the same - do nothing - if (email === currentUserEmail) { + 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}, - }); - }) - .then(() => - Onyx.multiSet({ - [ONYXKEYS.SESSION]: {email, authToken, encryptedAuthToken: decodeURIComponent(encryptedAuthToken), accountID: Number(accountID)}, - [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin, autoGeneratedPassword}, - [ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY]: isSingleNewDotEntry === 'true', - [ONYXKEYS.NVP_TRYNEWDOT]: { - classicRedirect: {completedHybridAppOnboarding: completedHybridAppOnboarding === 'true'}, - nudgeMigration: nudgeMigrationTimestamp ? {timestamp: new Date(nudgeMigrationTimestamp)} : undefined, - }, - }).then(() => Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin})), - ) - .then(() => { - if (clearOnyxOnStart === 'true') { - return openApp(); - } - return getLastUpdateIDAppliedToClient().then((lastUpdateId) => { - return reconnectApp(lastUpdateId); - }); + 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(() => HybridAppActions.prepareHybridAppAfterTransitionToNewDot(hybridApp)) + .then(resetDidUserLoginDuringSessionIfNeeded) + .then(() => Onyx.multiSet(newDotOnyxValues)) .catch((error) => { Log.hmmm('[HybridApp] Initialization of HybridApp has failed. Forcing transition', {error}); }) .finally(() => { - resolve(`${route}${isSingleNewDotEntry === 'true' ? '?singleNewDotEntry=true' : ''}` as Route); + resolve(`${route}${hybridApp.isSingleNewDotEntry ? '?singleNewDotEntry=true' : ''}` as Route); }); }); - - return setSessionDataAndOpenApp; } /** @@ -699,7 +682,17 @@ function signIn(validateCode: string, twoFactorAuthCode?: string) { params.validateCode = validateCode || credentials.validateCode; } - API.write(WRITE_COMMANDS.SIGN_IN_USER, params, {optimisticData, successData, failureData}); + // eslint-disable-next-line rulesdir/no-api-side-effects-method + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.SIGN_IN_USER, params, { + optimisticData, + successData, + failureData, + }).then((response) => { + if (!response) { + return; + } + Onyx.merge(ONYXKEYS.NVP_TRYNEWDOT, {classicRedirect: {dismissed: !response.tryNewDot}}); + }); }); } @@ -1332,7 +1325,7 @@ export { isSupportAuthToken, hasStashedSession, signUpUser, - signInAfterTransitionFromOldDot, + setupNewDotAfterTransitionFromOldDot, validateUserAndGetAccessiblePolicies, isUserOnPrivateDomain, resetSMSDeliveryFailureStatus, diff --git a/src/libs/actions/SignInRedirect.ts b/src/libs/actions/SignInRedirect.ts index e20c02f2d465..ee9064ad8fc2 100644 --- a/src/libs/actions/SignInRedirect.ts +++ b/src/libs/actions/SignInRedirect.ts @@ -23,6 +23,7 @@ function clearStorageAndRedirect(errorMessage?: string): Promise { keysToPreserve.push(ONYXKEYS.PREFERRED_THEME); keysToPreserve.push(ONYXKEYS.ACTIVE_CLIENTS); keysToPreserve.push(ONYXKEYS.DEVICE_ID); + keysToPreserve.push(ONYXKEYS.HYBRID_APP); // After signing out, set ourselves as offline if we were offline before logging out and we are not forcing it. // If we are forcing offline, ignore it while signed out, otherwise it would require a refresh because there's no way to toggle the switch to go back online while signed out. diff --git a/src/libs/actions/Welcome/index.ts b/src/libs/actions/Welcome/index.ts index 585f5a59937c..654a600fad2c 100644 --- a/src/libs/actions/Welcome/index.ts +++ b/src/libs/actions/Welcome/index.ts @@ -145,7 +145,7 @@ function completeHybridAppOnboarding() { // No matter what the response is, we want to mark the onboarding as completed (user saw the explanation modal) Log.info(`[HybridApp] Onboarding status has changed. Propagating new value to OldDot`, true); - NativeModules.HybridAppModule.completeOnboarding(true); + NativeModules.HybridAppModule.completeOnboarding({status: true}); }); } diff --git a/src/pages/ErrorPage/SessionExpiredPage.tsx b/src/pages/ErrorPage/SessionExpiredPage.tsx index 5ccf70c40ab6..906c32dbb102 100644 --- a/src/pages/ErrorPage/SessionExpiredPage.tsx +++ b/src/pages/ErrorPage/SessionExpiredPage.tsx @@ -37,7 +37,7 @@ function SessionExpiredPage() { Navigation.goBack(); return; } - NativeModules.HybridAppModule.closeReactNativeApp(true, false); + NativeModules.HybridAppModule.closeReactNativeApp({shouldSignOut: true, shouldSetNVP: false}); }} > {translate('deeplinkWrapper.signIn')} diff --git a/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx b/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx index 1a4b9bd79e61..6a5f9ccf4c57 100644 --- a/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx +++ b/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx @@ -1,8 +1,7 @@ -import React, {useContext, useMemo, useState} from 'react'; +import React, {useMemo, useState} from 'react'; import {NativeModules} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; -import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -15,6 +14,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import {isPaidGroupPolicy} from '@libs/PolicyUtils'; +import {setIsRootStatusBarEnabled} from '@userActions/HybridApp'; import {createWorkspace, generatePolicyID} from '@userActions/Policy/Policy'; import {completeOnboarding} from '@userActions/Report'; import {setOnboardingAdminsChatReportID, setOnboardingCompanySize, setOnboardingPolicyID} from '@userActions/Welcome'; @@ -35,7 +35,6 @@ function BaseOnboardingEmployees({shouldUseNativeStyles, route}: BaseOnboardingE const [onboardingPolicyID] = useOnyx(ONYXKEYS.ONBOARDING_POLICY_ID); const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); - const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); const paidGroupPolicy = Object.values(allPolicies ?? {}).find(isPaidGroupPolicy); @@ -107,8 +106,8 @@ function BaseOnboardingEmployees({shouldUseNativeStyles, route}: BaseOnboardingE ); } - NativeModules.HybridAppModule.closeReactNativeApp(false, true); - setRootStatusBarEnabled(false); + NativeModules.HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: true}); + setIsRootStatusBarEnabled(false); }} pressOnEnter /> diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index e5b269a7be6e..de259a5734fb 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -8,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 DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -39,6 +38,8 @@ import {hasGlobalWorkspaceSettingsRBR} from '@libs/WorkspacesSettingsUtils'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import variables from '@styles/variables'; import {confirmReadyToOpenApp} from '@userActions/App'; +import * as HybridAppActions from '@userActions/HybridApp'; +import {setIsRootStatusBarEnabled} from '@userActions/HybridApp'; import {buildOldDotURL, openExternalLink, openOldDotLink} from '@userActions/Link'; import {hasPaymentMethodError} from '@userActions/PaymentMethods'; import {isSupportAuthToken, signOutAndRedirectToSignIn} from '@userActions/Session'; @@ -101,7 +102,6 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr const emojiCode = currentUserPersonalDetails?.status?.emojiCode ?? ''; const [allConnectionSyncProgresses] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}`); const {setInitialURL} = useContext(InitialURLContext); - const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION); const subscriptionPlan = useSubscriptionPlan(); @@ -243,9 +243,9 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr ...(NativeModules.HybridAppModule ? { action: () => { - NativeModules.HybridAppModule.closeReactNativeApp(false, true); + NativeModules.HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: true}); setInitialURL(undefined); - setRootStatusBarEnabled(false); + setIsRootStatusBarEnabled(false); }, } : { @@ -284,11 +284,12 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr icon: Expensicons.Exit, action: () => { signOut(false); + HybridAppActions.resetSignInFlow(); }, }, ], }; - }, [styles.pt4, setInitialURL, setRootStatusBarEnabled, isActingAsDelegate, shouldOpenBookACall, signOut]); + }, [styles.pt4, setInitialURL, isActingAsDelegate, shouldOpenBookACall, signOut]); /** * Retuns JSX.Element with menu items diff --git a/src/pages/signin/SignUpWelcomeForm.tsx b/src/pages/signin/SignUpWelcomeForm.tsx index 1f8687c218b7..4552b8e05105 100644 --- a/src/pages/signin/SignUpWelcomeForm.tsx +++ b/src/pages/signin/SignUpWelcomeForm.tsx @@ -7,7 +7,9 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; +import {setNewDotSignInState, setReadyToShowAuthScreens} from '@userActions/HybridApp'; import * as Session from '@userActions/Session'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ChangeExpensifyLoginLink from './ChangeExpensifyLoginLink'; import Terms from './Terms'; @@ -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); + Session.signUpUser(); + setReadyToShowAuthScreens(true); + }} pressOnEnter style={[styles.mb2]} /> diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx index f73c0a1602fb..64b4d83284a6 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import type {ForwardedRef} from 'react'; -import {View} from 'react-native'; +import {NativeModules, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import SafariFormWrapper from '@components/Form/SafariFormWrapper'; @@ -24,6 +24,7 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; import ChangeExpensifyLoginLink from '@pages/signin/ChangeExpensifyLoginLink'; import Terms from '@pages/signin/Terms'; +import * as HybridAppActions from '@userActions/HybridApp'; import * as SessionActions from '@userActions/Session'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; @@ -153,6 +154,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 (NativeModules.HybridAppModule) { + HybridAppActions.resetSignInFlow(); + } + User.resendValidateCode(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,6 +237,10 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco * Check that all the form fields are valid, then trigger the submit callback */ const validateAndSubmitForm = useCallback(() => { + if (NativeModules.HybridAppModule && session?.authToken) { + HybridAppActions.resetSignInFlow(); + } + if (account?.isLoading) { return; } @@ -282,13 +291,14 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco const recoveryCodeOr2faCode = isUsingRecoveryCode ? recoveryCode : twoFactorAuthCode; + HybridAppActions.setNewDotSignInState(CONST.HYBRID_APP_SIGN_IN_STATE.STARTED); const accountID = credentials?.accountID; if (accountID) { SessionActions.signInWithValidateCode(accountID, validateCode, recoveryCodeOr2faCode); } else { SessionActions.signIn(validateCode, recoveryCodeOr2faCode); } - }, [account, credentials, twoFactorAuthCode, validateCode, isUsingRecoveryCode, recoveryCode]); + }, [session?.authToken, account?.isLoading, account?.errors, account?.requiresTwoFactorAuth, isUsingRecoveryCode, recoveryCode, twoFactorAuthCode, credentials?.accountID, validateCode]); return ( diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts index ae63bf77b2a0..ed1bd3d0f0f0 100644 --- a/src/types/modules/react-native.d.ts +++ b/src/types/modules/react-native.d.ts @@ -6,9 +6,11 @@ import type {ShortcutManagerModule} from '@libs/ShortcutManager'; import type StartupTimer from '@libs/StartupTimer/types'; type HybridAppModule = { - closeReactNativeApp: (shouldSignOut: boolean, shouldSetNVP: boolean) => void; - completeOnboarding: (status: boolean) => void; - switchAccount: (newDotCurrentAccountEmail: string, authToken: string, policyID: string, accountID: string) => void; + closeReactNativeApp: ({shouldSignOut: boolean, shouldSetNVP: boolean}) => void; + completeOnboarding: ({status: boolean}) => void; + switchAccount: ({newDotCurrentAccountEmail: string, authToken: string, policyID: string, accountID: string}) => void; + signInToOldDot: ({autoGeneratedLogin: string, autoGeneratedPassword: string, authToken: string, email: string, policyID: string}) => void; + signOutFromOldDot: () => void; }; type RNTextInputResetModule = { diff --git a/src/types/onyx/HybridApp.ts b/src/types/onyx/HybridApp.ts new file mode 100644 index 000000000000..816d9227bfaf --- /dev/null +++ b/src/types/onyx/HybridApp.ts @@ -0,0 +1,49 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +/** */ +type HybridAppDelegateAccessData = { + /** */ + isDelegateAccess?: boolean; + + /** */ + oldDotCurrentUserEmail?: string; + + /** */ + oldDotCurrentAuthToken?: string; + + /** */ + oldDotCurrentEncryptedAuthToken?: string; + + /** */ + oldDotCurrentAccountID?: number; +}; + +/** */ +type HybridApp = { + /** Stores the information if HybridApp uses NewDot's sign-in flow */ + useNewDotSignInPage?: boolean; + + /** Tells if we can show AuthScreens */ + readyToShowAuthScreens?: boolean; + + /** States whether we transitioned from OldDot to show only certain group of screens */ + isSingleNewDotEntry?: boolean; + + /** Stores information if last log out was performed from OldDot */ + loggedOutFromOldDot?: boolean; + + /** */ + shouldRemoveDelegatedAccess?: boolean; + + /** Describes current stage of NewDot sign-in */ + newDotSignInState?: ValueOf; + + /** */ + delegateAccessData?: HybridAppDelegateAccessData; + + /** */ + isRootStatusBarEnabled?: boolean; +}; + +export default HybridApp; diff --git a/src/types/onyx/Response.ts b/src/types/onyx/Response.ts index 146cf327dd31..9eed7f8cb137 100644 --- a/src/types/onyx/Response.ts +++ b/src/types/onyx/Response.ts @@ -89,6 +89,9 @@ type Response = { /** The ID of the original user (returned when in delegate mode) */ requesterID?: number; + + /** [HybridApp] Determines which app should be opened, NewDot or OldDot */ + tryNewDot?: boolean; }; export default Response; diff --git a/src/types/onyx/TryNewDot.ts b/src/types/onyx/TryNewDot.ts index 427a5486aeb5..826f266b6437 100644 --- a/src/types/onyx/TryNewDot.ts +++ b/src/types/onyx/TryNewDot.ts @@ -9,7 +9,7 @@ type TryNewDot = { /** * Indicates if transistion from OldDot to NewDot should happen in HybridApp. */ - dismissed: boolean | string; + dismissed: boolean; /** * Indicates timestamp of an action. */ diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 569504437fb2..ee808ec34c76 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -29,6 +29,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'; @@ -250,4 +251,5 @@ export type { JoinablePolicies, DismissedProductTraining, TravelProvisioning, + HybridApp, };