Skip to content
Merged
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
7 changes: 6 additions & 1 deletion src/components/MagicCodeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ type MagicCodeInputProps = {
/** Function to call when the input is changed */
onChangeText?: (value: string) => void;

/** Callback that is called when the text input is focused */
onFocus?: () => void;

/** Function to call when the input is submitted or fully complete */
onFulfill?: (value: string) => void;

Expand Down Expand Up @@ -137,6 +140,7 @@ function MagicCodeInput(
errorText = '',
shouldSubmitOnComplete = true,
onChangeText: onChangeTextProp = () => {},
onFocus: onFocusProps,
maxLength = CONST.MAGIC_CODE_LENGTH,
onFulfill = () => {},
isDisableKeyboard = false,
Expand Down Expand Up @@ -257,6 +261,7 @@ function MagicCodeInput(
lastValue.current = TEXT_INPUT_EMPTY_STATE;
setInputAndIndex(lastFocusedIndex.current);
}
onFocusProps?.();
event.preventDefault();
};

Expand Down Expand Up @@ -480,7 +485,7 @@ function MagicCodeInput(
inputStyle={[styles.inputTransparent]}
role={CONST.ROLE.PRESENTATION}
style={[styles.inputTransparent]}
textInputContainerStyles={[styles.borderNone, styles.bgTransparent]}
textInputContainerStyles={[styles.borderTransparent, styles.bgTransparent]}
testID={testID}
/>
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ type BaseTwoFactorAuthFormProps = {
// Set this to false in order to disable 2FA when a valid code is entered.
validateInsteadOfDisable?: boolean;

/** Callback that is called when the text input is focused */
onFocus?: () => void;

shouldAutoFocusOnMobile?: boolean;
};

const isMobile = !canFocusInputOnScreenFocus();

function BaseTwoFactorAuthForm({autoComplete, validateInsteadOfDisable, shouldAutoFocusOnMobile = true}: BaseTwoFactorAuthFormProps, ref: ForwardedRef<BaseTwoFactorAuthFormRef>) {
function BaseTwoFactorAuthForm({autoComplete, validateInsteadOfDisable, onFocus, shouldAutoFocusOnMobile = true}: BaseTwoFactorAuthFormProps, ref: ForwardedRef<BaseTwoFactorAuthFormRef>) {
const {translate} = useLocalize();
const [formError, setFormError] = useState<{twoFactorAuthCode?: string}>({});
const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: false});
Expand Down Expand Up @@ -117,6 +120,7 @@ function BaseTwoFactorAuthForm({autoComplete, validateInsteadOfDisable, shouldAu
name="twoFactorAuthCode"
value={twoFactorAuthCode}
onChangeText={onTextInput}
onFocus={onFocus}
onFulfill={validateAndSubmitForm}
errorText={formError.twoFactorAuthCode ?? getLatestErrorMessage(account)}
ref={inputRef}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import React from 'react';
import BaseTwoFactorAuthForm from './BaseTwoFactorAuthForm';
import type {TwoFactorAuthFormProps} from './types';

function TwoFactorAuthForm({innerRef, validateInsteadOfDisable, shouldAutoFocusOnMobile}: TwoFactorAuthFormProps) {
function TwoFactorAuthForm({innerRef, validateInsteadOfDisable, onFocus, shouldAutoFocusOnMobile}: TwoFactorAuthFormProps) {
return (
<BaseTwoFactorAuthForm
ref={innerRef}
autoComplete="one-time-code"
validateInsteadOfDisable={validateInsteadOfDisable}
onFocus={onFocus}
shouldAutoFocusOnMobile={shouldAutoFocusOnMobile}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ type TwoFactorAuthFormProps = {
// Set this to true in order to call the validateTwoFactorAuth action which is used when setting up 2FA for the first time.
// Set this to false in order to disable 2FA when a valid code is entered.
validateInsteadOfDisable?: boolean;

/** Callback that is called when the text input is focused */
onFocus?: () => void;
};

export type {TwoFactorAuthFormProps, BaseTwoFactorAuthFormRef};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import useOnyx from '@hooks/useOnyx';
import useViewportOffsetTop from '@hooks/useViewportOffsetTop';
import {quitAndNavigateBack} from '@libs/actions/TwoFactorAuthActions';
import CONST from '@src/CONST';
import type {StepCounterParams} from '@src/languages/params';
Expand All @@ -28,10 +29,21 @@ type TwoFactorAuthWrapperProps = ChildrenProps & {

/** Flag to indicate if the keyboard avoiding view should be enabled */
shouldEnableKeyboardAvoidingView?: boolean;

/** Flag to indicate if the viewport offset top should be enabled */
shouldEnableViewportOffsetTop?: boolean;
};

function TwoFactorAuthWrapper({stepName, title, stepCounter, onBackButtonPress, shouldEnableKeyboardAvoidingView = true, children}: TwoFactorAuthWrapperProps) {
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
function TwoFactorAuthWrapper({
stepName,
title,
stepCounter,
onBackButtonPress,
shouldEnableKeyboardAvoidingView = true,
shouldEnableViewportOffsetTop = false,
children,
}: TwoFactorAuthWrapperProps) {
const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: false});
const isActingAsDelegate = !!account?.delegatedAccess?.delegate;

// eslint-disable-next-line rulesdir/no-negated-variables
Expand All @@ -58,6 +70,8 @@ function TwoFactorAuthWrapper({stepName, title, stepCounter, onBackButtonPress,
}
}, [account, stepName]);

const viewportOffsetTop = useViewportOffsetTop();

if (isActingAsDelegate) {
return (
<ScreenWrapper
Expand All @@ -78,6 +92,7 @@ function TwoFactorAuthWrapper({stepName, title, stepCounter, onBackButtonPress,
shouldEnableKeyboardAvoidingView={shouldEnableKeyboardAvoidingView}
shouldEnableMaxHeight
testID={stepName}
style={shouldEnableViewportOffsetTop ? {marginTop: viewportOffsetTop} : undefined}
>
<FullPageNotFoundView
shouldShow={shouldShowNotFound}
Expand Down
58 changes: 36 additions & 22 deletions src/pages/settings/Security/TwoFactorAuth/VerifyPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, {useEffect, useRef} from 'react';
import {View} from 'react-native';
import React, {useCallback, useEffect, useRef} from 'react';
import {InteractionManager, View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import type {ScrollView as RNScrollView} from 'react-native';
import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png';
import Button from '@components/Button';
import FixedFooter from '@components/FixedFooter';
Expand Down Expand Up @@ -70,6 +72,15 @@ function VerifyPage({route}: VerifyPageProps) {
return `otpauth://totp/Expensify:${contactMethod}?secret=${account?.twoFactorAuthSecretKey}&issuer=Expensify`;
}

const scrollViewRef = useRef<RNScrollView>(null);
const handleInputFocus = useCallback(() => {
InteractionManager.runAfterInteractions(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@suneox why was this InteractionManager necessary here? (I'm working on InteractionManager migration and I need to understand the use case)

Thank you in advance!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On iOS/mWeb, we trigger a scroll to the bottom to handle the issue keyboard above input

requestAnimationFrame(() => {
scrollViewRef.current?.scrollToEnd({animated: true});
});
});
}, []);

return (
<TwoFactorAuthWrapper
stepName={CONST.TWO_FACTOR_AUTH_STEPS.VERIFY}
Expand All @@ -80,12 +91,14 @@ function VerifyPage({route}: VerifyPageProps) {
total: 3,
}}
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_2FA_ROOT.getRoute(route.params?.backTo, route.params?.forwardTo))}
shouldEnableViewportOffsetTop
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still didn't fix iOS safari issue. More details: #75199 (comment)

Copy link
Contributor Author

@suneox suneox Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This functionality worked in the past on older iOS versions

Screen.Recording.2025-08-02.at.15.58.27.mp4

and the new fix on PR #81812 will cause the navigation to stop sticking to the top & the save button to stop sticking to the bottom while scrolling

>
<ScrollView
ref={scrollViewRef}
keyboardShouldPersistTaps="handled"
contentContainerStyle={styles.flexGrow1}
>
<View style={[styles.ph5, styles.mt3, styles.flexGrow1]}>
<View style={[styles.ph5, styles.mt3]}>
<Text>
{translate('twoFactorAuth.scanCode')}
<TextLink href={TROUBLESHOOTING_LINK}> {translate('twoFactorAuth.authenticatorApp')}</TextLink>.
Expand Down Expand Up @@ -116,27 +129,28 @@ function VerifyPage({route}: VerifyPageProps) {
</View>
<Text style={styles.mt11}>{translate('twoFactorAuth.enterCode')}</Text>
</View>
<FixedFooter style={[styles.mt2, styles.pt2]}>
<View style={[styles.mh5, styles.mb4]}>
<TwoFactorAuthForm
innerRef={formRef}
shouldAutoFocusOnMobile={false}
/>
</View>
<Button
success
large
text={translate('common.next')}
isLoading={account?.isLoading}
onPress={() => {
if (!formRef.current) {
return;
}
formRef.current.validateAndSubmitForm();
}}
<View style={[styles.mh5, styles.mb4, styles.mt3]}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apply the same style at DisablePage

<TwoFactorAuthForm
innerRef={formRef}
shouldAutoFocusOnMobile={false}
onFocus={handleInputFocus}
/>
</FixedFooter>
</View>
</ScrollView>
<FixedFooter style={[styles.mt2, styles.pt2]}>
<Button
success
large
text={translate('common.next')}
isLoading={account?.isLoading}
onPress={() => {
if (!formRef.current) {
return;
}
formRef.current.validateAndSubmitForm();
}}
/>
</FixedFooter>
</TwoFactorAuthWrapper>
);
}
Expand Down
3 changes: 3 additions & 0 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2561,6 +2561,9 @@ const styles = (theme: ThemeColors) =>
borderWidth: 0,
borderBottomWidth: 0,
},
borderTransparent: {
borderColor: 'transparent',
},

borderRight: {
borderRightWidth: 1,
Expand Down
Loading