diff --git a/contributingGuides/NAVIGATION.md b/contributingGuides/NAVIGATION.md
index 2fd461eb2060..35b18e8b173b 100644
--- a/contributingGuides/NAVIGATION.md
+++ b/contributingGuides/NAVIGATION.md
@@ -13,6 +13,9 @@ The navigation in the app is built on top of the `react-navigation` library. To
- [Dismissing modals with opening a report](#dismissing-modals-with-opening-a-report)
- [Summary](#summary)
- [Adding new screens](#adding-new-screens)
+ - [Multi-step flows with URL synchronization](#multi-step-flows-with-url-synchronization)
+ - [When to use](#when-to-use)
+ - [Implementation pattern](#implementation-pattern)
- [Debugging](#debugging)
- [Reading state when it changes](#reading-state-when-it-changes)
- [Finding the code that calls the navigation function](#finding-the-code-that-calls-the-navigation-function)
@@ -396,7 +399,7 @@ export default NewSettingsScreen;
});
```
- Let's assume that we want to have PreferencesPage below our new Settings RHP screen.
+ Let's assume that we want to have PreferencesPage below our new Settings RHP screen.
```ts
// src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts
@@ -415,6 +418,230 @@ export default NewSettingsScreen;
export default SETTINGS_TO_RHP;
```
+## Multi-step flows with URL synchronization
+
+Multi-step flows (wizards, forms with multiple screens) should use URL-based navigation via the `useSubPage` hook or via basic navigation between plain static routes. This approach ensures browser navigation works correctly and page refreshes preserve the current position.
+
+### When to use
+
+You can use `useSubPage` hook for any multi-step flow where:
+- Users progress through a series of screens to complete a task
+- Each step collects or displays different information
+- The flow has a final confirmation or summary step
+- You need proper browser back/forward button support
+- Page refresh should preserve the user's current position
+
+Common examples include:
+- Account setup wizards
+- Form flows with multiple sections
+- Settings configuration flows
+
+### Implementation pattern
+
+#### 1. Define your routes
+
+Add routes with a `subPage` parameter to `ROUTES.ts`. The `action` parameter is used for edit mode:
+
+```ts
+MY_FLOW: {
+ route: 'my-flow/:subPage?/:action?',
+ getRoute: (subPage?: string, action?: 'edit') => {
+ if (!subPage) {
+ return 'my-flow' as const;
+ }
+ return `my-flow/${subPage}${action ? `/${action}` : ''}` as const;
+ },
+},
+```
+
+#### 2. Add navigation config
+
+Register the screen in `linkingConfig/config.ts`:
+
+```ts
+[SCREENS.MY_FLOW]: {
+ path: ROUTES.MY_FLOW.route,
+ exact: true,
+},
+```
+
+#### 3. Define page constants
+
+Add page name constants to `CONST.ts`:
+
+```ts
+MY_FLOW: {
+ STEP_INDEX_LIST: ['1', '2', '3'],
+ PAGE_NAME: {
+ STEP_ONE: 'step-one',
+ STEP_TWO: 'step-two',
+ CONFIRMATION: 'confirmation',
+ },
+},
+```
+
+#### 4. Create sub-page components
+
+Each sub-page receives `SubPageProps` and any custom props you define:
+
+```ts
+import type {SubPageProps} from '@hooks/useSubPage/types';
+
+type CustomSubPageProps = SubPageProps & {
+ // Add custom props specific to your flow
+ formValues: MyFormType;
+};
+
+function StepOne({isEditing, onNext, onMove, formValues}: CustomSubPageProps) {
+ const handleSubmit = (data: FormData) => {
+ saveData(data);
+ onNext();
+ };
+
+ return (
+
+ {/* Form fields */}
+
+ );
+}
+```
+
+#### 5. Implement the main flow component
+
+```ts
+import useSubPage from '@hooks/useSubPage';
+import type {SubPageProps} from '@hooks/useSubPage/types';
+import InteractiveStepSubPageHeader from '@components/InteractiveStepSubPageHeader';
+
+type CustomSubPageProps = SubPageProps & {
+ formValues: MyFormType;
+};
+
+const pages = [
+ {pageName: CONST.MY_FLOW.PAGE_NAME.STEP_ONE, component: StepOne},
+ {pageName: CONST.MY_FLOW.PAGE_NAME.STEP_TWO, component: StepTwo},
+ {pageName: CONST.MY_FLOW.PAGE_NAME.CONFIRMATION, component: Confirmation},
+];
+
+function MyFlowContent() {
+ const {
+ CurrentPage,
+ isEditing,
+ pageIndex,
+ prevPage,
+ nextPage,
+ lastPageIndex,
+ moveTo,
+ resetToPage,
+ } = useSubPage({
+ pages,
+ startFrom: 0,
+ onFinished: () => {
+ // Handle flow completion
+ Navigation.goBack();
+ },
+ buildRoute: (pageName, action) => ROUTES.MY_FLOW.getRoute(pageName, action),
+ });
+
+ const handleBackButtonPress = () => {
+ if (isEditing) {
+ Navigation.goBack();
+ return;
+ }
+
+ if (pageIndex === 0) {
+ Navigation.closeRHPFlow();
+ return;
+ }
+
+ prevPage();
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+```
+
+### Using InteractiveStepSubPageHeader
+
+The `InteractiveStepSubPageHeader` component is designed to work with the `useSubPage` hook for URL-based multi-step flows.
+
+Key features:
+- Displays numbered step indicators with connecting lines
+- Shows completed steps with a checkmark icon
+- Allows users to tap on completed steps to navigate back (entering edit mode)
+- Locked (future) steps are disabled
+- Automatically syncs with the current page index from the URL
+
+```ts
+
+```
+
+> **Note**: The `stepNames` array determines the number of steps displayed. The `currentStepIndex` is 0-based. When a user taps a completed step, `onStepSelected` is called with that step's index, which triggers edit mode navigation.
+
+#### 6. Handle edit mode
+
+The hook automatically manages edit mode via the `action=edit` URL parameter. When users navigate to a previous step using `moveTo()`, they enter edit mode. In edit mode, calling `nextPage()` returns them to the last page (typically confirmation) instead of advancing sequentially:
+
+```ts
+// In Confirmation component - allow editing previous steps
+function Confirmation({onMove, formValues}: CustomSubPageProps) {
+ return (
+
+
+ );
+}
+```
+
+#### 7. Skip pages conditionally
+
+Use `skipPages` to conditionally skip steps based on user input or feature flags:
+
+```ts
+const skipPages = useMemo(() => {
+ const pagesToSkip: string[] = [];
+ if (!requiresAdditionalInfo) {
+ pagesToSkip.push(CONST.MY_FLOW.PAGE_NAME.STEP_TWO);
+ }
+ return pagesToSkip;
+}, [requiresAdditionalInfo]);
+
+const {...} = useSubPage({
+ pages,
+ skipPages,
+ // ...other props
+});
+```
+
## Debugging
### Reading state when it changes
diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 2b6e2f223bae..bdba0e164ebb 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -2684,6 +2684,16 @@ const CONST = {
VENDOR_BILL: 'VENDOR_BILL',
},
+ MISSING_PERSONAL_DETAILS: {
+ STEP_INDEX_LIST: ['1', '2', '3', '4'],
+ PAGE_NAME: {
+ LEGAL_NAME: 'legal-name',
+ DATE_OF_BIRTH: 'date-of-birth',
+ ADDRESS: 'address',
+ PHONE_NUMBER: 'phone-number',
+ CONFIRM: 'confirm',
+ },
+ },
MISSING_PERSONAL_DETAILS_INDEXES: {
MAPPING: {
LEGAL_NAME: 0,
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 50d6749e940f..f9fc0d98ab80 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -3153,7 +3153,15 @@ const ROUTES = {
route: 'restricted-action/workspace/:policyID',
getRoute: (policyID: string) => `restricted-action/workspace/${policyID}` as const,
},
- MISSING_PERSONAL_DETAILS: 'missing-personal-details',
+ MISSING_PERSONAL_DETAILS: {
+ route: 'missing-personal-details/:subPage?/:action?',
+ getRoute: (subPage?: string, action?: 'edit') => {
+ if (!subPage) {
+ return 'missing-personal-details' as const;
+ }
+ return `missing-personal-details/${subPage}${action ? `/${action}` : ''}` as const;
+ },
+ },
MISSING_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE: 'missing-personal-details/confirm-magic-code',
POLICY_ACCOUNTING_NETSUITE_SUBSIDIARY_SELECTOR: {
route: 'workspaces/:policyID/accounting/netsuite/subsidiary-selector',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 739c5ac47279..f7f882f4e73a 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -860,7 +860,7 @@ const SCREENS = {
TRANSACTION_RECEIPT: 'TransactionReceipt',
FEATURE_TRAINING_ROOT: 'FeatureTraining_Root',
RESTRICTED_ACTION_ROOT: 'RestrictedAction_Root',
- MISSING_PERSONAL_DETAILS_ROOT: 'MissingPersonalDetails_Root',
+ MISSING_PERSONAL_DETAILS: 'MissingPersonalDetails',
MISSING_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE: 'MissingPersonalDetails_ConfirmMagicCode',
ADD_UNREPORTED_EXPENSES_ROOT: 'AddUnreportedExpenses_Root',
DEBUG: {
diff --git a/src/components/InteractiveStepSubPageHeader.tsx b/src/components/InteractiveStepSubPageHeader.tsx
new file mode 100644
index 000000000000..66b5762490c6
--- /dev/null
+++ b/src/components/InteractiveStepSubPageHeader.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import type {ViewStyle} from 'react-native';
+import {View} from 'react-native';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import useThemeStyles from '@hooks/useThemeStyles';
+import colors from '@styles/theme/colors';
+import variables from '@styles/variables';
+import CONST from '@src/CONST';
+import Icon from './Icon';
+import PressableWithFeedback from './Pressable/PressableWithFeedback';
+import Text from './Text';
+
+type InteractiveStepSubPageHeaderProps = {
+ /** List of step names to display */
+ stepNames: readonly string[];
+
+ /** Current step index (0-based) */
+ currentStepIndex: number;
+
+ /** Function to call when a step is selected */
+ onStepSelected?: (stepIndex: number) => void;
+};
+
+const MIN_AMOUNT_FOR_EXPANDING = 3;
+const MIN_AMOUNT_OF_STEPS = 2;
+
+function InteractiveStepSubPageHeader({stepNames, currentStepIndex, onStepSelected}: InteractiveStepSubPageHeaderProps) {
+ const styles = useThemeStyles();
+ const icons = useMemoizedLazyExpensifyIcons(['Checkmark']);
+ const containerWidthStyle: ViewStyle = stepNames.length < MIN_AMOUNT_FOR_EXPANDING ? styles.mnw60 : styles.mnw100;
+
+ if (stepNames.length < MIN_AMOUNT_OF_STEPS) {
+ throw new Error(`stepNames list must have at least ${MIN_AMOUNT_OF_STEPS} elements.`);
+ }
+
+ const lastStepIndex = stepNames.length - 1;
+
+ const handleStepPress = (isLockedStep: boolean, index: number) => {
+ if (isLockedStep || !onStepSelected) {
+ return;
+ }
+ onStepSelected(index);
+ };
+
+ return (
+
+ {stepNames.map((stepName, index) => {
+ const isCompletedStep = currentStepIndex > index;
+ const isLockedStep = currentStepIndex < index;
+ const isLockedLine = currentStepIndex < index + 1;
+ const hasConnectingLine = index < lastStepIndex;
+
+ return (
+
+ handleStepPress(isLockedStep, index)}
+ accessible
+ accessibilityLabel={stepName}
+ role={CONST.ROLE.BUTTON}
+ >
+ {isCompletedStep ? (
+
+ ) : (
+ {index + 1}
+ )}
+
+ {hasConnectingLine ? : null}
+
+ );
+ })}
+
+ );
+}
+
+export default InteractiveStepSubPageHeader;
diff --git a/src/components/ReportActionItem/IssueCardMessage.tsx b/src/components/ReportActionItem/IssueCardMessage.tsx
index 215038f2d9c3..9adb3619602b 100644
--- a/src/components/ReportActionItem/IssueCardMessage.tsx
+++ b/src/components/ReportActionItem/IssueCardMessage.tsx
@@ -45,7 +45,7 @@ function IssueCardMessage({action, policyID}: IssueCardMessageProps) {
${getCardIssuedMessage({reportAction: action, shouldRenderHTML: true, policyID, expensifyCard, companyCard, translate})}`} />
{shouldShowAddMissingDetailsButton && (