[Submit] In-product modal for existing "Get paid back" intent users (Wave 4) - #95099
Conversation
… users Introduces a guard-triggered, dismiss-once modal (behind the SUBMIT_2026 beta) for users who previously selected the "Get paid back" (EMPLOYER) onboarding intent but are not on any paid workspace. "Get the free plan" creates a Submit workspace via useAutoCreateSubmitWorkspace and routes to Categories with the #admins room in the RHP; "No thanks" dismisses it permanently via the DismissedProductTraining NVP.
|
Hey, I noticed you changed If you want to automatically generate translations for other locales, an Expensify employee will have to:
Alternatively, if you are an external contributor, you can run the translation script locally with your own OpenAI API key. To learn more, try running: npx ts-node ./scripts/generateTranslations.ts --helpTypically, you'd want to translate only what you changed by running |
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
Update the title/intro copy and add the four-item benefits bullet list
("Get reimbursed faster", "Build expense reports in seconds", "Categorize
your expenses", "Invite your boss when you're ready") to match the design
mockup, across all supported languages.
Per the design doc, the modal targets the legacy cohort who selected the "Get paid back" intent before the Submit workspace existed and are therefore "not on a workspace". Gate on getGroupPoliciesWhereReportCanBeCreated (paid Team/Corporate AND free Submit workspaces) instead of only paid policies, so users who already own a Submit workspace - including anyone who just completed the EMPLOYER onboarding flow - are excluded. This also removes the transient NotFound /home flash caused by the guard racing the onboarding navigation.
- Persist "modal shown" via new nvp_submitMigrationModalShown NVP (SetNameValuePair) instead of the product-training NVP, per BE decision - Skip CompleteGuidedSetup for already-onboarded users so the modal closes immediately on "Get the free plan" - Use the ReceiptWrangler lasso illustration on a yellow panel to match the design
Add SUBMIT_PLAN_MODAL_NAVIGATOR to MODAL_ROUTES_TO_DISMISS so that Navigation.dismissModal() actually pops the modal when the user taps "Get the free plan", instead of leaving it on screen while navigating to Categories.
|
it looks like the backend doesn't preserve the choice value yet ... am i passing the values right? @iwiznia
|
|
also @garrettmknight let me know when the new illustration is ready so that i can use it |
|
Also, I have another question ... given that a user does the following steps
now should the |
I'm not sure what I am looking there exactly, if your refer that the NVP is not returned in openapp nor updates are sent to it, that's because it was deployed: 6 hours ago (Jul 3, 2026, 6:33 AM GMT+2) |
When the guard proactively opens the modal on boot, the active route can be a screen that isn't a valid entry screen for the submit-plan-welcome dynamic route (e.g. /settings/troubleshoot after "Clear cache and restart"), which made the route resolve to NotFound. Because the modal never mounted, it also never persisted the "shown" NVP and kept re-triggering. Fall back to HOME when the active route isn't an allowed entry screen so the modal renders correctly and records that it was seen.
|
@iwiznia thanks! but it still doesn't work even after the deploy .. repro:
i'm setting it via SetNameValuePair with name: can you check if it's being persisted/returned on the BE side? Screen.Recording.2026-07-05.at.12.26.54.mov |
|
@abzokhattab illustration has been added here, but hasn't deployed yet. |
|
great thanks for tackling that ... i will get back on this once this BE is handled #95099 (comment) |
That's the problem. The NVPs in onyx are prefixed with |
The backend NVP is named `submitMigrationModalShown`; the `nvp_` prefix is an Onyx-key-only convention. Sending the prefixed key as the API name meant the value was never persisted or returned in OpenApp, so the modal kept re-triggering. Send the unprefixed name to the API while still storing the value under the prefixed Onyx key.
…odal-87912 # Conflicts: # src/CONST/index.ts # src/NAVIGATORS.ts # src/ROUTES.ts # src/SCREENS.ts # src/languages/en.ts # src/libs/Navigation/AppNavigator/AuthScreens.tsx # src/libs/Navigation/guards/index.ts # src/libs/Navigation/linkingConfig/config.ts # src/libs/Navigation/types.ts
Coalesce the guard's proactive-navigation decision onto a microtask so it runs once after the OpenApp Onyx batch fully settles, preventing a callback-ordering race where a stale hasShownSubmitMigrationModal reopened the modal for users who already saw it. Data subscriptions now only feed the cache; the one-shot redirect is driven solely by the app-load/session signal. Also swaps the modal illustration to ReceiptWranglerSpaceCowgirl with the correct aspect ratio and inset padding.
Drop the unused shouldShowSubmitPlanWelcomeModal export and add a UI test asserting setSubmitMigrationModalShown fires when the modal is removed from the navigation stack.
Gate the submit plan welcome decision on HAS_LOADED_APP so it waits for OpenApp to deliver this session's account data (including the shown-flag). This closes the sign-in race where SignInUser authenticates and delivers the eligibility NVPs before OpenApp fetches nvp_submitMigrationModalShown, which made the modal reappear for users who had already seen it. Also removes the debug/info logs from the guard and modal and drops the onboarding-completion warn log (the catch now swallows with an explanation).
|
🚧 iwiznia has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/iwiznia in version: 9.4.34-0 🚀
|
|
Deploy Blocker #96121 was identified to be related to this PR. |
|
Deploy Blocker #96123 was identified to be related to this PR. |
|
@hungvu193 @abzokhattab @iwiznia @tgolen Is this one testable by Applause? Submit2026 beta is enabled for applause.expensifail account and onboarding options are not displayed for the same. Any workaround for this? |
|
Could this be tested internally? The QA team doesn't have access to verify this on our side. |
Yeah
What are you seeing? What options are not displayed or are different? |
|
I think to test, you'll need an existing account, pre-Submit testing with the
|
|
@iwiznia Tried with expensifail account, No onboarding indents for applause.expensifail domain. I created a submit workspace in OldDot and deleted the same. No "Get paid back, for free" modal displayed. Am I missing anything here? Please bear with the long video. It was necessary to capture the complete behavior. Recording.3215.mp4 |
|
Hmmmm weird, not really sure why you are not seeing the onboarding modal... |
I am not seeing Submit workspace by logging in to applausetester+tc3255member1777185572988@applause.expensifail.com Recording.3218.mp4 |
|
Hmmm I think @garrettmknight is right and you need an existing account?
Which I think it's an existing account that selected submit to my employer before the beta? @abzokhattab can you confirm? |
|
🚀 Deployed to production by https://github.com/roryabraham in version: 9.4.34-14 🚀
|
|
🚀 Deployed to production by https://github.com/roryabraham in version: 9.4.34-14 🚀
Bundle Size Analysis (Sentry): |
|
I queried for the earliest applause user with the correct intent and it doesn't look like that login is part of any workspaces. |



Explanation of Change
Wave 4 of the Submit plan project (#87912). Adds an in-product welcome modal that nudges existing users who previously selected the "Get paid back" (
EMPLOYER) onboarding intent but are not an owner or member of any paid workspace to adopt the free Submit plan.The whole feature is gated behind the
SUBMIT_2026beta and mirrors the existingMigratedUserWelcomeModalarchitecture: a centered modal in its own navigator, proactively shown by a navigation guard, and dismissed once via theDismissedProductTrainingNVP.Behavior:
useAutoCreateSubmitWorkspace(firstName, lastName), which creates aSUBMITworkspace, completes onboarding (producing the#adminsConcierge welcome message), and routes the user to Workspace → Categories with#adminsopen in the RHP.Trigger conditions (all required): authenticated and app not loading,
SUBMIT_2026beta enabled,introSelected.choice === EMPLOYER, onboarding completed, no paid (Team/Corporate) policy, and not previously dismissed.Main changes:
SubmitPlanWelcomeModalcomponent,SubmitPlanWelcomeModalNavigator, andSubmitPlanWelcomeModalGuard(registered inguards/index.ts).SCREENS,NAVIGATORS,ROUTES(submit-plan-welcomedynamic route), navigation param-list types,linkingConfig, and aRootStack.ScreeninAuthScreens.SUBMIT_PLAN_WELCOME_MODALconstant +DismissedProductTrainingtype entry.Fixed Issues
$ #87912
PROPOSAL:
Tests
Enable the
SUBMIT_2026beta for your account.Use an eligible account: chose "Get paid back by my employer" during onboarding, finished onboarding, and is not in any group workspace — either log in with an account that has no workspace, or delete all existing workspaces.
Open the app. The "Get paid back, for free" modal should appear automatically.
Tap Get the free plan and verify a Submit workspace is created and you are routed to Workspace → Categories.
Reload the app and verify the modal does not appear again.
Repeat with another eligible account, tap No thanks (or backdrop / ESC), and verify the modal closes and does not reappear after reload.
Verify that no errors appear in the JS console
Offline tests
QA Steps
Same as tests.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)Avatar, 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
Android: mWeb Chrome
iOS: Native
Screen.Recording.2026-07-11.at.21.53.45.mov
iOS: mWeb Safari
Screen.Recording.2026-07-11.at.21.56.04.mov
MacOS: Chrome / Safari
Screen.Recording.2026-07-11.at.21.35.10.mov
Screen.Recording.2026-07-11.at.21.51.14.mov