Skip to content

Commit

Permalink
Merge pull request #48007 from software-mansion-labs/share-token-in-h…
Browse files Browse the repository at this point in the history
…ybridapp

[HybridApp] Share auth token in HybridApp

(cherry picked from commit 217774b)

(CP triggered by AndrewGable)
  • Loading branch information
AndrewGable authored and OSBotify committed Sep 4, 2024
1 parent cbd6547 commit e815cb9
Show file tree
Hide file tree
Showing 27 changed files with 332 additions and 415 deletions.
2 changes: 0 additions & 2 deletions __mocks__/react-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ jest.doMock('react-native', () => {
type ReactNativeMock = typeof ReactNative & {
NativeModules: typeof ReactNative.NativeModules & {
BootSplash: {
getVisibilityStatus: typeof BootSplash.getVisibilityStatus;
hide: typeof BootSplash.hide;
logoSizeRatio: number;
navigationBarHeight: number;
Expand All @@ -46,7 +45,6 @@ jest.doMock('react-native', () => {
NativeModules: {
...ReactNative.NativeModules,
BootSplash: {
getVisibilityStatus: jest.fn(),
hide: jest.fn(),
logoSizeRatio: 1,
navigationBarHeight: 0,
Expand Down
40 changes: 40 additions & 0 deletions patches/react-native+0.75.2+017+redactAppParameters.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
diff --git a/node_modules/react-native/Libraries/ReactNative/AppRegistry.js b/node_modules/react-native/Libraries/ReactNative/AppRegistry.js
index 68bd389..be9b5bf 100644
--- a/node_modules/react-native/Libraries/ReactNative/AppRegistry.js
+++ b/node_modules/react-native/Libraries/ReactNative/AppRegistry.js
@@ -232,12 +232,34 @@ const AppRegistry = {
appParameters: Object,
displayMode?: number,
): void {
+ const redactAppParameters = (parameters) => {
+ const initialProps = parameters['initialProps'];
+ const url = initialProps['url'];
+
+ if(!url) {
+ return parameters;
+ }
+
+ const sensitiveParams = ['authToken', 'autoGeneratedPassword', 'autoGeneratedLogin'];
+ const [urlBase, queryString] = url.split('?');
+
+ if (!queryString) {
+ return parameters;
+ }
+
+ const redactedSearchParams = queryString.split('&').map((param) => {
+ const [key, value] = param.split('=');
+ return `${key}=${sensitiveParams.includes(key) ? '<REDACTED>' : value}`
+ });
+ return {...parameters, initialProps: {...initialProps, url: `${urlBase}?${redactedSearchParams.join('&')}`}};
+ }
+
if (appKey !== 'LogBox') {
const msg =
'Updating props for Surface "' +
appKey +
'" with ' +
- JSON.stringify(appParameters);
+ JSON.stringify(redactAppParameters(appParameters));
infoLog(msg);
BugReporting.addSource(
'AppRegistry.setSurfaceProps' + runCount++,
85 changes: 44 additions & 41 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {ReportIDsContextProvider} from './hooks/useReportIDs';
import OnyxUpdateManager from './libs/actions/OnyxUpdateManager';
import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext';
import type {Route} from './ROUTES';
import {SplashScreenStateContextProvider} from './SplashScreenStateContext';

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

return (
<StrictModeWrapper>
<InitialURLContextProvider url={url}>
<GestureHandlerRootView style={fill}>
<ComposeProviders
components={[
OnyxProvider,
ThemeProvider,
ThemeStylesProvider,
ThemeIllustrationsProvider,
SafeAreaProvider,
PortalProvider,
SafeArea,
LocaleContextProvider,
HTMLEngineProvider,
WindowDimensionsProvider,
KeyboardStateProvider,
PopoverContextProvider,
CurrentReportIDContextProvider,
ScrollOffsetContextProvider,
ReportAttachmentsProvider,
PickerStateProvider,
EnvironmentProvider,
CustomStatusBarAndBackgroundContextProvider,
ActiveElementRoleProvider,
ActiveWorkspaceContextProvider,
ReportIDsContextProvider,
PlaybackContextProvider,
FullScreenContextProvider,
VolumeContextProvider,
VideoPopoverMenuContextProvider,
KeyboardProvider,
]}
>
<CustomStatusBarAndBackground />
<ErrorBoundary errorMessage="NewExpensify crash caught by error boundary">
<ColorSchemeWrapper>
<Expensify />
</ColorSchemeWrapper>
</ErrorBoundary>
</ComposeProviders>
</GestureHandlerRootView>
</InitialURLContextProvider>
<SplashScreenStateContextProvider>
<InitialURLContextProvider url={url}>
<GestureHandlerRootView style={fill}>
<ComposeProviders
components={[
OnyxProvider,
ThemeProvider,
ThemeStylesProvider,
ThemeIllustrationsProvider,
SafeAreaProvider,
PortalProvider,
SafeArea,
LocaleContextProvider,
HTMLEngineProvider,
WindowDimensionsProvider,
KeyboardStateProvider,
PopoverContextProvider,
CurrentReportIDContextProvider,
ScrollOffsetContextProvider,
ReportAttachmentsProvider,
PickerStateProvider,
EnvironmentProvider,
CustomStatusBarAndBackgroundContextProvider,
ActiveElementRoleProvider,
ActiveWorkspaceContextProvider,
ReportIDsContextProvider,
PlaybackContextProvider,
FullScreenContextProvider,
VolumeContextProvider,
VideoPopoverMenuContextProvider,
KeyboardProvider,
]}
>
<CustomStatusBarAndBackground />
<ErrorBoundary errorMessage="NewExpensify crash caught by error boundary">
<ColorSchemeWrapper>
<Expensify />
</ColorSchemeWrapper>
</ErrorBoundary>
</ComposeProviders>
</GestureHandlerRootView>
</InitialURLContextProvider>
</SplashScreenStateContextProvider>
</StrictModeWrapper>
);
}
Expand Down
6 changes: 6 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5495,6 +5495,12 @@ const CONST = {
},
},

BOOT_SPLASH_STATE: {
VISIBLE: 'visible',
READY_TO_BE_HIDDEN: 'readyToBeHidden',
HIDDEN: `hidden`,
},

CSV_IMPORT_COLUMNS: {
EMAIL: 'email',
NAME: 'name',
Expand Down
94 changes: 42 additions & 52 deletions src/Expensify.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Audio} from 'expo-av';
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
import type {NativeEventSubscription} from 'react-native';
import {AppState, Linking, NativeModules, Platform} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
Expand All @@ -20,8 +20,8 @@ import {updateLastRoute} from './libs/actions/App';
import * as EmojiPickerAction from './libs/actions/EmojiPickerAction';
import * as Report from './libs/actions/Report';
import * as User from './libs/actions/User';
import {handleHybridAppOnboarding} from './libs/actions/Welcome';
import * as ActiveClientManager from './libs/ActiveClientManager';
import BootSplash from './libs/BootSplash';
import FS from './libs/Fullstory';
import * as Growl from './libs/Growl';
import Log from './libs/Log';
Expand All @@ -42,6 +42,7 @@ import PopoverReportActionContextMenu from './pages/home/report/ContextMenu/Popo
import * as ReportActionContextMenu from './pages/home/report/ContextMenu/ReportActionContextMenu';
import type {Route} from './ROUTES';
import ROUTES from './ROUTES';
import SplashScreenStateContext from './SplashScreenStateContext';
import type {ScreenShareRequest} from './types/onyx';

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

type ExpensifyProps = ExpensifyOnyxProps;

// HybridApp needs access to SetStateAction in order to properly hide SplashScreen when React Native was booted before.
type SplashScreenHiddenContextType = {isSplashHidden?: boolean; setIsSplashHidden: React.Dispatch<React.SetStateAction<boolean>>};

const SplashScreenHiddenContext = React.createContext<SplashScreenHiddenContextType>({
setIsSplashHidden: () => {},
});

function Expensify({
isCheckingPublicRoom = true,
updateAvailable,
Expand All @@ -99,12 +93,13 @@ function Expensify({
const appStateChangeListener = useRef<NativeEventSubscription | null>(null);
const [isNavigationReady, setIsNavigationReady] = useState(false);
const [isOnyxMigrated, setIsOnyxMigrated] = useState(false);
const [isSplashHidden, setIsSplashHidden] = useState(false);
const {splashScreenState, setSplashScreenState} = useContext(SplashScreenStateContext);
const [hasAttemptedToOpenPublicRoom, setAttemptedToOpenPublicRoom] = useState(false);
const {translate} = useLocalize();
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const [session] = useOnyx(ONYXKEYS.SESSION);
const [lastRoute] = useOnyx(ONYXKEYS.LAST_ROUTE);
const [tryNewDotData] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT);
const [shouldShowRequire2FAModal, setShouldShowRequire2FAModal] = useState(false);

useEffect(() => {
Expand All @@ -123,11 +118,21 @@ function Expensify({
setAttemptedToOpenPublicRoom(true);
}, [isCheckingPublicRoom]);

useEffect(() => {
if (splashScreenState !== CONST.BOOT_SPLASH_STATE.HIDDEN || tryNewDotData === undefined) {
return;
}

handleHybridAppOnboarding();
}, [splashScreenState, tryNewDotData]);

const isAuthenticated = useMemo(() => !!(session?.authToken ?? null), [session]);
const autoAuthState = useMemo(() => session?.autoAuthState ?? '', [session]);

const shouldInit = isNavigationReady && hasAttemptedToOpenPublicRoom;
const shouldHideSplash = shouldInit && !isSplashHidden;
const shouldHideSplash =
shouldInit &&
(NativeModules.HybridAppModule ? splashScreenState === CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN && isAuthenticated : splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE);

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

const onSplashHide = useCallback(() => {
setIsSplashHidden(true);
setSplashScreenState(CONST.BOOT_SPLASH_STATE.HIDDEN);
Performance.markEnd(CONST.TIMING.SIDEBAR_LOADED);
}, []);

const contextValue = useMemo(
() => ({
isSplashHidden,
setIsSplashHidden,
}),
[isSplashHidden, setIsSplashHidden],
);
}, [setSplashScreenState]);

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

useEffect(() => {
setTimeout(() => {
BootSplash.getVisibilityStatus().then((status) => {
const appState = AppState.currentState;
Log.info('[BootSplash] splash screen status', false, {appState, status});

if (status === 'visible') {
const propsToLog: Omit<ExpensifyProps & {isAuthenticated: boolean}, 'children' | 'session'> = {
isCheckingPublicRoom,
updateRequired,
updateAvailable,
isSidebarLoaded,
screenShareRequest,
focusModeNotification,
isAuthenticated,
lastVisitedPath,
};
Log.alert('[BootSplash] splash screen is still visible', {propsToLog}, false);
}
});
const appState = AppState.currentState;
Log.info('[BootSplash] splash screen status', false, {appState, splashScreenState});

if (splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE) {
const propsToLog: Omit<ExpensifyProps & {isAuthenticated: boolean}, 'children' | 'session'> = {
isCheckingPublicRoom,
updateRequired,
updateAvailable,
isSidebarLoaded,
screenShareRequest,
focusModeNotification,
isAuthenticated,
lastVisitedPath,
};
Log.alert('[BootSplash] splash screen is still visible', {propsToLog}, false);
}
}, 30 * 1000);

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

<AppleAuthWrapper />
{hasAttemptedToOpenPublicRoom && (
<SplashScreenHiddenContext.Provider value={contextValue}>
<NavigationRoot
onReady={setNavigationReady}
authenticated={isAuthenticated}
lastVisitedPath={lastVisitedPath as Route}
initialUrl={initialUrl}
shouldShowRequire2FAModal={shouldShowRequire2FAModal}
/>
</SplashScreenHiddenContext.Provider>
<NavigationRoot
onReady={setNavigationReady}
authenticated={isAuthenticated}
lastVisitedPath={lastVisitedPath as Route}
initialUrl={initialUrl}
shouldShowRequire2FAModal={shouldShowRequire2FAModal}
/>
)}
{/* HybridApp has own middleware to hide SplashScreen */}
{!NativeModules.HybridAppModule && shouldHideSplash && <SplashScreenHider onHide={onSplashHide} />}
{shouldHideSplash && <SplashScreenHider onHide={onSplashHide} />}
</DeeplinkWrapper>
);
}
Expand Down Expand Up @@ -349,5 +341,3 @@ export default withOnyx<ExpensifyProps, ExpensifyOnyxProps>({
key: ONYXKEYS.LAST_VISITED_PATH,
},
})(Expensify);

export {SplashScreenHiddenContext};
34 changes: 34 additions & 0 deletions src/SplashScreenStateContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, {useContext, useMemo, useState} from 'react';
import type {ValueOf} from 'type-fest';
import CONST from './CONST';
import type ChildrenProps from './types/utils/ChildrenProps';

type SplashScreenStateContextType = {
splashScreenState: ValueOf<typeof CONST.BOOT_SPLASH_STATE>;
setSplashScreenState: React.Dispatch<React.SetStateAction<ValueOf<typeof CONST.BOOT_SPLASH_STATE>>>;
};

const SplashScreenStateContext = React.createContext<SplashScreenStateContextType>({
splashScreenState: CONST.BOOT_SPLASH_STATE.VISIBLE,
setSplashScreenState: () => {},
});

function SplashScreenStateContextProvider({children}: ChildrenProps) {
const [splashScreenState, setSplashScreenState] = useState<ValueOf<typeof CONST.BOOT_SPLASH_STATE>>(CONST.BOOT_SPLASH_STATE.VISIBLE);
const splashScreenStateContext = useMemo(
() => ({
splashScreenState,
setSplashScreenState,
}),
[splashScreenState],
);

return <SplashScreenStateContext.Provider value={splashScreenStateContext}>{children}</SplashScreenStateContext.Provider>;
}

function useSplashScreenStateContext() {
return useContext(SplashScreenStateContext);
}

export default SplashScreenStateContext;
export {SplashScreenStateContextProvider, useSplashScreenStateContext};
5 changes: 4 additions & 1 deletion src/components/ErrorBoundary/BaseErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import BootSplash from '@libs/BootSplash';
import GenericErrorPage from '@pages/ErrorPage/GenericErrorPage';
import UpdateRequiredView from '@pages/ErrorPage/UpdateRequiredView';
import CONST from '@src/CONST';
import {useSplashScreenStateContext} from '@src/SplashScreenStateContext';
import type {BaseErrorBoundaryProps, LogError} from './types';

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

function BaseErrorBoundary({logError = () => {}, errorMessage, children}: BaseErrorBoundaryProps) {
const [errorContent, setErrorContent] = useState('');
const {setSplashScreenState} = useSplashScreenStateContext();

const catchError = (errorObject: Error, errorInfo: React.ErrorInfo) => {
logError(errorMessage, errorObject, JSON.stringify(errorInfo));
// We hide the splash screen since the error might happened during app init
BootSplash.hide();
BootSplash.hide().then(() => setSplashScreenState(CONST.BOOT_SPLASH_STATE.HIDDEN));
setErrorContent(errorObject.message);
};
const updateRequired = errorContent === CONST.ERROR.UPDATE_REQUIRED;
Expand Down
Loading

0 comments on commit e815cb9

Please sign in to comment.