Book Travel enablement flow has no progress stepper #95692
Book Travel enablement flow has no progress stepper #95692linhvovan29546 wants to merge 25 commits into
Conversation
- TermsStep: use current expensify-common Str import - VerifyAccountStep: use lastValidateCodeRequestedAt hydration guard (validateCodeSent no longer exists) and match current validateSecondaryLogin(contactMethod, validateCode) signature
… stepper BookTravelButton now hands off entirely to the new EnableTravel stepper for self-serve provisioning, so the old cascade of standalone screens (domain selector, workspace address, legal-entity tax ID, terms) and the nextStepRoute hand-off mechanism they relied on are dead code. Simplify VerifyAccountPage to drop its now-unreachable self-serve forward-navigation branch (kept only for the legacy beta-off request-access path). Also fixes a real bug in the ported stepper: WorkspaceAddressStep's country picker navigates through the ADDRESS_COUNTRY dynamic route, which is gated by an entryScreens allowlist that didn't include the new stepper's screen - country selection would have silently failed. Also carries forward a terms error-handling fix that landed on main after the reference branch was cut (clearing the provisioning session before showing the verify-company modal).
EnableTravelContentTest verifies the core acceptance criteria: the step count reflects only what a given workspace actually needs (single-step flows hide the progress bar, provisioned workspaces skip domain/address/tax ID regardless of currency or admin domain count, unprovisioned non-USD workspaces with multiple domains and no address need every step). TaxIDStepTest replaces the coverage lost when TravelLegalEntityTaxIDPageTest was deleted along with the old standalone tax ID page.
Codecov Report❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.
|
- DomainSelectorStep: migrate from the deprecated default Button import to the composed ButtonComposed API (no-restricted-imports) - EnableTravelContentTest: use computed object keys for admin email test fixtures instead of literal email-like property names, and disable no-unsafe-type-assertion for the require() casts in the RenderHTML mock (matching the pattern used elsewhere in this test suite) - Reword "unprovisioned" to "not-yet-provisioned" in a comment and a test name; cspell doesn't recognize the un- prefixed form
…pper Each step in the enablement stepper is a separate navigation push (a fresh mount, not an in-place update), and completing a step flips the very Onyx flag that decided whether that step was included. Recomputing the step list on every mount caused the total step count to shrink as the user progressed, and made going back land on a mount with a mismatched step list. Persist the frozen step list to TRAVEL_PROVISIONING.enabledSteps instead, matching the existing pattern for other cross-step state in this flow (domain, taxID) - it's computed once on the first step and read back on every subsequent mount instead of recomputed, and reset by cleanupTravelProvisioningSession() at the start of every fresh session. Also fixes a regression introduced while chasing this: gating the whole screen on travelProvisioning's own loading metadata could leave it stuck on the loading indicator, since it's a plain optional session key with no guaranteed write on account creation. EnableTravelContent already treats "not loaded yet" and "no persisted enabledSteps" the same way, so the screen no longer waits on it.
…illed steps Restore the enabledSteps session snapshot on TRAVEL_PROVISIONING that the flash fix accidentally removed: each step is a fresh mount, so recomputing the step list per mount made completed steps vanish (e.g. legal name after back + Next). The list is frozen per flow session again, and hardened: the flow-entry mount (no subPage param yet) always recomputes and overwrites whatever is persisted, so a stale list from an abandoned session is never trusted. Also only turn on edit mode for stepper-dot presses coming from the last step. From any earlier step a dot press is plain navigation now, since edit mode's Next jumps straight to the last step and would skip steps the user hasn't filled in yet (address could be bypassed entirely).
|
This PR adds a new |
|
This should be ready for the first round of review. I'm still recording screenshots to complete the PR author checklist |
connectWithoutView is only used in the test file |
Same as #95692 (comment) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d6f8a5a89b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
Removing onyx expert review based off this comment |
|
Adding Shawn as a reviewer. @shawnborton as a reminder this PR is a result of this thread where we decided to make the travel enablement flow into a dynamic stepper. Can you look at the screenshots / videos to make sure it looks the way you thought it would? |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppAndroid: mWeb ChromeiOS: HybridAppiOS: mWeb SafariMacOS: Chrome / Safari |
|
There is an awkward delay after you confirm the centered modal and then it takes a moment for the RHP to close. Can that all close at the same time? CleanShot.2026-07-16.at.09.15.22.mp4 |
|
@linhvovan29546 Could you help review the comment above? |
@linhvovan29546 Could you merge main into your branch to fix this issue? |
Confirming the verify-company modal only resolved once its own dismiss animation finished, so the RHP close it triggers played strictly after the modal had already fully closed. Resolve immediately on tap and close the modal explicitly once the RHP-close/navigate kicks off, so both happen together instead of one waiting on the other.
|
This PR adds a new |
Fixed Screen.Recording.2026-07-17.at.14.52.34.mov |
Screen.Recording.2026-07-17.at.15.04.06.movBUG: When I open the Travel page and reload it, I'm unexpectedly redirected to the Workspace report chat in the background. |
@linhvovan29546 How can we clear the legal name? |
Run the following command in the console to clear the legal name: |
…Test This is a one-shot read in a test helper, not a React view, so the connectWithoutView/connect distinction doesn't apply - other tests (AttachmentTest, DelegateTest, etc.) already use plain connect() for this. connectWithoutView also unconditionally re-triggers the Onyx performance-reviewer request workflow on every push for the life of the PR, regardless of relevance.
I can fix this by using |
I think we can do something like:
But let's see what @Expensify/marketing thinks (cc @jamesdeanexpensify - waiting for copy label doesn't work on PRs?) |








