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
6 changes: 5 additions & 1 deletion src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {IOURequestType} from './libs/actions/IOU';
import Log from './libs/Log';
import type {RootNavigatorParamList} from './libs/Navigation/types';
import type {ReimbursementAccountStepToOpen} from './libs/ReimbursementAccountUtils';
import {getUrlWithParams} from './libs/Url';
import SCREENS from './SCREENS';
import type {Screen} from './SCREENS';
import type {ExitReason} from './types/form/ExitSurveyReasonForm';
Expand Down Expand Up @@ -345,7 +346,10 @@ const ROUTES = {
// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
getUrlWithBackToParam(forwardTo ? `settings/profile/contact-methods/verify?forwardTo=${encodeURIComponent(forwardTo)}` : 'settings/profile/contact-methods/verify', backTo),
},

SETTINGS_2FA_VERIFY_ACCOUNT: {
route: `settings/security/two-factor-auth/${VERIFY_ACCOUNT}`,
getRoute: (params: {backTo?: string; forwardTo?: string} = {}) => getUrlWithParams(`settings/security/two-factor-auth/${VERIFY_ACCOUNT}`, params),
},
SETTINGS_2FA_ROOT: {
route: 'settings/security/two-factor-auth',
getRoute: (backTo?: string, forwardTo?: string) =>
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ const SCREENS = {
TWO_FACTOR_AUTH: {
ROOT: 'Settings_TwoFactorAuth_Root',
VERIFY: 'Settings_TwoFactorAuth_Verify',
VERIFY_ACCOUNT: 'Settings_TwoFactorAuth_VerifyAccount',
SUCCESS: 'Settings_TwoFactorAuth_Success',
DISABLED: 'Settings_TwoFactorAuth_Disabled',
DISABLE: 'Settings_TwoFactorAuth_Disable',
Expand Down
16 changes: 15 additions & 1 deletion src/components/ConnectToXeroFlow/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function ConnectToXeroFlow({policyID}: ConnectToXeroFlowProps) {
const authToken = session?.authToken ?? null;

const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: false});
const isUserValidated = account?.validated;
const is2FAEnabled = account?.requiresTwoFactorAuth ?? false;

const renderLoading = () => <FullScreenLoadingIndicator />;
Expand All @@ -44,7 +45,20 @@ function ConnectToXeroFlow({policyID}: ConnectToXeroFlowProps) {
<RequireTwoFactorAuthenticationModal
onSubmit={() => {
setIsRequire2FAModalOpen(false);
close(() => Navigation.navigate(ROUTES.SETTINGS_2FA_ROOT.getRoute(ROUTES.POLICY_ACCOUNTING.getRoute(policyID), getXeroSetupLink(policyID))));
close(() => {
const backTo = ROUTES.POLICY_ACCOUNTING.getRoute(policyID);
const validatedUserForwardTo = getXeroSetupLink(policyID);
if (isUserValidated) {
Navigation.navigate(ROUTES.SETTINGS_2FA_ROOT.getRoute(backTo, validatedUserForwardTo));
return;
}
Navigation.navigate(
ROUTES.SETTINGS_2FA_VERIFY_ACCOUNT.getRoute({
backTo,
forwardTo: ROUTES.SETTINGS_2FA_ROOT.getRoute(backTo, validatedUserForwardTo),
}),
);
});
}}
onCancel={() => setIsRequire2FAModalOpen(false)}
isVisible={isRequire2FAModalOpen}
Expand Down
16 changes: 15 additions & 1 deletion src/components/ConnectToXeroFlow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function ConnectToXeroFlow({policyID}: ConnectToXeroFlowProps) {

const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: false});
const is2FAEnabled = account?.requiresTwoFactorAuth;
const isUserValidated = account?.validated;

const [isRequire2FAModalOpen, setIsRequire2FAModalOpen] = useState(false);

Expand All @@ -34,7 +35,20 @@ function ConnectToXeroFlow({policyID}: ConnectToXeroFlowProps) {
<RequireTwoFactorAuthenticationModal
onSubmit={() => {
setIsRequire2FAModalOpen(false);
close(() => Navigation.navigate(ROUTES.SETTINGS_2FA_ROOT.getRoute(ROUTES.POLICY_ACCOUNTING.getRoute(policyID), getXeroSetupLink(policyID))));
close(() => {
const backTo = ROUTES.POLICY_ACCOUNTING.getRoute(policyID);
const validatedUserForwardTo = getXeroSetupLink(policyID);
if (isUserValidated) {
Navigation.navigate(ROUTES.SETTINGS_2FA_ROOT.getRoute(backTo, validatedUserForwardTo));
return;
}
Navigation.navigate(
ROUTES.SETTINGS_2FA_VERIFY_ACCOUNT.getRoute({
backTo,
forwardTo: ROUTES.SETTINGS_2FA_ROOT.getRoute(backTo, validatedUserForwardTo),
}),
);
});
}}
onCancel={() => {
setIsRequire2FAModalOpen(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
const TwoFactorAuthenticatorStackNavigator = createModalStackNavigator<EnablePaymentsNavigatorParamList>({
[SCREENS.TWO_FACTOR_AUTH.ROOT]: () => require<ReactComponentModule>('../../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default,
[SCREENS.TWO_FACTOR_AUTH.VERIFY]: () => require<ReactComponentModule>('../../../../pages/settings/Security/TwoFactorAuth/VerifyPage').default,
[SCREENS.TWO_FACTOR_AUTH.VERIFY_ACCOUNT]: () => require<ReactComponentModule>('../../../../pages/settings/Security/TwoFactorAuth/VerifyAccountPage').default,
[SCREENS.TWO_FACTOR_AUTH.DISABLED]: () => require<ReactComponentModule>('../../../../pages/settings/Security/TwoFactorAuth/DisabledPage').default,
[SCREENS.TWO_FACTOR_AUTH.DISABLE]: () => require<ReactComponentModule>('../../../../pages/settings/Security/TwoFactorAuth/DisablePage').default,
[SCREENS.TWO_FACTOR_AUTH.SUCCESS]: () => require<ReactComponentModule>('../../../../pages/settings/Security/TwoFactorAuth/SuccessPage').default,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const SETTINGS_TO_RHP: Partial<Record<keyof SettingsSplitNavigatorParamList, str
],
[SCREENS.SETTINGS.SECURITY]: [
SCREENS.TWO_FACTOR_AUTH.ROOT,
SCREENS.TWO_FACTOR_AUTH.VERIFY_ACCOUNT,
SCREENS.TWO_FACTOR_AUTH.VERIFY,
SCREENS.TWO_FACTOR_AUTH.SUCCESS,
SCREENS.TWO_FACTOR_AUTH.DISABLED,
Expand Down
4 changes: 4 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,10 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
},
[SCREENS.RIGHT_MODAL.TWO_FACTOR_AUTH]: {
screens: {
[SCREENS.TWO_FACTOR_AUTH.VERIFY_ACCOUNT]: {
path: ROUTES.SETTINGS_2FA_VERIFY_ACCOUNT.route,
exact: true,
},
[SCREENS.TWO_FACTOR_AUTH.ROOT]: {
path: ROUTES.SETTINGS_2FA_ROOT.route,
exact: true,
Expand Down
4 changes: 4 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,10 @@ type TwoFactorAuthNavigatorParamList = {
backTo?: Routes;
forwardTo?: string;
};
[SCREENS.TWO_FACTOR_AUTH.VERIFY_ACCOUNT]: {
backTo?: Routes;
Comment thread
mountiny marked this conversation as resolved.
forwardTo?: Routes;
};
[SCREENS.TWO_FACTOR_AUTH.VERIFY]: {
backTo?: Routes;
forwardTo?: string;
Expand Down
25 changes: 24 additions & 1 deletion src/libs/Url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,27 @@ function getSearchParamFromUrl(currentUrl: string, param: string) {
return currentUrl ? new URL(currentUrl).searchParams.get(param) : null;
}

export {getSearchParamFromUrl, hasSameExpensifyOrigin, getPathFromURL, appendParam, hasURL, addLeadingForwardSlash, extractUrlDomain};
type UrlWithParams<TBase extends string> = `${TBase}${'' | `?${string}` | `&${string}`}`;
type UrlParams = {backTo?: string; forwardTo?: string} & Record<string, string | number | undefined>;
/**
* Generate a URL with properly encoded query parameters.
*
* @param baseUrl - The base URL.
* @param params - Object containing key-value pairs for query parameters.
* @returns A URL string with encoded query parameters.
*/
function getUrlWithParams<TBase extends string, TParams extends UrlParams>(baseUrl: TBase, params: TParams): UrlWithParams<TBase> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is this method needed now? Feels like something we should already have a solution for

@jmusial jmusial Sep 10, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Wouldn't say it's needed, but nice to have.

Assumed that if we want to get rid of backTo and forwardTo and we probably want to pass params somehow in url would be good to have an util for that.

Currently there's plenty of manually stitched urls in ROUTES like
search/saved-search/rename?name=${name}&q=${jsonQuery} or getUrlWithBackToParam(forwardTo ? 'settings/profile/contact-methods/verify?forwardTo=${encodeURIComponent(forwardTo)}' : 'settings/profile/contact-methods/verify', backTo),

IMO would be cleaner if we can just pass an object to util.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ok I see and it does look cleaner, but in general, lets please try to do these changes that are not necessary for the core purpose of the PR in separate PRs to keep scopes of changes as small as possible

const [path, existingQuery] = baseUrl.split('?', 2);
const searchParams = new URLSearchParams(existingQuery || '');

for (const [key, value] of Object.entries(params)) {
if (value) {
searchParams.set(key, String(value));
}
}

const queryString = searchParams.toString();
return (queryString ? `${path}?${queryString}` : path) as UrlWithParams<TBase>;
}

export {getSearchParamFromUrl, hasSameExpensifyOrigin, getPathFromURL, appendParam, hasURL, addLeadingForwardSlash, extractUrlDomain, getUrlWithParams};
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@ function Enable2FACard({policyID}: Enable2FACardProps) {
<Section
title={translate('validationStep.enable2FATitle')}
icon={ShieldYellow}
titleStyles={[styles.mb4]}
containerStyles={[styles.mh5]}
titleStyles={styles.mb4}
containerStyles={styles.mh5}
menuItems={[
{
title: translate('validationStep.secureYourAccount'),
onPress: () => {
Navigation.navigate(ROUTES.SETTINGS_2FA_ROOT.getRoute(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID)));
},
// Assuming user is validated here, validation is checked at the beginning of ConnectBank Flow
onPress: () => Navigation.navigate(ROUTES.SETTINGS_2FA_ROOT.getRoute(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID))),
icon: Shield,
shouldShowRightIcon: true,
outerWrapperStyle: shouldUseNarrowLayout ? styles.mhn5 : styles.mhn8,
Expand Down
19 changes: 17 additions & 2 deletions src/pages/RequireTwoFactorAuthenticationPage.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
import React from 'react';
import React, {useCallback} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import Button from '@components/Button';
import Icon from '@components/Icon';
import {Encryption} from '@components/Icon/Illustrations';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import variables from '@styles/variables';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type Account from '@src/types/onyx/Account';

const accountValidatedSelector = (account: OnyxEntry<Account>) => account?.validated;

function RequireTwoFactorAuthenticationPage() {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [isUserValidated = false] = useOnyx(ONYXKEYS.ACCOUNT, {selector: accountValidatedSelector, canBeMissing: true});

const handleOnPress = useCallback(() => {
if (isUserValidated) {
Navigation.navigate(ROUTES.SETTINGS_2FA_ROOT.getRoute(ROUTES.REQUIRE_TWO_FACTOR_AUTH));
return;
}
Navigation.navigate(ROUTES.SETTINGS_2FA_VERIFY_ACCOUNT.getRoute({backTo: ROUTES.REQUIRE_TWO_FACTOR_AUTH, forwardTo: ROUTES.SETTINGS_2FA_ROOT.getRoute()}));
}, [isUserValidated]);
Comment on lines +25 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The handleOnPress callback is creating new string values on each render by calling getRoute() functions. According to [PERF-4], functions passed as props should be properly memoized. Consider extracting the route strings outside the callback and using them as dependencies:

const handleOnPress = useCallback(() => {
  if (isUserValidated) {
    Navigation.navigate(settingsTwoFARoute);
    return;
  }
  Navigation.navigate(verifyAccountRoute);
}, [isUserValidated, settingsTwoFARoute, verifyAccountRoute]);

Where the route variables are defined and memoized outside the callback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

IMO this is memoization taken one step too far...

Comment on lines +25 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The handleOnPress callback is creating new route strings on each render by calling getRoute() functions inside the callback. This violates [PERF-4] which requires memoizing objects and functions passed as props. Consider extracting these route values outside the callback and using them as dependencies:

const settingsTwoFARoute = useMemo(() => ROUTES.SETTINGS_2FA_ROOT.getRoute(ROUTES.REQUIRE_TWO_FACTOR_AUTH), []);
const verifyAccountRoute = useMemo(() => ROUTES.SETTINGS_2FA_VERIFY_ACCOUNT.getRoute({backTo: ROUTES.REQUIRE_TWO_FACTOR_AUTH, forwardTo: ROUTES.SETTINGS_2FA_ROOT.getRoute()}), []);

const handleOnPress = useCallback(() => {
  if (isUserValidated) {
    Navigation.navigate(settingsTwoFARoute);
    return;
  }
  Navigation.navigate(verifyAccountRoute);
}, [isUserValidated, settingsTwoFARoute, verifyAccountRoute]);


return (
<ScreenWrapper testID={RequireTwoFactorAuthenticationPage.displayName}>
Expand All @@ -34,7 +49,7 @@ function RequireTwoFactorAuthenticationPage() {
large
success
pressOnEnter
onPress={() => Navigation.navigate(ROUTES.SETTINGS_2FA_ROOT.getRoute(ROUTES.REQUIRE_TWO_FACTOR_AUTH))}
onPress={handleOnPress}
text={translate('twoFactorAuth.enableTwoFactorAuth')}
/>
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ function VerifyAccountPage({route}: VerifyAccountPageProps) {
/>
);
}
VerifyAccountPage.displayName = 'VerifyAccountPage';

export default VerifyAccountPage;
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ function VerifyAccountPage() {
/>
);
}
VerifyAccountPage.displayName = 'VerifyAccountPage';

export default VerifyAccountPage;
5 changes: 5 additions & 0 deletions src/pages/settings/Security/SecuritySettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ function SecuritySettingsPage() {
showLockedAccountModal();
return;
}
if (!isUserValidated) {
Navigation.navigate(ROUTES.SETTINGS_2FA_VERIFY_ACCOUNT.getRoute());
return;
}
Navigation.navigate(ROUTES.SETTINGS_2FA_ROOT.getRoute());
},
},
Expand Down Expand Up @@ -203,6 +207,7 @@ function SecuritySettingsPage() {
}, [
isAccountLocked,
isDelegateAccessRestricted,
isUserValidated,
showDelegateNoAccessModal,
showLockedAccountModal,
privateSubscription?.type,
Expand Down
39 changes: 6 additions & 33 deletions src/pages/settings/Security/TwoFactorAuth/CopyCodesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useEffect, useMemo, useState} from 'react';
import React, {useEffect, useState} from 'react';
import {ActivityIndicator, View} from 'react-native';
import Button from '@components/Button';
import FixedFooter from '@components/FixedFooter';
Expand All @@ -9,51 +9,42 @@ import PressableWithDelayToggle from '@components/Pressable/PressableWithDelayTo
import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import ValidateCodeActionModal from '@components/ValidateCodeActionModal';
import useBeforeRemove from '@hooks/useBeforeRemove';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {READ_COMMANDS} from '@libs/API/types';
import Clipboard from '@libs/Clipboard';
import {getEarliestErrorField, getLatestErrorField} from '@libs/ErrorUtils';
import localFileDownload from '@libs/localFileDownload';
import Navigation from '@libs/Navigation/Navigation';
import {toggleTwoFactorAuth} from '@userActions/Session';
import {quitAndNavigateBack, setCodesAreCopied} from '@userActions/TwoFactorAuthActions';
import {clearContactMethodErrors, requestValidateCodeAction, validateSecondaryLogin} from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import type {TwoFactorAuthPageProps} from './TwoFactorAuthPage';
import TwoFactorAuthWrapper from './TwoFactorAuthWrapper';

function CopyCodesPage({route}: TwoFactorAuthPageProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate, formatPhoneNumber} = useLocalize();
const {translate} = useLocalize();
// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use correct style
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isExtraSmallScreenWidth, isSmallScreenWidth} = useResponsiveLayout();
const [error, setError] = useState('');

const [account, accountMetadata] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true});
const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true});

const isUserValidated = account?.validated ?? false;
const contactMethod = account?.primaryLogin ?? '';

const loginData = useMemo(() => loginList?.[contactMethod], [loginList, contactMethod]);
const validateLoginError = getEarliestErrorField(loginData, 'validateLogin');

const [isValidateModalVisible, setIsValidateModalVisible] = useState(!isUserValidated);

useEffect(() => {
setIsValidateModalVisible(!isUserValidated);
if (!isUserValidated) {
Navigation.navigate(ROUTES.SETTINGS_2FA_VERIFY_ACCOUNT.getRoute());
return;
}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (isLoadingOnyxValue(accountMetadata) || account?.requiresTwoFactorAuth || account?.recoveryCodes || !isUserValidated) {
return;
Comment on lines 49 to 50

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There's a redundant condition check that could cause issues. The code first checks !isUserValidated and redirects, but then immediately checks !isUserValidated again as part of a compound condition. This could lead to confusing behavior if the component doesn't unmount after the navigation. Simplify the logic by removing the redundant check:

useEffect(() => {
    if (!isUserValidated) {
        Navigation.navigate(ROUTES.SETTINGS_2FA_VERIFY_ACCOUNT.getRoute());
        return;
    }
    
    if (isLoadingOnyxValue(accountMetadata) || account?.requiresTwoFactorAuth || account?.recoveryCodes) {
        return;
    }
    
    toggleTwoFactorAuth(true, READ_COMMANDS.GET_RECOVERY_CODES);
}, [isUserValidated, accountMetadata.status]);

Expand All @@ -62,8 +53,6 @@ function CopyCodesPage({route}: TwoFactorAuthPageProps) {
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- We want to run this when component mounts
}, [isUserValidated, accountMetadata.status]);

useBeforeRemove(() => setIsValidateModalVisible(false));

return (
<TwoFactorAuthWrapper
title={translate('twoFactorAuth.headerTitle')}
Expand Down Expand Up @@ -167,22 +156,6 @@ function CopyCodesPage({route}: TwoFactorAuthPageProps) {
/>
</FixedFooter>
</ScrollView>
<ValidateCodeActionModal
title={translate('contacts.validateAccount')}
descriptionPrimary={translate('contacts.featureRequiresValidate')}
descriptionSecondary={translate('contacts.enterMagicCode', {contactMethod})}
isVisible={isValidateModalVisible}
validateCodeActionErrorField="validateLogin"
validatePendingAction={loginData?.pendingFields?.validateCodeSent}
sendValidateCode={() => requestValidateCodeAction()}
handleSubmitForm={(validateCode) => validateSecondaryLogin(loginList, contactMethod, validateCode, formatPhoneNumber, true)}
validateError={!isEmptyObject(validateLoginError) ? validateLoginError : getLatestErrorField(loginData, 'validateCodeSent')}
clearError={() => clearContactMethodErrors(contactMethod, !isEmptyObject(validateLoginError) ? 'validateLogin' : 'validateCodeSent')}
onClose={() => {
setIsValidateModalVisible(false);
quitAndNavigateBack();
}}
/>
</TwoFactorAuthWrapper>
);
}
Expand Down
19 changes: 19 additions & 0 deletions src/pages/settings/Security/TwoFactorAuth/VerifyAccountPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {TwoFactorAuthNavigatorParamList} from '@libs/Navigation/types';
import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';

type VerifyAccountPageProps = PlatformStackScreenProps<TwoFactorAuthNavigatorParamList, typeof SCREENS.TWO_FACTOR_AUTH.VERIFY_ACCOUNT>;

function VerifyAccountPage({route}: VerifyAccountPageProps) {
return (
<VerifyAccountPageBase
navigateBackTo={route?.params?.backTo ?? ROUTES.SETTINGS_SECURITY}
navigateForwardTo={route?.params?.forwardTo ?? ROUTES.SETTINGS_2FA_ROOT.getRoute()}
/>
);
Comment thread
mountiny marked this conversation as resolved.
}
Comment thread
jmusial marked this conversation as resolved.
VerifyAccountPage.displayName = 'VerifyAccountPage';
export default VerifyAccountPage;
12 changes: 12 additions & 0 deletions tests/unit/UrlTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,16 @@ describe('Url', () => {
});
});
});
describe('getUrlWithParams', () => {
it.each([
['adds params to URL without existing query', '/search', {q: 'hello world', page: 2}, '/search?q=hello+world&page=2'],
['merges with existing query params', '/search?q=old', {page: 2, q: 'new'}, '/search?q=new&page=2'],
['works with absolute URL', 'https://example.com/items?sort=asc', {page: 5}, 'https://example.com/items?sort=asc&page=5'],
['encodes special characters', '/search', {q: 'hello & world'}, '/search?q=hello+%26+world'],
['returns same URL if no params', '/search', {}, '/search'],
['returns same URL if params are undefined', '/search', {q: undefined}, '/search'],
])('%s', (_, baseUrl, params, expected) => {
expect(Url.getUrlWithParams(baseUrl, params)).toBe(expected);
});
});
});
Loading