Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,14 @@ module.exports = {
property: 'isHybridApp',
message: 'Use CONFIG.IS_HYBRID_APP instead.',
},
// Prevent direct use of HybridAppModule.closeReactNativeApp().
// Instead, use the `closeReactNativeApp` action from `@userActions/HybridApp`,
// which correctly updates `hybridApp.closingReactNativeApp` when closing NewDot
{
object: 'HybridAppModule',
property: 'closeReactNativeApp',
message: 'Use `closeReactNativeApp` from `@userActions/HybridApp` instead.',
},
],
'no-restricted-imports': [
'error',
Expand Down
8 changes: 4 additions & 4 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',

Expand Down Expand Up @@ -851,6 +848,9 @@ const ONYXKEYS = {
REPORT_ATTRIBUTES: 'reportAttributes',
REPORT_TRANSACTIONS_AND_VIOLATIONS: 'reportTransactionsAndViolations',
},

/** Stores HybridApp specific state required to interoperate with OldDot */
HYBRID_APP: 'hybridApp',
} as const;

type AllOnyxKeys = DeepValueOf<typeof ONYXKEYS>;
Expand Down Expand Up @@ -1173,7 +1173,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<string, string>;
[ONYXKEYS.CONCIERGE_REPORT_ID]: string;
Expand All @@ -1199,6 +1198,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_LAST_IPHONE_LOGIN]: string;
[ONYXKEYS.NVP_LAST_ANDROID_LOGIN]: string;
[ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_REPORT_IDS]: string[];
[ONYXKEYS.HYBRID_APP]: OnyxTypes.HybridApp;
};

type OnyxDerivedValuesMapping = {
Expand Down
18 changes: 7 additions & 11 deletions src/components/BookTravelButton.tsx
Original file line number Diff line number Diff line change
@@ -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 useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
Expand All @@ -15,14 +14,14 @@ import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import {getActivePolicies, getAdminsPrivateEmailDomains, isPaidGroupPolicy} from '@libs/PolicyUtils';
import colors from '@styles/theme/colors';
import closeReactNativeApp from '@userActions/HybridApp';
import CONFIG from '@src/CONFIG';
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 ConfirmModal from './ConfirmModal';
import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext';
import DotIndicatorMessage from './DotIndicatorMessage';
import {RocketDude} from './Icon/Illustrations';
import Text from './Text';
Expand Down Expand Up @@ -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);
Expand All @@ -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 [wasNewDotLaunchedJustForTravel] = useOnyx(ONYXKEYS.HYBRID_APP, {selector: (hybridApp) => hybridApp?.isSingleNewDotEntry, canBeMissing: false});