Explanation of Change
This PR rebuilds the "Book Travel" workspace-enablement flow as a single dynamic multi-step flow with a progress stepper, replacing the old chain of separate full-screen pages.
New flow (
travel/enable/:policyID/:subPage?/:action?,src/pages/Travel/EnableTravel/):useSubPage+InteractiveStepSubPageHeader, so each step is URL-addressable and survives web refresh.TRAVEL_PROVISIONING.enabledSteps. Each step transition is a separate navigation push (a fresh mount), and completing a step flips the very Onyx flag that decided the step was included — without freezing, the step count would shrink out from under the user mid-flow (e.g. the legal name step vanishing after saving it and navigating back). The flow-entry mount (nosubPagein the URL yet) always recomputes and overwrites the persisted list, so a stale list from an abandoned session is never trusted.updateLegalNamegained ashouldGoBackparam: the standalone legal-name page still navigates back on save, but the stepper drives its own forward navigation, and the old built-ingoBack()raced with the stepper's push causing a visible flash.Removed pages/routes (the old per-screen flow is unreachable now that
BookTravelButtonroutes to the stepper):DynamicDomainSelectorPage.tsxdomain-selector(dynamic suffix)subPages/DomainSelectorStep.tsxWorkspaceAddressForTravelPage.tsxtravel/:domain/workspace-addresssubPages/WorkspaceAddressStep.tsxTravelLegalEntityTaxIDPage.tsxtravel/:domain/legal-entity-tax-idsubPages/TaxIDStep.tsxDynamicTravelTerms.tsxterms/:domain/accept/:policyID?subPages/TermsStep.tsxEach step keeps the old page's form/UI and submit logic. What was intentionally not carried over into the steps: the per-page screen chrome (
ScreenWrapper/HeaderWithBackButton/AccessOrNotFoundWrapper, now provided once by the stepper shell) and the hardcoded cross-screen routing — the OTP-validate-first gates and "where to go next" branching (setTravelProvisioningNextStep,getTravelAcceptTermsRoute) — which the stepper's computed step list replaces (account verification is its own step, and each step just callsonNext()).No
OldRoutesredirects were added for the deleted routes: a grep of the repo found no hardcoded references to them, they were client-driven wizard targets (not backend-generated links, unlike magic-code links which go throughVERIFY_ACCOUNT— kept), and two of them carriedpolicyIDas a query param whichOldRoutes.tscan't rewrite into the new path anyway. A stale bookmarked URL shows Not Found, and re-entering via Book travel resumes at the correct step with no data loss, since the stepper derives the steps from the same persisted state the old pages read.Unchanged (per the issue's explicit constraints):
IS_TRAVEL_VERIFIEDbeta off), allBookTravelButtonpre-checks (blocked-feature beta, phone/SMS login, public domain, no admin domains, upgrade, non-paid workspace, already-enabled → TravelDot), theAcceptSpotnanaTermsAPI command/params,WorkspaceTravelInvoicingSection, andVerifyAccountPagefor the legacy path.Unit tests cover the step composition per condition, the frozen step list across remounts, the stale-session overwrite on flow entry, and the Tax ID step.
Fixed Issues
$ #95649
PROPOSAL: N/A
Tests
Prerequisites: an account with the
IS_TRAVEL_VERIFIEDbeta on, admin of a paid group workspace that is not yet provisioned for travel. To vary the step conditions: clear/fill your legal name (Settings → Profile → Personal details), use an unvalidated vs. validated account, set/clear the workspace address (Workspace → Overview), and switch the workspace currency to non-USD (e.g. GBP) to exercise the Tax ID step.IS_TRAVEL_VERIFIEDbeta off and verify Book Travel uses the old request-access path (no stepper); verify the pre-checks still fire before the stepper (public-domain email → error screen, phone/SMS login → inline error, no paid workspace → upgrade screen, terms already accepted → TravelDot directly)Offline tests
Same as the Tests section above.
QA Steps
Prerequisites: an account with the
IS_TRAVEL_VERIFIEDbeta on, admin of a paid group workspace that is not yet provisioned for travel. To vary the step conditions: clear/fill your legal name (Settings → Profile → Personal details), use an unvalidated vs. validated account, set/clear the workspace address (Workspace → Overview), and switch the workspace currency to non-USD (e.g. GBP) to exercise the Tax ID step.Verify the enablement stepper opens on the Legal name step, with a progress bar showing one dot per needed step (Terms always last)
Submit a legal name and verify the flow advances to the next step, the completed step shows a checkmark, and the total dot count does not change
Press the header back button on a later step and verify it returns to the previous step; on the first step, verify it closes the flow
From a mid-flow step (e.g. Workspace address), tap the completed Legal name dot, then press Next — verify the flow continues sequentially to the following step (it must not jump to Terms and skip unfilled steps)
Walk forward to the Terms step, tap a completed dot, re-save that step, and verify Next returns you straight to Terms
Verify step composition per condition: Verify account appears only for an unvalidated account, Domain selector only with more than one admin private email domain, Workspace address only when the workspace has no address, Tax ID only for a non-USD workspace with no stored tax ID. When nothing is needed, verify the flow lands directly on Terms with no progress bar
On web, refresh mid-flow and verify you stay on the same step (the step is in the URL)
On the Terms step, verify Continue is disabled until the checkbox is checked; accept the terms and verify the flow closes and TravelDot opens
Turn the
IS_TRAVEL_VERIFIEDbeta off and verify Book Travel uses the old request-access path (no stepper); verify the pre-checks still fire before the stepper (public-domain email → error screen, phone/SMS login → inline error, no paid workspace → upgrade screen, terms already accepted → TravelDot directly)Verify that no errors appear in the JS console
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
telegram-cloud-document-5-6249039001768959679.mp4
Android: mWeb Chrome
telegram-cloud-document-5-6249039001768959680.mp4
iOS: Native
Screen.Recording.2026-07-15.at.11.53.49.mov
iOS: mWeb Safari
Screen.Recording.2026-07-15.at.11.21.58.mov
MacOS: Chrome / Safari
Screen.Recording.2026-07-15.at.11.08.08.mov