Skip to content
Open
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
41 changes: 18 additions & 23 deletions src/components/ScreenWrapper/ScreenWrapperContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddi
import useInitialDimensions from '@hooks/useInitialWindowDimensions';
import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useTackInputFocus from '@hooks/useTackInputFocus';
import useTestToolsTapResponder from '@hooks/useTestToolsTapResponder';
import useThemeStyles from '@hooks/useThemeStyles';
import useViewportOffsetTop from '@hooks/useViewportOffsetTop';
import useWindowDimensions from '@hooks/useWindowDimensions';
Expand All @@ -14,14 +15,12 @@ import {isMobile, isMobileWebKit, isSafari} from '@libs/Browser';
import type {ForwardedFSClassProps} from '@libs/Fullstory/types';
import addViewportResizeListener from '@libs/VisualViewport';

import toggleTestToolsModal from '@userActions/TestTool';

import CONST from '@src/CONST';

import type {ComponentRef, ForwardedRef, ReactNode} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';

import React, {useContext, useEffect, useMemo, useRef} from 'react';
import React, {useContext, useEffect, useMemo, useState} from 'react';
import {Keyboard, PanResponder, View} from 'react-native';
import {PickerAvoidingView} from 'react-native-picker-select';

Expand Down Expand Up @@ -161,24 +160,20 @@ function ScreenWrapperContainer({
[isUsingEdgeToEdgeMode, edgeToEdgeBottomContentStyle, legacyBottomContentStyle, bottomContentStyleProp],
);

const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponderCapture: (_e, gestureState) => gestureState.numberActiveTouches === CONST.TEST_TOOL.NUMBER_OF_TAPS,
onPanResponderRelease: toggleTestToolsModal,
}),
).current;

const keyboardDismissPanResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponderCapture: (_e, gestureState) => {
const isHorizontalSwipe = Math.abs(gestureState.dx) > Math.abs(gestureState.dy);
const shouldDismissKeyboard = shouldDismissKeyboardBeforeClose && Keyboard.isVisible() && isMobile();

return isHorizontalSwipe && shouldDismissKeyboard;
},
onPanResponderGrant: Keyboard.dismiss,
}),
).current;
const testToolsTapResponder = useTestToolsTapResponder();

const [keyboardDismissPanResponder] = useState(
() =>
PanResponder.create({
onMoveShouldSetPanResponderCapture: (_e, gestureState) => {
const isHorizontalSwipe = Math.abs(gestureState.dx) > Math.abs(gestureState.dy);
const shouldDismissKeyboard = shouldDismissKeyboardBeforeClose && Keyboard.isVisible() && isMobile();

return isHorizontalSwipe && shouldDismissKeyboard;
},
onPanResponderGrant: Keyboard.dismiss,
}).panHandlers,
);