const hidePreventionModal = () => setPreventionModalVisibility(false);
const hideVerificationModal = () => setVerificationModalVisibility(false);
Expand Down Expand Up @@ -137,8 +135,7 @@ function BookTravelButton({text, shouldRenderErrorMessageBelowButton = false, se

// 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'));
Expand Down Expand Up @@ -170,17 +167,16 @@ function BookTravelButton({text, shouldRenderErrorMessageBelowButton = false, se
isBlockedFromSpotnanaTravel,
primaryContactMethod,
policy,
groupPaidPolicies.length,
travelSettings?.hasAcceptedTerms,
travelSettings?.lastTravelSignupRequestTime,
isBetaEnabled,
styles.flexRow,
styles.link,
StyleUtils,
translate,
wasNewDotLaunchedJustForTravel,
setRootStatusBarEnabled,
isUserValidated,
groupPaidPolicies.length,
isBetaEnabled,
travelSettings?.lastTravelSignupRequestTime,
]);

return (
Expand Down
7 changes: 6 additions & 1 deletion src/components/CustomStatusBarAndBackground/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, {useCallback, useContext, useEffect, useRef, useState} from 'react';
import {interpolateColor, runOnJS, useAnimatedReaction, useSharedValue, withDelay, withTiming} from 'react-native-reanimated';
import useOnyx from '@hooks/useOnyx';
import usePrevious from '@hooks/usePrevious';
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 CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackgroundContext';
import updateGlobalBackgroundColor from './updateGlobalBackgroundColor';
import updateStatusBarAppearance from './updateStatusBarAppearance';
Expand All @@ -19,8 +21,11 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack
const {isRootStatusBarEnabled, setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext);
const theme = useTheme();
const [statusBarStyle, setStatusBarStyle] = useState<StatusBarStyle>();
const [closingReactNativeApp = false] = useOnyx(ONYXKEYS.HYBRID_APP, {selector: (hybridApp) => hybridApp?.closingReactNativeApp, canBeMissing: true});

const isDisabled = !isNested && !isRootStatusBarEnabled;
// Include `closingReactNativeApp` to disable the StatusBar when switching from HybridApp to OldDot,
// preventing unexpected status bar blinking during the transition
const isDisabled = (!isNested && !isRootStatusBarEnabled) || closingReactNativeApp;

// Disable the root status bar when a nested status bar is rendered
useEffect(() => {
Expand Down
11 changes: 4 additions & 7 deletions src/components/ScreenWrapper/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
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';
import type {StyleProp, View, ViewStyle} from 'react-native';
import {Keyboard} from 'react-native';
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';
Expand All @@ -22,6 +20,7 @@ import NarrowPaneContext from '@libs/Navigation/AppNavigator/Navigators/NarrowPa
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
import type {ReportsSplitNavigatorParamList, RootNavigatorParamList} from '@libs/Navigation/types';
import closeReactNativeApp from '@userActions/HybridApp';
import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -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 [isSingleNewDotEntry = false] = useOnyx(ONYXKEYS.HYBRID_APP, {selector: (hybridApp) => hybridApp?.isSingleNewDotEntry, canBeMissing: true});

usePreventRemove((isSingleNewDotEntry ?? false) && initialURL === Navigation.getActiveRouteWithoutParams(), () => {
usePreventRemove(isSingleNewDotEntry && initialURL === Navigation.getActiveRouteWithoutParams(), () => {
if (!CONFIG.IS_HYBRID_APP) {
return;
}
HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false});
setRootStatusBarEnabled(false);
closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false});
});

useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useOnboardingFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: (hybridApp) => hybridApp?.isSingleNewDotEntry, canBeMissing: true});

useEffect(() => {
// This should delay opening the onboarding modal so it does not interfere with the ongoing ReportScreen params changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
});

Expand Down
14 changes: 14 additions & 0 deletions src/libs/actions/HybridApp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import HybridAppModule from '@expensify/react-native-hybrid-app';
import Onyx from 'react-native-onyx';
import CONFIG from '@src/CONFIG';
import ONYXKEYS from '@src/ONYXKEYS';

function closeReactNativeApp({shouldSignOut, shouldSetNVP}: {shouldSignOut: boolean; shouldSetNVP: boolean}) {
if (CONFIG.IS_HYBRID_APP) {
Onyx.merge(ONYXKEYS.HYBRID_APP, {closingReactNativeApp: true});
}
// eslint-disable-next-line no-restricted-properties
HybridAppModule.closeReactNativeApp({shouldSignOut, shouldSetNVP});
}

export default closeReactNativeApp;
8 changes: 5 additions & 3 deletions src/libs/actions/Session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {hideContextMenu} from '@pages/home/report/ContextMenu/ReportActionContex
import {KEYS_TO_PRESERVE, openApp, reconnectApp} from '@userActions/App';
import {KEYS_TO_PRESERVE_DELEGATE_ACCESS} from '@userActions/Delegate';
import * as Device from '@userActions/Device';
import closeReactNativeApp from '@userActions/HybridApp';
import redirectToSignIn from '@userActions/SignInRedirect';
import Timing from '@userActions/Timing';
import * as Welcome from '@userActions/Welcome';
Expand Down Expand Up @@ -265,7 +266,7 @@ function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSess

// 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});
closeReactNativeApp({shouldSignOut: true, shouldSetNVP: false});
return;
}

Expand Down Expand Up @@ -616,13 +617,14 @@ function signInAfterTransitionFromOldDot(hybridAppSettings: string) {
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})),
})
.then(() => Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin, requiresTwoFactorAuth, needsTwoFactorAuthSetup}))
.then(() => Onyx.merge(ONYXKEYS.HYBRID_APP, {isSingleNewDotEntry, closingReactNativeApp: false})),
)
.then(() => {
if (clearOnyxOnStart) {
Expand Down
4 changes: 2 additions & 2 deletions src/pages/ErrorPage/SessionExpiredPage.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,6 +9,7 @@ import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import closeReactNativeApp from '@userActions/HybridApp';
import {clearSignInData} from '@userActions/Session';
import CONFIG from '@src/CONFIG';

Expand Down Expand Up @@ -39,7 +39,7 @@ function SessionExpiredPage() {
Navigation.goBack();
return;
}
HybridAppModule.closeReactNativeApp({shouldSignOut: true, shouldSetNVP: false});
closeReactNativeApp({shouldSignOut: true, shouldSetNVP: false});
}}
>
{translate('deeplinkWrapper.signIn')}
Expand Down
11 changes: 4 additions & 7 deletions src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
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 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';
Expand All @@ -25,6 +23,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import closeReactNativeApp from '@libs/actions/HybridApp';
import {openOldDotLink} from '@libs/actions/Link';
import {createWorkspace, generatePolicyID} from '@libs/actions/Policy/Policy';
import {completeOnboarding} from '@libs/actions/Report';
Expand Down Expand Up @@ -102,7 +101,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
Expand Down Expand Up @@ -139,14 +137,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 => ({
Expand Down
Loading
Loading