Skip to content

Commit e815cb9

Browse files
AndrewGableOSBotify
authored andcommitted
Merge pull request #48007 from software-mansion-labs/share-token-in-hybridapp
[HybridApp] Share auth token in HybridApp (cherry picked from commit 217774b) (CP triggered by AndrewGable)
1 parent cbd6547 commit e815cb9

27 files changed

+332
-415
lines changed

__mocks__/react-native.ts

-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ jest.doMock('react-native', () => {
2626
type ReactNativeMock = typeof ReactNative & {
2727
NativeModules: typeof ReactNative.NativeModules & {
2828
BootSplash: {
29-
getVisibilityStatus: typeof BootSplash.getVisibilityStatus;
3029
hide: typeof BootSplash.hide;
3130
logoSizeRatio: number;
3231
navigationBarHeight: number;
@@ -46,7 +45,6 @@ jest.doMock('react-native', () => {
4645
NativeModules: {
4746
...ReactNative.NativeModules,
4847
BootSplash: {
49-
getVisibilityStatus: jest.fn(),
5048
hide: jest.fn(),
5149
logoSizeRatio: 1,
5250
navigationBarHeight: 0,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
diff --git a/node_modules/react-native/Libraries/ReactNative/AppRegistry.js b/node_modules/react-native/Libraries/ReactNative/AppRegistry.js
2+
index 68bd389..be9b5bf 100644
3+
--- a/node_modules/react-native/Libraries/ReactNative/AppRegistry.js
4+
+++ b/node_modules/react-native/Libraries/ReactNative/AppRegistry.js
5+
@@ -232,12 +232,34 @@ const AppRegistry = {
6+
appParameters: Object,
7+
displayMode?: number,
8+
): void {
9+
+ const redactAppParameters = (parameters) => {
10+
+ const initialProps = parameters['initialProps'];
11+
+ const url = initialProps['url'];
12+
+
13+
+ if(!url) {
14+
+ return parameters;
15+
+ }
16+
+
17+
+ const sensitiveParams = ['authToken', 'autoGeneratedPassword', 'autoGeneratedLogin'];
18+
+ const [urlBase, queryString] = url.split('?');
19+
+
20+
+ if (!queryString) {
21+
+ return parameters;
22+
+ }
23+
+
24+
+ const redactedSearchParams = queryString.split('&').map((param) => {
25+
+ const [key, value] = param.split('=');
26+
+ return `${key}=${sensitiveParams.includes(key) ? '<REDACTED>' : value}`
27+
+ });
28+
+ return {...parameters, initialProps: {...initialProps, url: `${urlBase}?${redactedSearchParams.join('&')}`}};
29+
+ }
30+
+
31+
if (appKey !== 'LogBox') {
32+
const msg =
33+
'Updating props for Surface "' +
34+
appKey +
35+
'" with ' +
36+
- JSON.stringify(appParameters);
37+
+ JSON.stringify(redactAppParameters(appParameters));
38+
infoLog(msg);
39+
BugReporting.addSource(
40+
'AppRegistry.setSurfaceProps' + runCount++,

src/App.tsx

+44-41
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {ReportIDsContextProvider} from './hooks/useReportIDs';
3838
import OnyxUpdateManager from './libs/actions/OnyxUpdateManager';
3939
import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext';
4040
import type {Route} from './ROUTES';
41+
import {SplashScreenStateContextProvider} from './SplashScreenStateContext';
4142

4243
type AppProps = {
4344
/** URL passed to our top-level React Native component by HybridApp. Will always be undefined in "pure" NewDot builds. */
@@ -64,47 +65,49 @@ function App({url}: AppProps) {
6465

6566
return (
6667
<StrictModeWrapper>
67-
<InitialURLContextProvider url={url}>
68-
<GestureHandlerRootView style={fill}>
69-
<ComposeProviders
70-
components={[
71-
OnyxProvider,
72-
ThemeProvider,
73-
ThemeStylesProvider,
74-
ThemeIllustrationsProvider,
75-
SafeAreaProvider,
76-
PortalProvider,
77-
SafeArea,
78-
LocaleContextProvider,
79-
HTMLEngineProvider,
80-
WindowDimensionsProvider,
81-
KeyboardStateProvider,
82-
PopoverContextProvider,
83-
CurrentReportIDContextProvider,
84-
ScrollOffsetContextProvider,
85-
ReportAttachmentsProvider,
86-
PickerStateProvider,
87-
EnvironmentProvider,
88-
CustomStatusBarAndBackgroundContextProvider,
89-
ActiveElementRoleProvider,
90-
ActiveWorkspaceContextProvider,
91-
ReportIDsContextProvider,
92-
PlaybackContextProvider,
93-
FullScreenContextProvider,
94-
VolumeContextProvider,
95-
VideoPopoverMenuContextProvider,
96-
KeyboardProvider,
97-
]}
98-
>
99-
<CustomStatusBarAndBackground />
100-
<ErrorBoundary errorMessage="NewExpensify crash caught by error boundary">
101-
<ColorSchemeWrapper>
102-
<Expensify />
103-
</ColorSchemeWrapper>
104-
</ErrorBoundary>
105-
</ComposeProviders>
106-
</GestureHandlerRootView>
107-
</InitialURLContextProvider>
68+
<SplashScreenStateContextProvider>
69+
<InitialURLContextProvider url={url}>
70+
<GestureHandlerRootView style={fill}>
71+
<ComposeProviders
72+
components={[
73+
OnyxProvider,
74+
ThemeProvider,
75+
ThemeStylesProvider,
76+
ThemeIllustrationsProvider,
77+
SafeAreaProvider,
78+
PortalProvider,
79+
SafeArea,
80+
LocaleContextProvider,
81+
HTMLEngineProvider,
82+
WindowDimensionsProvider,
83+
KeyboardStateProvider,
84+
PopoverContextProvider,
85+
CurrentReportIDContextProvider,
86+
ScrollOffsetContextProvider,
87+
ReportAttachmentsProvider,
88+
PickerStateProvider,
89+
EnvironmentProvider,
90+
CustomStatusBarAndBackgroundContextProvider,
91+
ActiveElementRoleProvider,
92+
ActiveWorkspaceContextProvider,
93+
ReportIDsContextProvider,
94+
PlaybackContextProvider,
95+
FullScreenContextProvider,
96+
VolumeContextProvider,
97+
VideoPopoverMenuContextProvider,
98+
KeyboardProvider,
99+
]}
100+
>
101+
<CustomStatusBarAndBackground />
102+
<ErrorBoundary errorMessage="NewExpensify crash caught by error boundary">
103+
<ColorSchemeWrapper>
104+
<Expensify />
105+
</ColorSchemeWrapper>
106+
</ErrorBoundary>
107+
</ComposeProviders>
108+
</GestureHandlerRootView>
109+
</InitialURLContextProvider>
110+
</SplashScreenStateContextProvider>
108111
</StrictModeWrapper>
109112
);
110113
}

src/CONST.ts

+6
Original file line numberDiff line numberDiff line change
@@ -5495,6 +5495,12 @@ const CONST = {
54955495
},
54965496
},
54975497

5498+
BOOT_SPLASH_STATE: {
5499+
VISIBLE: 'visible',
5500+
READY_TO_BE_HIDDEN: 'readyToBeHidden',
5501+
HIDDEN: `hidden`,
5502+
},
5503+
54985504
CSV_IMPORT_COLUMNS: {
54995505
EMAIL: 'email',
55005506
NAME: 'name',

src/Expensify.tsx

+42-52
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Audio} from 'expo-av';
2-
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
2+
import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
33
import type {NativeEventSubscription} from 'react-native';
44
import {AppState, Linking, NativeModules, Platform} from 'react-native';
55
import type {OnyxEntry} from 'react-native-onyx';
@@ -20,8 +20,8 @@ import {updateLastRoute} from './libs/actions/App';
2020
import * as EmojiPickerAction from './libs/actions/EmojiPickerAction';
2121
import * as Report from './libs/actions/Report';
2222
import * as User from './libs/actions/User';
23+
import {handleHybridAppOnboarding} from './libs/actions/Welcome';
2324
import * as ActiveClientManager from './libs/ActiveClientManager';
24-
import BootSplash from './libs/BootSplash';
2525
import FS from './libs/Fullstory';
2626
import * as Growl from './libs/Growl';
2727
import Log from './libs/Log';
@@ -42,6 +42,7 @@ import PopoverReportActionContextMenu from './pages/home/report/ContextMenu/Popo
4242
import * as ReportActionContextMenu from './pages/home/report/ContextMenu/ReportActionContextMenu';
4343
import type {Route} from './ROUTES';
4444
import ROUTES from './ROUTES';
45+
import SplashScreenStateContext from './SplashScreenStateContext';
4546
import type {ScreenShareRequest} from './types/onyx';
4647

4748
Onyx.registerLogger(({level, message}) => {
@@ -80,13 +81,6 @@ type ExpensifyOnyxProps = {
8081

8182
type ExpensifyProps = ExpensifyOnyxProps;
8283

83-
// HybridApp needs access to SetStateAction in order to properly hide SplashScreen when React Native was booted before.
84-
type SplashScreenHiddenContextType = {isSplashHidden?: boolean; setIsSplashHidden: React.Dispatch<React.SetStateAction<boolean>>};
85-
86-
const SplashScreenHiddenContext = React.createContext<SplashScreenHiddenContextType>({
87-
setIsSplashHidden: () => {},
88-
});
89-
9084
function Expensify({
9185
isCheckingPublicRoom = true,
9286
updateAvailable,
@@ -99,12 +93,13 @@ function Expensify({
9993
const appStateChangeListener = useRef<NativeEventSubscription | null>(null);
10094
const [isNavigationReady, setIsNavigationReady] = useState(false);
10195
const [isOnyxMigrated, setIsOnyxMigrated] = useState(false);
102-
const [isSplashHidden, setIsSplashHidden] = useState(false);
96+
const {splashScreenState, setSplashScreenState} = useContext(SplashScreenStateContext);
10397
const [hasAttemptedToOpenPublicRoom, setAttemptedToOpenPublicRoom] = useState(false);
10498
const {translate} = useLocalize();
10599
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
106100
const [session] = useOnyx(ONYXKEYS.SESSION);
107101
const [lastRoute] = useOnyx(ONYXKEYS.LAST_ROUTE);
102+
const [tryNewDotData] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT);
108103
const [shouldShowRequire2FAModal, setShouldShowRequire2FAModal] = useState(false);
109104

110105
useEffect(() => {
@@ -123,11 +118,21 @@ function Expensify({
123118
setAttemptedToOpenPublicRoom(true);
124119
}, [isCheckingPublicRoom]);
125120

121+
useEffect(() => {
122+
if (splashScreenState !== CONST.BOOT_SPLASH_STATE.HIDDEN || tryNewDotData === undefined) {
123+
return;
124+
}
125+
126+
handleHybridAppOnboarding();
127+
}, [splashScreenState, tryNewDotData]);
128+
126129
const isAuthenticated = useMemo(() => !!(session?.authToken ?? null), [session]);
127130
const autoAuthState = useMemo(() => session?.autoAuthState ?? '', [session]);
128131

129132
const shouldInit = isNavigationReady && hasAttemptedToOpenPublicRoom;
130-
const shouldHideSplash = shouldInit && !isSplashHidden;
133+
const shouldHideSplash =
134+
shouldInit &&
135+
(NativeModules.HybridAppModule ? splashScreenState === CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN && isAuthenticated : splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE);
131136

132137
const initializeClient = () => {
133138
if (!Visibility.isVisible()) {
@@ -145,17 +150,9 @@ function Expensify({
145150
}, []);
146151

147152
const onSplashHide = useCallback(() => {
148-
setIsSplashHidden(true);
153+
setSplashScreenState(CONST.BOOT_SPLASH_STATE.HIDDEN);
149154
Performance.markEnd(CONST.TIMING.SIDEBAR_LOADED);
150-
}, []);
151-
152-
const contextValue = useMemo(
153-
() => ({
154-
isSplashHidden,
155-
setIsSplashHidden,
156-
}),
157-
[isSplashHidden, setIsSplashHidden],
158-
);
155+
}, [setSplashScreenState]);
159156

160157
useLayoutEffect(() => {
161158
// Initialize this client as being an active client
@@ -177,24 +174,22 @@ function Expensify({
177174

178175
useEffect(() => {
179176
setTimeout(() => {
180-
BootSplash.getVisibilityStatus().then((status) => {
181-
const appState = AppState.currentState;
182-
Log.info('[BootSplash] splash screen status', false, {appState, status});
183-
184-
if (status === 'visible') {
185-
const propsToLog: Omit<ExpensifyProps & {isAuthenticated: boolean}, 'children' | 'session'> = {
186-
isCheckingPublicRoom,
187-
updateRequired,
188-
updateAvailable,
189-
isSidebarLoaded,
190-
screenShareRequest,
191-
focusModeNotification,
192-
isAuthenticated,
193-
lastVisitedPath,
194-
};
195-
Log.alert('[BootSplash] splash screen is still visible', {propsToLog}, false);
196-
}
197-
});
177+
const appState = AppState.currentState;
178+
Log.info('[BootSplash] splash screen status', false, {appState, splashScreenState});
179+
180+
if (splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE) {
181+
const propsToLog: Omit<ExpensifyProps & {isAuthenticated: boolean}, 'children' | 'session'> = {
182+
isCheckingPublicRoom,
183+
updateRequired,
184+
updateAvailable,
185+
isSidebarLoaded,
186+
screenShareRequest,
187+
focusModeNotification,
188+
isAuthenticated,
189+
lastVisitedPath,
190+
};
191+
Log.alert('[BootSplash] splash screen is still visible', {propsToLog}, false);
192+
}
198193
}, 30 * 1000);
199194

200195
// This timer is set in the native layer when launching the app and we stop it here so we can measure how long
@@ -304,18 +299,15 @@ function Expensify({
304299

305300
<AppleAuthWrapper />
306301
{hasAttemptedToOpenPublicRoom && (
307-
<SplashScreenHiddenContext.Provider value={contextValue}>
308-
<NavigationRoot
309-
onReady={setNavigationReady}
310-
authenticated={isAuthenticated}
311-
lastVisitedPath={lastVisitedPath as Route}
312-
initialUrl={initialUrl}
313-
shouldShowRequire2FAModal={shouldShowRequire2FAModal}
314-
/>
315-
</SplashScreenHiddenContext.Provider>
302+
<NavigationRoot
303+
onReady={setNavigationReady}
304+
authenticated={isAuthenticated}
305+
lastVisitedPath={lastVisitedPath as Route}
306+
initialUrl={initialUrl}
307+
shouldShowRequire2FAModal={shouldShowRequire2FAModal}
308+
/>
316309
)}
317-
{/* HybridApp has own middleware to hide SplashScreen */}
318-
{!NativeModules.HybridAppModule && shouldHideSplash && <SplashScreenHider onHide={onSplashHide} />}
310+
{shouldHideSplash && <SplashScreenHider onHide={onSplashHide} />}
319311
</DeeplinkWrapper>
320312
);
321313
}
@@ -349,5 +341,3 @@ export default withOnyx<ExpensifyProps, ExpensifyOnyxProps>({
349341
key: ONYXKEYS.LAST_VISITED_PATH,
350342
},
351343
})(Expensify);
352-
353-
export {SplashScreenHiddenContext};

src/SplashScreenStateContext.tsx

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React, {useContext, useMemo, useState} from 'react';
2+
import type {ValueOf} from 'type-fest';
3+
import CONST from './CONST';
4+
import type ChildrenProps from './types/utils/ChildrenProps';
5+
6+
type SplashScreenStateContextType = {
7+
splashScreenState: ValueOf<typeof CONST.BOOT_SPLASH_STATE>;
8+
setSplashScreenState: React.Dispatch<React.SetStateAction<ValueOf<typeof CONST.BOOT_SPLASH_STATE>>>;
9+
};
10+
11+
const SplashScreenStateContext = React.createContext<SplashScreenStateContextType>({
12+
splashScreenState: CONST.BOOT_SPLASH_STATE.VISIBLE,
13+
setSplashScreenState: () => {},
14+
});
15+
16+
function SplashScreenStateContextProvider({children}: ChildrenProps) {
17+
const [splashScreenState, setSplashScreenState] = useState<ValueOf<typeof CONST.BOOT_SPLASH_STATE>>(CONST.BOOT_SPLASH_STATE.VISIBLE);
18+
const splashScreenStateContext = useMemo(
19+
() => ({
20+
splashScreenState,
21+
setSplashScreenState,
22+
}),
23+
[splashScreenState],
24+
);
25+
26+
return <SplashScreenStateContext.Provider value={splashScreenStateContext}>{children}</SplashScreenStateContext.Provider>;
27+
}
28+
29+
function useSplashScreenStateContext() {
30+
return useContext(SplashScreenStateContext);
31+
}
32+
33+
export default SplashScreenStateContext;
34+
export {SplashScreenStateContextProvider, useSplashScreenStateContext};

src/components/ErrorBoundary/BaseErrorBoundary.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import BootSplash from '@libs/BootSplash';
44
import GenericErrorPage from '@pages/ErrorPage/GenericErrorPage';
55
import UpdateRequiredView from '@pages/ErrorPage/UpdateRequiredView';
66
import CONST from '@src/CONST';
7+
import {useSplashScreenStateContext} from '@src/SplashScreenStateContext';
78
import type {BaseErrorBoundaryProps, LogError} from './types';
89

910
/**
@@ -14,10 +15,12 @@ import type {BaseErrorBoundaryProps, LogError} from './types';
1415

1516
function BaseErrorBoundary({logError = () => {}, errorMessage, children}: BaseErrorBoundaryProps) {
1617
const [errorContent, setErrorContent] = useState('');
18+
const {setSplashScreenState} = useSplashScreenStateContext();
19+
1720
const catchError = (errorObject: Error, errorInfo: React.ErrorInfo) => {
1821
logError(errorMessage, errorObject, JSON.stringify(errorInfo));
1922
// We hide the splash screen since the error might happened during app init
20-
BootSplash.hide();
23+
BootSplash.hide().then(() => setSplashScreenState(CONST.BOOT_SPLASH_STATE.HIDDEN));
2124
setErrorContent(errorObject.message);
2225
};
2326
const updateRequired = errorContent === CONST.ERROR.UPDATE_REQUIRED;

0 commit comments

Comments
 (0)