Skip to content

Commit fb81e1c

Browse files
authored
Merge pull request #49602 from software-mansion-labs/use-newdot-travel-page-on-olddot
[HybridApp] Allow classic experience users to use NewDot travel page
2 parents 2c00cc5 + 93522da commit fb81e1c

9 files changed

+88
-34
lines changed

src/ONYXKEYS.ts

+4
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,9 @@ const ONYXKEYS = {
441441
/** Stores recently used currencies */
442442
RECENTLY_USED_CURRENCIES: 'nvp_recentlyUsedCurrencies',
443443

444+
/** States whether we transitioned from OldDot to show only certain group of screens. It should be undefined on pure NewDot. */
445+
IS_SINGLE_NEW_DOT_ENTRY: 'isSingleNewDotEntry',
446+
444447
/** Company cards custom names */
445448
NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES: 'nvp_expensify_ccCustomNames',
446449

@@ -1004,6 +1007,7 @@ type OnyxValuesMapping = {
10041007
[ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx;
10051008
[ONYXKEYS.IMPORTED_SPREADSHEET]: OnyxTypes.ImportedSpreadsheet;
10061009
[ONYXKEYS.LAST_ROUTE]: string;
1010+
[ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY]: boolean | undefined;
10071011
[ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean;
10081012
[ONYXKEYS.SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP]: boolean;
10091013
[ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record<string, string>;

src/components/InitialURLContextProvider.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ function InitialURLContextProvider({children, url}: InitialURLContextProviderPro
3131

3232
useEffect(() => {
3333
if (url) {
34-
const route = signInAfterTransitionFromOldDot(url);
35-
setInitialURL(route);
36-
setSplashScreenState(CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN);
34+
signInAfterTransitionFromOldDot(url).then((route) => {
35+
setInitialURL(route);
36+
setSplashScreenState(CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN);
37+
});
3738
return;
3839
}
3940
Linking.getInitialURL().then((initURL) => {

src/components/ScreenWrapper.tsx

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import {useIsFocused, useNavigation} from '@react-navigation/native';
1+
import {UNSTABLE_usePreventRemove, useIsFocused, useNavigation, useRoute} from '@react-navigation/native';
22
import type {StackNavigationProp} from '@react-navigation/stack';
33
import type {ForwardedRef, ReactNode} from 'react';
44
import React, {createContext, forwardRef, useEffect, useMemo, useRef, useState} from 'react';
55
import type {StyleProp, ViewStyle} from 'react-native';
6-
import {Keyboard, PanResponder, View} from 'react-native';
6+
import {Keyboard, NativeModules, PanResponder, View} from 'react-native';
77
import {PickerAvoidingView} from 'react-native-picker-select';
88
import type {EdgeInsets} from 'react-native-safe-area-context';
99
import useEnvironment from '@hooks/useEnvironment';
@@ -161,6 +161,15 @@ function ScreenWrapper(
161161

162162
isKeyboardShownRef.current = keyboardState?.isKeyboardShown ?? false;
163163

164+
const route = useRoute();
165+
const shouldReturnToOldDot = useMemo(() => {
166+
return !!route?.params && 'singleNewDotEntry' in route.params && route.params.singleNewDotEntry === 'true';
167+
}, [route]);
168+
169+
UNSTABLE_usePreventRemove(shouldReturnToOldDot, () => {
170+
NativeModules.HybridAppModule?.closeReactNativeApp(false, false);
171+
});
172+
164173
const panResponder = useRef(
165174
PanResponder.create({
166175
onStartShouldSetPanResponderCapture: (_e, gestureState) => gestureState.numberActiveTouches === CONST.TEST_TOOL.NUMBER_OF_TAPS,

src/hooks/useOnboardingFlow.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,16 @@ function useOnboardingFlowRouter() {
2121
selector: hasCompletedHybridAppOnboardingFlowSelector,
2222
});
2323

24+
const [isSingleNewDotEntry, isSingleNewDotEntryMetadata] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY);
25+
2426
useEffect(() => {
25-
if (isLoadingOnyxValue(isOnboardingCompletedMetadata, isHybridAppOnboardingCompletedMetadata)) {
27+
if (isLoadingOnyxValue(isOnboardingCompletedMetadata, isHybridAppOnboardingCompletedMetadata, isSingleNewDotEntryMetadata)) {
2628
return;
2729
}
2830

2931
if (NativeModules.HybridAppModule) {
3032
// When user is transitioning from OldDot to NewDot, we usually show the explanation modal
31-
if (isHybridAppOnboardingCompleted === false) {
33+
if (isHybridAppOnboardingCompleted === false && !isSingleNewDotEntry) {
3234
Navigation.navigate(ROUTES.EXPLANATION_MODAL_ROOT);
3335
}
3436

@@ -43,7 +45,7 @@ function useOnboardingFlowRouter() {
4345
if (!NativeModules.HybridAppModule && isOnboardingCompleted === false) {
4446
OnboardingFlow.startOnboardingFlow();
4547
}
46-
}, [isOnboardingCompleted, isHybridAppOnboardingCompleted, isOnboardingCompletedMetadata, isHybridAppOnboardingCompletedMetadata]);
48+
}, [isOnboardingCompleted, isHybridAppOnboardingCompleted, isOnboardingCompletedMetadata, isHybridAppOnboardingCompletedMetadata, isSingleNewDotEntryMetadata, isSingleNewDotEntry]);
4749

4850
return {isOnboardingCompleted, isHybridAppOnboardingCompleted};
4951
}

src/libs/TripReservationUtils.ts

+22-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {Str} from 'expensify-common';
22
import type {Dispatch, SetStateAction} from 'react';
3+
import {NativeModules} from 'react-native';
34
import type {OnyxEntry} from 'react-native-onyx';
45
import Onyx from 'react-native-onyx';
56
import type {LocaleContextProps} from '@components/LocaleContextProvider';
@@ -13,6 +14,7 @@ import type Transaction from '@src/types/onyx/Transaction';
1314
import {isEmptyObject} from '@src/types/utils/EmptyObject';
1415
import type IconAsset from '@src/types/utils/IconAsset';
1516
import * as Link from './actions/Link';
17+
import Log from './Log';
1618
import Navigation from './Navigation/Navigation';
1719
import * as PolicyUtils from './PolicyUtils';
1820

@@ -40,6 +42,14 @@ Onyx.connect({
4042
},
4143
});
4244

45+
let isSingleNewDotEntry: boolean | undefined;
46+
Onyx.connect({
47+
key: ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY,
48+
callback: (val) => {
49+
isSingleNewDotEntry = val;
50+
},
51+
});
52+
4353
function getTripReservationIcon(reservationType: ReservationType): IconAsset {
4454
switch (reservationType) {
4555
case CONST.RESERVATION_TYPE.FLIGHT:
@@ -91,8 +101,17 @@ function bookATrip(translate: LocaleContextProps['translate'], setCtaErrorMessag
91101
if (ctaErrorMessage) {
92102
setCtaErrorMessage('');
93103
}
94-
Link.openTravelDotLink(activePolicyID)?.catch(() => {
95-
setCtaErrorMessage(translate('travel.errorMessage'));
96-
});
104+
Link.openTravelDotLink(activePolicyID)
105+
?.then(() => {
106+
if (!NativeModules.HybridAppModule || !isSingleNewDotEntry) {
107+
return;
108+
}
109+
110+
Log.info('[HybridApp] Returning to OldDot after opening TravelDot');
111+
NativeModules.HybridAppModule.closeReactNativeApp(false, false);
112+
})
113+
?.catch(() => {
114+
setCtaErrorMessage(translate('travel.errorMessage'));
115+
});
97116
}
98117
export {getTripReservationIcon, getReservationsFromTripTransactions, getTripEReceiptIcon, bookATrip};

src/libs/actions/Link.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ function openTravelDotLink(policyID: OnyxEntry<string>, postLoginPath?: string)
111111
policyID,
112112
};
113113

114-
return new Promise((_, reject) => {
114+
return new Promise((resolve, reject) => {
115115
const error = new Error('Failed to generate spotnana token.');
116116

117117
asyncOpenURL(
@@ -122,7 +122,9 @@ function openTravelDotLink(policyID: OnyxEntry<string>, postLoginPath?: string)
122122
reject(error);
123123
throw error;
124124
}
125-
return buildTravelDotURL(response.spotnanaToken, postLoginPath);
125+
const travelURL = buildTravelDotURL(response.spotnanaToken, postLoginPath);
126+
resolve(undefined);
127+
return travelURL;
126128
})
127129
.catch(() => {
128130
reject(error);

src/libs/actions/Session/index.ts

+34-19
Original file line numberDiff line numberDiff line change
@@ -482,28 +482,43 @@ function signUpUser() {
482482
function signInAfterTransitionFromOldDot(transitionURL: string) {
483483
const [route, queryParams] = transitionURL.split('?');
484484

485-
const {email, authToken, encryptedAuthToken, accountID, autoGeneratedLogin, autoGeneratedPassword, clearOnyxOnStart, completedHybridAppOnboarding} = Object.fromEntries(
486-
queryParams.split('&').map((param) => {
487-
const [key, value] = param.split('=');
488-
return [key, value];
489-
}),
490-
);
491-
492-
const setSessionDataAndOpenApp = () => {
493-
Onyx.multiSet({
494-
[ONYXKEYS.SESSION]: {email, authToken, encryptedAuthToken: decodeURIComponent(encryptedAuthToken), accountID: Number(accountID)},
495-
[ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin, autoGeneratedPassword},
496-
[ONYXKEYS.NVP_TRYNEWDOT]: {classicRedirect: {completedHybridAppOnboarding: completedHybridAppOnboarding === 'true'}},
497-
}).then(App.openApp);
485+
const {email, authToken, encryptedAuthToken, accountID, autoGeneratedLogin, autoGeneratedPassword, clearOnyxOnStart, completedHybridAppOnboarding, isSingleNewDotEntry, primaryLogin} =
486+
Object.fromEntries(
487+
queryParams.split('&').map((param) => {
488+
const [key, value] = param.split('=');
489+
return [key, value];
490+
}),
491+
);
492+
493+
const clearOnyxForNewAccount = () => {
494+
if (clearOnyxOnStart !== 'true') {
495+
return Promise.resolve();
496+
}
497+
498+
return Onyx.clear(KEYS_TO_PRESERVE);
498499
};
499500

500-
if (clearOnyxOnStart === 'true') {
501-
Onyx.clear(KEYS_TO_PRESERVE).then(setSessionDataAndOpenApp);
502-
} else {
503-
setSessionDataAndOpenApp();
504-
}
501+
const setSessionDataAndOpenApp = new Promise<Route>((resolve) => {
502+
clearOnyxForNewAccount()
503+
.then(() =>
504+
Onyx.multiSet({
505+
[ONYXKEYS.SESSION]: {email, authToken, encryptedAuthToken: decodeURIComponent(encryptedAuthToken), accountID: Number(accountID)},
506+
[ONYXKEYS.ACCOUNT]: {primaryLogin},
507+
[ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin, autoGeneratedPassword},
508+
[ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY]: isSingleNewDotEntry === 'true',
509+
[ONYXKEYS.NVP_TRYNEWDOT]: {classicRedirect: {completedHybridAppOnboarding: completedHybridAppOnboarding === 'true'}},
510+
}),
511+
)
512+
.then(App.openApp)
513+
.catch((error) => {
514+
Log.hmmm('[HybridApp] Initialization of HybridApp has failed. Forcing transition', {error});
515+
})
516+
.finally(() => {
517+
resolve(`${route}?singleNewDotEntry=${isSingleNewDotEntry}` as Route);
518+
});
519+
});
505520

506-
return route as Route;
521+
return setSessionDataAndOpenApp;
507522
}
508523

509524
/**

tests/perf-test/ReportScreen.perf-test.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ jest.mock('@react-navigation/native', () => {
118118
useFocusEffect: jest.fn(),
119119
useIsFocused: () => true,
120120
useRoute: () => jest.fn(),
121+
// eslint-disable-next-line @typescript-eslint/naming-convention
122+
UNSTABLE_usePreventRemove: () => jest.fn(),
121123
useNavigation: () => ({
122124
navigate: jest.fn(),
123125
addListener: () => jest.fn(),
@@ -231,7 +233,6 @@ test('[ReportScreen] should render ReportScreen', async () => {
231233
...reportCollectionDataSet,
232234
...reportActionsCollectionDataSet,
233235
});
234-
235236
await measureRenders(
236237
<ReportScreenWrapper
237238
navigation={navigation}
@@ -311,7 +312,6 @@ test('[ReportScreen] should render report list', async () => {
311312
...reportCollectionDataSet,
312313
...reportActionsCollectionDataSet,
313314
});
314-
315315
await measureRenders(
316316
<ReportScreenWrapper
317317
navigation={navigation}

tests/perf-test/SearchRouter.perf-test.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ jest.mock('@react-navigation/native', () => {
5252
useFocusEffect: jest.fn(),
5353
useIsFocused: () => true,
5454
useRoute: () => jest.fn(),
55+
// eslint-disable-next-line @typescript-eslint/naming-convention
56+
UNSTABLE_usePreventRemove: () => jest.fn(),
5557
useNavigation: () => ({
5658
navigate: jest.fn(),
5759
addListener: () => jest.fn(),

0 commit comments

Comments
 (0)