Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
b7cdccf
Verify account uses navidation modal, Copy codes in progress
jmusial Aug 12, 2025
8438b99
Merge branch 'main' into feat/verify-account-uses-navigation-modal
jmusial Aug 19, 2025
9b8387e
fix link issue for 2fa
jmusial Aug 19, 2025
0eb3a20
prettier fix
jmusial Aug 19, 2025
e612903
remove modal implementation
jmusial Aug 19, 2025
0273f8e
add JSDoc to the useRunOnFirstRender
jmusial Aug 20, 2025
f76d104
update dep array
jmusial Aug 20, 2025
8d79824
fix PR comments
jmusial Aug 20, 2025
06c3237
refactor so it does not use back to and forward to urls
jmusial Aug 20, 2025
8559648
remove footer, clear lint errors
jmusial Aug 21, 2025
a583cd2
Merge branch 'main' into feat/verify-account-uses-navigation-modal
jmusial Aug 21, 2025
581f836
fix lint
jmusial Aug 21, 2025
04f29d9
Remove useless import
jmusial Aug 21, 2025
bae8879
revert existing route to still use back to and forward to
jmusial Aug 21, 2025
96e0575
change hook name
jmusial Aug 21, 2025
d58c24b
fix prettier
jmusial Aug 21, 2025
b41be4d
fix prettier
jmusial Aug 21, 2025
8b07a52
Merge branch 'main' into feat/verify-account-uses-navigation-modal
jmusial Aug 25, 2025
82c3a5b
implement url params util
jmusial Aug 25, 2025
ae949a1
remove changes connected with two factor auth
jmusial Aug 25, 2025
7f88263
small fixes
jmusial Aug 25, 2025
6f3c04e
cleanup
jmusial Aug 25, 2025
2cd68f0
fix ts
jmusial Aug 25, 2025
6c94062
Merge branch 'main' into feat/verify-account-uses-navigation-modal
jmusial Aug 25, 2025
1d790c3
add comment
jmusial Aug 25, 2025
9c22c93
Merge branch 'main' into feat/verify-account-uses-navigation-modal
jmusial Aug 26, 2025
08d0654
fix prettier
jmusial Aug 26, 2025
7fd3a68
Merge branch 'main' into feat/verify-account-uses-navigation-modal
mhawryluk Sep 2, 2025
ef09552
Merge branch 'main' into feat/verify-account-uses-navigation-modal
mhawryluk Sep 2, 2025
3e124f4
Fix after merge
mhawryluk Sep 2, 2025
bdc4921
Fix wrong back button destination when accessing verify-account via a…
mhawryluk Sep 4, 2025
6166cf3
Allow detecting verify account routes with query parameters
mhawryluk Sep 5, 2025
b111aaa
Merge branch 'main' into feat/verify-account-uses-navigation-modal
jmusial Sep 9, 2025
1ec6a1a
Merge branch 'main' into feat/verify-account-uses-navigation-modal
jmusial Sep 9, 2025
49bee79
Merge branch 'main' into feat/verify-account-uses-navigation-modal
jmusial Sep 11, 2025
74db137
remove useEffectOnMount and NavigationIsready
jmusial Sep 11, 2025
23c5a62
Update src/ROUTES.ts
jmusial Sep 11, 2025
85ff1d1
Merge branch 'main' into feat/verify-account-uses-navigation-modal
jmusial Sep 11, 2025
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: 5 additions & 2 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ const PUBLIC_SCREENS_ROUTES = {
SAML_SIGN_IN: 'sign-in-with-saml',
} as const;

// Exported for identifying a url as a verify-account route, associated with a page extending the VerifyAccountPageBase component
const VERIFY_ACCOUNT = 'verify-account';
Comment thread
jmusial marked this conversation as resolved.

