Skip to content

Commit ccfdb74

Browse files
authored
Merge pull request Expensify#65435 from software-mansion-labs/war-in/create-hybridapp-onyxkey
[Pre SignInPage] Add HYBRID_APP onyx key
2 parents c73ede0 + 2e131ed commit ccfdb74

16 files changed

Lines changed: 79 additions & 60 deletions

File tree

.eslintrc.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,14 @@ module.exports = {
277277
property: 'isHybridApp',
278278
message: 'Use CONFIG.IS_HYBRID_APP instead.',
279279
},
280+
// Prevent direct use of HybridAppModule.closeReactNativeApp().
281+
// Instead, use the `closeReactNativeApp` action from `@userActions/HybridApp`,
282+
// which correctly updates `hybridApp.closingReactNativeApp` when closing NewDot
283+
{
284+
object: 'HybridAppModule',
285+
property: 'closeReactNativeApp',
286+
message: 'Use `closeReactNativeApp` from `@userActions/HybridApp` instead.',
287+
},
280288
],
281289
'no-restricted-imports': [
282290
'error',

src/ONYXKEYS.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -476,9 +476,6 @@ const ONYXKEYS = {
476476
/** Stores recently used currencies */
477477
RECENTLY_USED_CURRENCIES: 'nvp_recentlyUsedCurrencies',
478478

479-
/** States whether we transitioned from OldDot to show only certain group of screens. It should be undefined on pure NewDot. */
480-
IS_SINGLE_NEW_DOT_ENTRY: 'isSingleNewDotEntry',
481-
482479
/** Company cards custom names */
483480
NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES: 'nvp_expensify_ccCustomNames',
484481

@@ -851,6 +848,9 @@ const ONYXKEYS = {
851848
REPORT_ATTRIBUTES: 'reportAttributes',
852849
REPORT_TRANSACTIONS_AND_VIOLATIONS: 'reportTransactionsAndViolations',
853850
},
851+
852+
/** Stores HybridApp specific state required to interoperate with OldDot */
853+
HYBRID_APP: 'hybridApp',
854854
} as const;
855855

856856
type AllOnyxKeys = DeepValueOf<typeof ONYXKEYS>;
@@ -1173,7 +1173,6 @@ type OnyxValuesMapping = {
11731173
[ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx;
11741174
[ONYXKEYS.IMPORTED_SPREADSHEET]: OnyxTypes.ImportedSpreadsheet;
11751175
[ONYXKEYS.LAST_ROUTE]: string;
1176-
[ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY]: boolean | undefined;
11771176
[ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean;
11781177
[ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record<string, string>;
11791178
[ONYXKEYS.CONCIERGE_REPORT_ID]: string;
@@ -1199,6 +1198,7 @@ type OnyxValuesMapping = {
11991198
[ONYXKEYS.NVP_LAST_IPHONE_LOGIN]: string;
12001199
[ONYXKEYS.NVP_LAST_ANDROID_LOGIN]: string;
12011200
[ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_REPORT_IDS]: string[];
1201+
[ONYXKEYS.HYBRID_APP]: OnyxTypes.HybridApp;
12021202
};
12031203

12041204
type OnyxDerivedValuesMapping = {

src/components/BookTravelButton.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import HybridAppModule from '@expensify/react-native-hybrid-app';
21
import {Str} from 'expensify-common';
32
import type {ReactElement} from 'react';
4-
import React, {useCallback, useContext, useEffect, useState} from 'react';
3+
import React, {useCallback, useEffect, useState} from 'react';
54
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
65
import useLocalize from '@hooks/useLocalize';
76
import useOnyx from '@hooks/useOnyx';
@@ -15,14 +14,14 @@ import Log from '@libs/Log';
1514
import Navigation from '@libs/Navigation/Navigation';
1615
import {getActivePolicies, getAdminsPrivateEmailDomains, isPaidGroupPolicy} from '@libs/PolicyUtils';
1716
import colors from '@styles/theme/colors';
17+
import closeReactNativeApp from '@userActions/HybridApp';
1818
import CONFIG from '@src/CONFIG';
1919
import CONST from '@src/CONST';
2020
import ONYXKEYS from '@src/ONYXKEYS';
2121
import ROUTES from '@src/ROUTES';
2222
import {isEmptyObject} from '@src/types/utils/EmptyObject';
2323
import Button from './Button';
2424
import ConfirmModal from './ConfirmModal';
25-
import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext';
2625
import DotIndicatorMessage from './DotIndicatorMessage';
2726
import {RocketDude} from './Icon/Illustrations';
2827
import Text from './Text';
@@ -62,7 +61,6 @@ function BookTravelButton({text, shouldRenderErrorMessageBelowButton = false, se
6261
const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS, {canBeMissing: false});
6362
const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email, canBeMissing: false});
6463
const primaryContactMethod = primaryLogin ?? sessionEmail ?? '';
65-
const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext);
6664
const {isBlockedFromSpotnanaTravel, isBetaEnabled} = usePermissions();
6765
const [isPreventionModalVisible, setPreventionModalVisibility] = useState(false);
6866
const [isVerificationModalVisible, setVerificationModalVisibility] = useState(false);
@@ -72,7 +70,7 @@ function BookTravelButton({text, shouldRenderErrorMessageBelowButton = false, se
7270
const groupPaidPolicies = activePolicies.filter((activePolicy) => activePolicy.type !== CONST.POLICY.TYPE.PERSONAL && isPaidGroupPolicy(activePolicy));
7371
// Flag indicating whether NewDot was launched exclusively for Travel,
7472
// e.g., when the user selects "Trips" from the Expensify Classic menu in HybridApp.
75-
const [wasNewDotLaunchedJustForTravel] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY, {canBeMissing: false});
73+
const [wasNewDotLaunchedJustForTravel] = useOnyx(ONYXKEYS.HYBRID_APP, {selector: (hybridApp) => hybridApp?.isSingleNewDotEntry, canBeMissing: false});
7674

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

138136
// Close NewDot if it was opened only for Travel, as its purpose is now fulfilled.
139137
Log.info('[HybridApp] Returning to OldDot after opening TravelDot');
140-
HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false});
141-
setRootStatusBarEnabled(false);
138+
closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false});
142139
})
143140
?.catch(() => {
144141
setErrorMessage(translate('travel.errorMessage'));
@@ -170,17 +167,16 @@ function BookTravelButton({text, shouldRenderErrorMessageBelowButton = false, se
170167
isBlockedFromSpotnanaTravel,
171168
primaryContactMethod,
172169
policy,
170+
groupPaidPolicies.length,
173171
travelSettings?.hasAcceptedTerms,
172+
travelSettings?.lastTravelSignupRequestTime,
173+
isBetaEnabled,
174174
styles.flexRow,
175175
styles.link,
176176
StyleUtils,
177177
translate,
178178
wasNewDotLaunchedJustForTravel,
179-
setRootStatusBarEnabled,
180179
isUserValidated,
181-
groupPaidPolicies.length,
182-
isBetaEnabled,
183-
travelSettings?.lastTravelSignupRequestTime,
184180
]);
185181

186182
return (

src/components/CustomStatusBarAndBackground/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import React, {useCallback, useContext, useEffect, useRef, useState} from 'react';
22
import {interpolateColor, runOnJS, useAnimatedReaction, useSharedValue, withDelay, withTiming} from 'react-native-reanimated';
3+
import useOnyx from '@hooks/useOnyx';
34
import usePrevious from '@hooks/usePrevious';
45
import useTheme from '@hooks/useTheme';
56
import {navigationRef} from '@libs/Navigation/Navigation';
67
import StatusBar from '@libs/StatusBar';
78
import type {StatusBarStyle} from '@styles/index';
9+
import ONYXKEYS from '@src/ONYXKEYS';
810
import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackgroundContext';
911
import updateGlobalBackgroundColor from './updateGlobalBackgroundColor';
1012
import updateStatusBarAppearance from './updateStatusBarAppearance';
@@ -19,8 +21,11 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack
1921
const {isRootStatusBarEnabled, setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext);
2022
const theme = useTheme();
2123
const [statusBarStyle, setStatusBarStyle] = useState<StatusBarStyle>();
24+
const [closingReactNativeApp = false] = useOnyx(ONYXKEYS.HYBRID_APP, {selector: (hybridApp) => hybridApp?.closingReactNativeApp, canBeMissing: true});
2225

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

2530
// Disable the root status bar when a nested status bar is rendered
2631
useEffect(() => {

src/components/ScreenWrapper/index.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import HybridAppModule from '@expensify/react-native-hybrid-app';
21
import {useIsFocused, useNavigation, usePreventRemove} from '@react-navigation/native';
32
import type {ForwardedRef, ReactNode} from 'react';
43
import React, {forwardRef, useContext, useEffect, useMemo, useState} from 'react';
54
import type {StyleProp, View, ViewStyle} from 'react-native';
65
import {Keyboard} from 'react-native';
76
import type {EdgeInsets} from 'react-native-safe-area-context';
87
import CustomDevMenu from '@components/CustomDevMenu';
9-
import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext';
108
import FocusTrapForScreen from '@components/FocusTrap/FocusTrapForScreen';
119
import type FocusTrapForScreenProps from '@components/FocusTrap/FocusTrapForScreen/FocusTrapProps';
1210
import HeaderGap from '@components/HeaderGap';
@@ -22,6 +20,7 @@ import NarrowPaneContext from '@libs/Navigation/AppNavigator/Navigators/NarrowPa
2220
import Navigation from '@libs/Navigation/Navigation';
2321
import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
2422
import type {ReportsSplitNavigatorParamList, RootNavigatorParamList} from '@libs/Navigation/types';
23+
import closeReactNativeApp from '@userActions/HybridApp';
2524
import CONFIG from '@src/CONFIG';
2625
import CONST from '@src/CONST';
2726
import ONYXKEYS from '@src/ONYXKEYS';
@@ -170,15 +169,13 @@ function ScreenWrapper(
170169
const shouldOffsetMobileOfflineIndicator = displaySmallScreenOfflineIndicator && addSmallScreenOfflineIndicatorBottomSafeAreaPadding && isOffline;
171170

172171
const {initialURL} = useContext(InitialURLContext);
173-
const [isSingleNewDotEntry] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY, {canBeMissing: true});
174-
const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext);
172+
const [isSingleNewDotEntry = false] = useOnyx(ONYXKEYS.HYBRID_APP, {selector: (hybridApp) => hybridApp?.isSingleNewDotEntry, canBeMissing: true});
175173

176-
usePreventRemove((isSingleNewDotEntry ?? false) && initialURL === Navigation.getActiveRouteWithoutParams(), () => {
174+
usePreventRemove(isSingleNewDotEntry && initialURL === Navigation.getActiveRouteWithoutParams(), () => {
177175
if (!CONFIG.IS_HYBRID_APP) {
178176
return;
179177
}
180-
HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false});
181-
setRootStatusBarEnabled(false);
178+
closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false});
182179
});
183180

184181
useEffect(() => {

src/hooks/useOnboardingFlow.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function useOnboardingFlowRouter() {
3232

3333
const [dismissedProductTraining, dismissedProductTrainingMetadata] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true});
3434

35-
const [isSingleNewDotEntry, isSingleNewDotEntryMetadata] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY, {canBeMissing: true});
35+
const [isSingleNewDotEntry, isSingleNewDotEntryMetadata] = useOnyx(ONYXKEYS.HYBRID_APP, {selector: (hybridApp) => hybridApp?.isSingleNewDotEntry, canBeMissing: true});
3636

3737
useEffect(() => {
3838
// This should delay opening the onboarding modal so it does not interfere with the ongoing ReportScreen params changes

src/libs/Notification/PushNotification/subscribeToPushNotifications.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ Onyx.connect({
4141

4242
let isSingleNewDotEntry: boolean | undefined;
4343
Onyx.connect({
44-
key: ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY,
44+
key: ONYXKEYS.HYBRID_APP,
4545
callback: (value) => {
4646
if (!value) {
4747
return;
4848
}
49-
isSingleNewDotEntry = value;
49+
isSingleNewDotEntry = value?.isSingleNewDotEntry;
5050
},
5151
});
5252

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import HybridAppModule from '@expensify/react-native-hybrid-app';
2+
import Onyx from 'react-native-onyx';
3+
import CONFIG from '@src/CONFIG';
4+
import ONYXKEYS from '@src/ONYXKEYS';
5+
6+
function closeReactNativeApp({shouldSignOut, shouldSetNVP}: {shouldSignOut: boolean; shouldSetNVP: boolean}) {
7+
if (CONFIG.IS_HYBRID_APP) {
8+
Onyx.merge(ONYXKEYS.HYBRID_APP, {closingReactNativeApp: true});
9+
}
10+
// eslint-disable-next-line no-restricted-properties
11+
HybridAppModule.closeReactNativeApp({shouldSignOut, shouldSetNVP});
12+
}
13+
14+
export default closeReactNativeApp;

src/libs/actions/Session/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {hideContextMenu} from '@pages/home/report/ContextMenu/ReportActionContex
4747
import {KEYS_TO_PRESERVE, openApp, reconnectApp} from '@userActions/App';
4848
import {KEYS_TO_PRESERVE_DELEGATE_ACCESS} from '@userActions/Delegate';
4949
import * as Device from '@userActions/Device';
50+
import closeReactNativeApp from '@userActions/HybridApp';
5051
import redirectToSignIn from '@userActions/SignInRedirect';
5152
import Timing from '@userActions/Timing';
5253
import * as Welcome from '@userActions/Welcome';
@@ -265,7 +266,7 @@ function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSess
265266

266267
// In the HybridApp, we want the Old Dot to handle the sign out process
267268
if (CONFIG.IS_HYBRID_APP && shouldKillHybridApp) {
268-
HybridAppModule.closeReactNativeApp({shouldSignOut: true, shouldSetNVP: false});
269+
closeReactNativeApp({shouldSignOut: true, shouldSetNVP: false});
269270
return;
270271
}
271272

@@ -616,13 +617,14 @@ function signInAfterTransitionFromOldDot(hybridAppSettings: string) {
616617
Onyx.multiSet({
617618
[ONYXKEYS.SESSION]: {email, authToken, encryptedAuthToken: decodeURIComponent(encryptedAuthToken), accountID: Number(accountID)},
618619
[ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin, autoGeneratedPassword},
619-
[ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY]: isSingleNewDotEntry,
620620
[ONYXKEYS.NVP_TRY_NEW_DOT]: {
621621
classicRedirect: {completedHybridAppOnboarding},
622622
nudgeMigration: nudgeMigrationTimestamp ? {timestamp: new Date(nudgeMigrationTimestamp)} : undefined,
623623
},
624624
[ONYXKEYS.ACCOUNT]: {shouldUseStagingServer: isStaging},
625-
}).then(() => Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin, requiresTwoFactorAuth, needsTwoFactorAuthSetup})),
625+
})
626+
.then(() => Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin, requiresTwoFactorAuth, needsTwoFactorAuthSetup}))
627+
.then(() => Onyx.merge(ONYXKEYS.HYBRID_APP, {isSingleNewDotEntry, closingReactNativeApp: false})),
626628
)
627629
.then(() => {
628630
if (clearOnyxOnStart) {

src/pages/ErrorPage/SessionExpiredPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import HybridAppModule from '@expensify/react-native-hybrid-app';
21
import React from 'react';
32
import {View} from 'react-native';
43
import Icon from '@components/Icon';
@@ -10,6 +9,7 @@ import useLocalize from '@hooks/useLocalize';
109
import useTheme from '@hooks/useTheme';
1110
import useThemeStyles from '@hooks/useThemeStyles';
1211
import Navigation from '@libs/Navigation/Navigation';
12+
import closeReactNativeApp from '@userActions/HybridApp';
1313
import {clearSignInData} from '@userActions/Session';
1414
import CONFIG from '@src/CONFIG';
1515

@@ -39,7 +39,7 @@ function SessionExpiredPage() {
3939
Navigation.goBack();
4040
return;
4141
}
42-
HybridAppModule.closeReactNativeApp({shouldSignOut: true, shouldSetNVP: false});
42+
closeReactNativeApp({shouldSignOut: true, shouldSetNVP: false});
4343
}}
4444
>
4545
{translate('deeplinkWrapper.signIn')}

0 commit comments

Comments
 (0)