useEffect(() => {
/**
Expand Down Expand Up @@ -206,7 +201,7 @@ function ScreenWrapperContainer({
ref={ref}
// This style gives the background for the screens. Stack cards are transparent to make different width screens in RHP possible.
style={[styles.flex1, styles.appBG, styles.screenWrapperContainerMinHeight(minHeight)]}
{...panResponder.panHandlers}
{...testToolsTapResponder}
testID={testID}
fsClass={forwardedFSClass}
tabIndex={-1}
Expand All @@ -215,7 +210,7 @@ function ScreenWrapperContainer({
>
<View
style={[shouldEnableMaxHeight && {marginTop: viewportOffsetTop}, style, paddingTopStyle]}
{...keyboardDismissPanResponder.panHandlers}
{...keyboardDismissPanResponder}
>
<KeyboardAvoidingView
style={[styles.w100, styles.h100, !isBlurred ? {maxHeight} : undefined, isAvoidingViewportScroll ? [styles.overflowAuto, styles.overscrollBehaviorContain] : {}]}
Expand Down
68 changes: 68 additions & 0 deletions src/hooks/useRequire2FAOverlayVisibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {getDeepestFocusedScreen, isTwoFactorSetupScreen} from '@libs/Navigation/Navigation';

import NAVIGATORS from '@src/NAVIGATORS';
import SCREENS from '@src/SCREENS';

import {useNavigation} from '@react-navigation/core';

import useRootNavigationState from './useRootNavigationState';
import useShouldShowRequire2FAPage from './useShouldShowRequire2FAPage';

/**
* Portal host rendered above the required-2FA overlay.
* Test tools render here only while that overlay is covering the root stack.
*/
const REQUIRE_2FA_ABOVE_PORTAL_HOST = 'require2FAAbove';
Comment thread
neerajbachani marked this conversation as resolved.

const REQUIRE_2FA_FOCUSED_SCREEN = {
OTHER: 0,
TWO_FACTOR_SETUP: 1,
TEST_TOOLS: 2,
} as const;

type Require2FAFocusedScreen = (typeof REQUIRE_2FA_FOCUSED_SCREEN)[keyof typeof REQUIRE_2FA_FOCUSED_SCREEN];

const TEST_TOOLS_SCREENS = new Set<string>([NAVIGATORS.TEST_TOOLS_MODAL_NAVIGATOR, SCREENS.TEST_TOOLS_MODAL.ROOT, SCREENS.TEST_TOOLS_MODAL.SERVER]);

/**
* Classifies the focused screen for the required-2FA overlay.
* Returns a primitive so navigation listeners can skip unchanged values.
*/
function getRequire2FAFocusedScreen(screenName: string | undefined): Require2FAFocusedScreen {
if (isTwoFactorSetupScreen(screenName)) {
return REQUIRE_2FA_FOCUSED_SCREEN.TWO_FACTOR_SETUP;
}
if (screenName && TEST_TOOLS_SCREENS.has(screenName)) {
return REQUIRE_2FA_FOCUSED_SCREEN.TEST_TOOLS;
}
return REQUIRE_2FA_FOCUSED_SCREEN.OTHER;
}

type Require2FAOverlayVisibility = {
/** The blocking required-2FA screen is mounted over the app. */
isRequire2FAOverlayVisible: boolean;
/** Test tools are the focused route, so they must sit above the overlay and take input. */
isTestToolsRouteFocused: boolean;
};

/**
* Shared by the required-2FA overlay and the test-tools navigator.
* Test tools move above the overlay only while the overlay is covering the root stack.
*/
function useRequire2FAOverlayVisibility(): Require2FAOverlayVisibility {
const navigation = useNavigation();
const shouldShowRequire2FAPage = useShouldShowRequire2FAPage();
const focusedScreen = useRootNavigationState((state) => {
// When navigation is not ready yet, use the navigation state from the navigation hook.
const screenName = getDeepestFocusedScreen(state ?? navigation.getState())?.name;
return getRequire2FAFocusedScreen(screenName);
});

return {
isRequire2FAOverlayVisible: shouldShowRequire2FAPage && focusedScreen !== REQUIRE_2FA_FOCUSED_SCREEN.TWO_FACTOR_SETUP,
isTestToolsRouteFocused: focusedScreen === REQUIRE_2FA_FOCUSED_SCREEN.TEST_TOOLS,
};
}

export default useRequire2FAOverlayVisibility;
export {REQUIRE_2FA_ABOVE_PORTAL_HOST, REQUIRE_2FA_FOCUSED_SCREEN, getRequire2FAFocusedScreen};
21 changes: 21 additions & 0 deletions src/hooks/useTestToolsTapResponder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import toggleTestToolsModal from '@userActions/TestTool';

import CONST from '@src/CONST';

import {useState} from 'react';
import {PanResponder} from 'react-native';

/** Four-finger tap that opens test tools. Shared by screen wrappers and the required-2FA overlay, which sits above those screens. */
function useTestToolsTapResponder() {
const [panHandlers] = useState(
() =>
PanResponder.create({
onStartShouldSetPanResponderCapture: (_event, gestureState) => gestureState.numberActiveTouches === CONST.TEST_TOOL.NUMBER_OF_TAPS,
onPanResponderRelease: toggleTestToolsModal,
}).panHandlers,
);

return panHandlers;
}

export default useTestToolsTapResponder;
11 changes: 11 additions & 0 deletions src/libs/Navigation/AppNavigator/AuthScreens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ import WideRHPContextProvider from '@components/WideRHPContextProvider';
import useGPSInProgressModal from '@hooks/useGPSInProgressModal';
import useGPSTripStateChecker from '@hooks/useGPSTripStateChecker';
import useOnboardingFlowRouter from '@hooks/useOnboardingFlow';
import {REQUIRE_2FA_ABOVE_PORTAL_HOST} from '@hooks/useRequire2FAOverlayVisibility';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useShouldSuppressPromotionalUI from '@hooks/useShouldSuppressPromotionalUI';
import {SidebarOrderedReportsContextProvider} from '@hooks/useSidebarOrderedReports';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';

import setFullscreenVisibility from '@libs/actions/setFullscreenVisibility';
import {READ_COMMANDS} from '@libs/API/types';
Expand All @@ -54,7 +56,9 @@ import '@src/libs/subscribeToFullReconnect';
import type {RouteProp} from '@react-navigation/native';
import type {StackCardInterpolationProps} from '@react-navigation/stack';

import {PortalHost} from '@gorhom/portal';
import React, {useEffect} from 'react';
import {StyleSheet, View} from 'react-native';

import attachmentModalScreenOptions from './attachmentModalScreenOptions';
import AuthScreensInitHandler from './AuthScreensInitHandler';
Expand Down Expand Up @@ -131,6 +135,7 @@ const modalScreenListenersWithCancelSearch = {

function AuthScreens() {
const theme = useTheme();
const themeStyles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const rootNavigatorScreenOptions = useRootNavigatorScreenOptions();
Expand Down Expand Up @@ -415,6 +420,12 @@ function AuthScreens() {
/>
</RootStack.Navigator>
<RequireTwoFactorAuthenticationOverlay />
<View
pointerEvents="box-none"
style={[StyleSheet.absoluteFill, themeStyles.twoFARequiredTestToolsHost]}
>
<PortalHost name={REQUIRE_2FA_ABOVE_PORTAL_HOST} />
</View>
<MultifactorAuthenticationModalNavigator />
<SearchRouterModal />
<OpenAppFailureModal />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type {CardAnimationContext} from '@react-navigation/stack';
import type {ContextType} from 'react';

import {createContext} from 'react';

type TestToolsLift = {
/** Move the focused screen into the portal host above the required-2FA overlay. */
shouldLift: boolean;

/** Card animation of the root test-tools modal, captured before the nested stack replaces it. */
cardAnimation: ContextType<typeof CardAnimationContext>;
};

const TestToolsLiftContext = createContext<TestToolsLift>({
shouldLift: false,
cardAnimation: undefined,
});

export default TestToolsLiftContext;
Loading
Loading