const ROUTES = {
...PUBLIC_SCREENS_ROUTES,
// This route renders the list of reports.
Expand Down Expand Up @@ -223,6 +226,7 @@ const ROUTES = {
SETTINGS_LOCK_ACCOUNT: 'settings/security/lock-account',
SETTINGS_UNLOCK_ACCOUNT: 'settings/security/unlock-account',
SETTINGS_FAILED_TO_LOCK_ACCOUNT: 'settings/security/failed-to-lock-account',
SETTINGS_DELEGATE_VERIFY_ACCOUNT: `settings/security/delegate/${VERIFY_ACCOUNT}`,
SETTINGS_ADD_DELEGATE: 'settings/security/delegate',
SETTINGS_DELEGATE_ROLE: {
route: 'settings/security/delegate/:login/role/:role',
Expand Down Expand Up @@ -3205,8 +3209,7 @@ const SHARED_ROUTE_PARAMS: Partial<Record<Screen, string[]>> = {
[SCREENS.WORKSPACE.INITIAL]: ['backTo'],
} as const;

// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
export {getUrlWithBackToParam, PUBLIC_SCREENS_ROUTES, SHARED_ROUTE_PARAMS};
export {PUBLIC_SCREENS_ROUTES, SHARED_ROUTE_PARAMS, VERIFY_ACCOUNT};
export default ROUTES;

type ReportAttachmentsRoute = typeof ROUTES.ATTACHMENTS.route;
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ const SCREENS = {
},

DELEGATE: {
VERIFY_ACCOUNT: 'Settings_Delegate_VerifyAccount',
ADD_DELEGATE: 'Settings_Delegate_Add',
DELEGATE_ROLE: 'Settings_Delegate_Role',
DELEGATE_CONFIRM: 'Settings_Delegate_Confirm',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, {useCallback, useEffect, useRef} from 'react';
import {View} from 'react-native';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
import ONYXKEYS from '@src/ONYXKEYS';
import type {ValidateCodeActionContentProps} from './type';
import ValidateCodeForm from './ValidateCodeForm';
import type {ValidateCodeFormHandle} from './ValidateCodeForm/BaseValidateCodeForm';

function ValidateCodeActionContent({
title,
descriptionPrimary,
descriptionSecondary,
onClose,
validateError,
validatePendingAction,
validateCodeActionErrorField,
handleSubmitForm,
clearError,
sendValidateCode,
isLoading,
threeDotsMenuItems = [],
onThreeDotsButtonPress = () => {},
}: ValidateCodeActionContentProps) {
const themeStyles = useThemeStyles();
const validateCodeFormRef = useRef<ValidateCodeFormHandle>(null);
const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE, {canBeMissing: true});
const firstRenderRef = useRef(true);

useEffect(() => {
if (!firstRenderRef.current || validateCodeAction?.validateCodeSent) {
return;
}
firstRenderRef.current = false;

sendValidateCode();
// We only want to send validate code on first render not on change of validateCodeSent, so we don't add it as a dependency.
// eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sendValidateCode]);

const hide = useCallback(() => {
clearError();
onClose?.();
}, [onClose, clearError]);

return (
<ScreenWrapper
includeSafeAreaPaddingBottom
includePaddingTop
shouldEnableMaxHeight
testID={ValidateCodeActionContent.displayName}
offlineIndicatorStyle={themeStyles.mtAuto}
>
<HeaderWithBackButton
title={title}
onBackButtonPress={hide}
threeDotsMenuItems={threeDotsMenuItems}
shouldShowThreeDotsButton={threeDotsMenuItems.length > 0}
shouldOverlayDots
onThreeDotsButtonPress={onThreeDotsButtonPress}
/>

<ScrollView
style={[themeStyles.w100, themeStyles.h100, themeStyles.flex1]}
contentContainerStyle={themeStyles.flexGrow1}
keyboardShouldPersistTaps="handled"
>
<View style={[themeStyles.ph5, themeStyles.mt3, themeStyles.mb5, themeStyles.flex1]}>
<Text style={themeStyles.mb3}>{descriptionPrimary}</Text>
{!!descriptionSecondary && <Text style={themeStyles.mb3}>{descriptionSecondary}</Text>}
<ValidateCodeForm
isLoading={isLoading}
validatePendingAction={validatePendingAction}
validateCodeActionErrorField={validateCodeActionErrorField}
validateError={validateError}
handleSubmitForm={handleSubmitForm}
sendValidateCode={sendValidateCode}
clearError={clearError}
buttonStyles={[themeStyles.justifyContentEnd, themeStyles.flex1]}
ref={validateCodeFormRef}
/>
</View>
</ScrollView>
</ScreenWrapper>
);
}

ValidateCodeActionContent.displayName = 'ValidateCodeActionContent';

export default ValidateCodeActionContent;
99 changes: 19 additions & 80 deletions src/components/ValidateCodeActionModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,113 +1,52 @@
import React, {useCallback, useEffect, useRef} from 'react';
import {View} from 'react-native';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import React from 'react';
import Modal from '@components/Modal';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {ValidateCodeActionModalProps} from './type';
import ValidateCodeForm from './ValidateCodeForm';
import type {ValidateCodeFormHandle} from './ValidateCodeForm/BaseValidateCodeForm';
import ValidateCodeActionContent from './ValidateCodeActionContent';

function ValidateCodeActionModal({
isVisible,
title,
descriptionPrimary,
descriptionSecondary,
onClose,
onModalHide,
validateError,
validatePendingAction,
validateCodeActionErrorField,
handleSubmitForm,
clearError,
footer,
sendValidateCode,
isLoading,
shouldHandleNavigationBack,
disableAnimation,
threeDotsMenuItems = [],
onThreeDotsButtonPress = () => {},
}: ValidateCodeActionModalProps) {
const themeStyles = useThemeStyles();
const firstRenderRef = useRef(true);
const validateCodeFormRef = useRef<ValidateCodeFormHandle>(null);
const styles = useThemeStyles();
const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE, {canBeMissing: true});

const hide = useCallback(() => {
clearError();
onClose?.();
firstRenderRef.current = true;
}, [onClose, clearError]);

useEffect(() => {
if (!firstRenderRef.current || !isVisible || validateCodeAction?.validateCodeSent) {
return;
}
firstRenderRef.current = false;

sendValidateCode();
// We only want to send validate code on first render not on change of validateCodeSent, so we don't add it as a dependency.
// eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isVisible, sendValidateCode]);

return (
<Modal
shouldHandleNavigationBack={shouldHandleNavigationBack}
type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED}
isVisible={isVisible}
onClose={hide}
onModalHide={onModalHide ?? hide}
onBackdropPress={() => Navigation.dismissModal()}
onClose={onClose}
onBackdropPress={onClose}
shouldUseModalPaddingStyle={false}
animationInTiming={disableAnimation ? 1 : undefined}
>
<ScreenWrapper
includeSafeAreaPaddingBottom
includePaddingTop
shouldEnableMaxHeight
testID={ValidateCodeActionModal.displayName}
offlineIndicatorStyle={themeStyles.mtAuto}
>
<HeaderWithBackButton
title={title}
onBackButtonPress={hide}
threeDotsMenuItems={threeDotsMenuItems}
shouldShowThreeDotsButton={threeDotsMenuItems.length > 0}
shouldOverlayDots
onThreeDotsButtonPress={onThreeDotsButtonPress}
/>

<ScrollView
style={[styles.w100, styles.h100, styles.flex1]}
contentContainerStyle={styles.flexGrow1}
keyboardShouldPersistTaps="handled"
>
<View style={[themeStyles.ph5, themeStyles.mt3, themeStyles.mb5, themeStyles.flex1]}>
<Text style={[themeStyles.mb3]}>{descriptionPrimary}</Text>
{!!descriptionSecondary && <Text style={[themeStyles.mb3]}>{descriptionSecondary}</Text>}
<ValidateCodeForm
isLoading={isLoading}
validatePendingAction={validatePendingAction}
validateCodeActionErrorField={validateCodeActionErrorField}
validateError={validateError}
handleSubmitForm={handleSubmitForm}
sendValidateCode={sendValidateCode}
clearError={clearError}
buttonStyles={[themeStyles.justifyContentEnd, themeStyles.flex1]}
ref={validateCodeFormRef}
/>
</View>
</ScrollView>
{footer?.()}
</ScreenWrapper>
<ValidateCodeActionContent
title={title}
descriptionPrimary={descriptionPrimary}
descriptionSecondary={descriptionSecondary}
validateCodeActionErrorField={validateCodeActionErrorField}
handleSubmitForm={handleSubmitForm}
clearError={clearError}
onClose={onClose}
sendValidateCode={sendValidateCode}
validateError={validateError}
validatePendingAction={validatePendingAction}
threeDotsMenuItems={threeDotsMenuItems}
onThreeDotsButtonPress={onThreeDotsButtonPress}
isLoading={isLoading}
/>
</Modal>
);
}
Expand Down
32 changes: 13 additions & 19 deletions src/components/ValidateCodeActionModal/type.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import type React from 'react';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon';

type ValidateCodeActionModalProps = {
/** Whether the modal is visible */
isVisible: boolean;

type ValidateCodeActionContentProps = {
/** Title of the modal */
title: string;

Expand All @@ -18,9 +14,6 @@ type ValidateCodeActionModalProps = {
/** Function to call when the user closes the modal */
onClose?: () => void;

/** Function to be called when the modal is closed */
onModalHide?: () => void;

/** The pending action we're trying to validate */
validatePendingAction?: PendingAction;

Expand All @@ -36,27 +29,28 @@ type ValidateCodeActionModalProps = {
/** Function to clear error of the form */
clearError: () => void;

/** A component to be rendered inside the modal */
footer?: () => React.JSX.Element;

/** Function is called when validate code modal is mounted and on magic code resend */
sendValidateCode: () => void;

/** Whether the form is loading or not */
isLoading?: boolean;

/** Whether handle navigation back when modal show. */
shouldHandleNavigationBack?: boolean;

/** Whether disable the animations */
disableAnimation?: boolean;

/** List of menu items for more(three dots) menu */
threeDotsMenuItems?: PopoverMenuItem[];

/** Method to trigger when pressing more options button of the header */
onThreeDotsButtonPress?: () => void;
};

// eslint-disable-next-line import/prefer-default-export
export type {ValidateCodeActionModalProps};
type ValidateCodeActionModalProps = ValidateCodeActionContentProps & {
/** Whether the modal is visible */
isVisible: boolean;

/** Whether handle navigation back when modal show. */
shouldHandleNavigationBack?: boolean;

/** Whether disable the animations */
disableAnimation?: boolean;
};

export type {ValidateCodeActionContentProps, ValidateCodeActionModalProps};
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
require<ReactComponentModule>('../../../../pages/workspace/accounting/intacct/import/SageIntacctAddUserDimensionPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_EDIT_USER_DIMENSION]: () =>
require<ReactComponentModule>('../../../../pages/workspace/accounting/intacct/import/SageIntacctEditUserDimensionsPage').default,
[SCREENS.SETTINGS.DELEGATE.VERIFY_ACCOUNT]: () => require<ReactComponentModule>('../../../../pages/settings/Security/AddDelegate/VerifyAccountPage').default,
[SCREENS.SETTINGS.DELEGATE.ADD_DELEGATE]: () => require<ReactComponentModule>('../../../../pages/settings/Security/AddDelegate/AddDelegatePage').default,
[SCREENS.SETTINGS.DELEGATE.DELEGATE_ROLE]: () => require<ReactComponentModule>('../../../../pages/settings/Security/AddDelegate/SelectDelegateRolePage').default,
[SCREENS.SETTINGS.DELEGATE.UPDATE_DELEGATE_ROLE]: () =>
Expand Down
12 changes: 6 additions & 6 deletions src/libs/Navigation/Navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const navigationIsReadyPromise = new Promise<void>((resolve) => {
resolveNavigationIsReadyPromise = resolve;
});

let pendingRoute: Route | null = null;
let pendingNavigationCall: {route: Route; options?: LinkToOptions} | null = null;

let shouldPopToSidebar = false;

Expand Down Expand Up @@ -185,7 +185,7 @@ function navigate(route: Route, options?: LinkToOptions) {
// Store intended route if the navigator is not yet available,
// we will try again after the NavigationContainer is ready
Log.hmmm(`[Navigation] Container not yet ready, storing route as pending: ${route}`);
pendingRoute = route;
pendingNavigationCall = {route, options};
}
return;
}
Expand Down Expand Up @@ -455,12 +455,12 @@ function getRouteNameFromStateEvent(event: EventArg<'state', false, NavigationCo
* but the NavigationContainer was not ready when navigate() was called
*/
function goToPendingRoute() {
if (pendingRoute === null) {
if (pendingNavigationCall === null) {
return;
}
Log.hmmm(`[Navigation] Container now ready, going to pending route: ${pendingRoute}`);
navigate(pendingRoute);
pendingRoute = null;
Log.hmmm(`[Navigation] Container now ready, going to pending route: ${pendingNavigationCall.route}`);
navigate(pendingNavigationCall.route, pendingNavigationCall.options);
pendingNavigationCall = null;
}

function isNavigationReady(): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const SETTINGS_TO_RHP: Partial<Record<keyof SettingsSplitNavigatorParamList, str
SCREENS.SETTINGS.LOCK.LOCK_ACCOUNT,
SCREENS.SETTINGS.LOCK.UNLOCK_ACCOUNT,
SCREENS.SETTINGS.LOCK.FAILED_TO_LOCK_ACCOUNT,
SCREENS.SETTINGS.DELEGATE.VERIFY_ACCOUNT,
SCREENS.SETTINGS.DELEGATE.ADD_DELEGATE,
SCREENS.SETTINGS.DELEGATE.DELEGATE_ROLE,
SCREENS.SETTINGS.DELEGATE.UPDATE_DELEGATE_ROLE,
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 @@ -332,6 +332,10 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
path: ROUTES.SETTINGS_ADDRESS_STATE.route,
exact: true,
},
[SCREENS.SETTINGS.DELEGATE.VERIFY_ACCOUNT]: {
path: ROUTES.SETTINGS_DELEGATE_VERIFY_ACCOUNT,
exact: true,
},
[SCREENS.SETTINGS.DELEGATE.ADD_DELEGATE]: {
path: ROUTES.SETTINGS_ADD_DELEGATE,
exact: true,
Expand Down
1 change: 1 addition & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,7 @@ type SettingsNavigatorParamList = {
};
[SCREENS.TWO_FACTOR_AUTH.DISABLED]: undefined;
[SCREENS.TWO_FACTOR_AUTH.DISABLE]: undefined;
[SCREENS.SETTINGS.DELEGATE.VERIFY_ACCOUNT]: undefined;
[SCREENS.SETTINGS.DELEGATE.ADD_DELEGATE]: undefined;
[SCREENS.SETTINGS.DELEGATE.DELEGATE_ROLE]: {
login: string;
Expand Down
Loading
Loading