Concierge AI Feature Promo Modal v2#94012
Conversation
…ature-promo-modal" This reverts commit 9586f96.
`openSearchRouter` was pushing the `{isSearchModalOpen: true}` history entry
synchronously, before `close()` finished dismissing any pre-existing modals.
When the AI promo modal closed it ran `Navigation.goBack()` (default
behavior of `FeatureTrainingModal` via `shouldGoBack=true`), which popped
the just-pushed history entry. The resulting `popstate` event fired before
the close callback finished, immediately marking the router as closed.
Move the `pushState` inside the `close()` callback so the history entry is
added only after pre-existing modals' close paths (including any history-
mutating `goBack`) have settled.
Fixes Expensify#93964
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The carousel path bypassed the ScrollView Wrapper that the single-page path of FeatureTrainingModal uses, so in landscape (where the modal is hard-capped at 75% of window height via MODAL_MAX_HEIGHT_TO_WINDOW_HEIGHT_RATIO_LANDSCAPE_MODE) there was no scroller to make the buttons reachable when the illustration consumed most of the cap. Lift the Wrapper (ScrollView when shouldUseScrollView, else View) out so both branches share it, and remove the now-redundant landscape maxHeight and wrapperStyles from FeatureTrainingModalCarouselBody. Also drop `isCarousel` from the keyboard scroll-to-end early-return so the carousel benefits from the same keyboard behavior. Fixes Expensify#93968 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On narrow viewports (mWeb BOTTOM_DOCKED) the carousel's outer View had no intrinsic content (everything inside is gated on `carouselViewportWidth > 0`) and no width style, so inside the modal sheet's `fit-content` container it collapsed to 0×0. `onLayout` never fired with a positive width, the carousel never rendered, and the user saw the modal backdrop only. Add `styles.w100` to the outer View on narrow screens so it stretches to the sheet's full width and `onLayout` resolves immediately. The explicit `getWidthStyle(width)` continues to apply on medium+ screens. Fixes Expensify#93969 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
react-native-web translates FlatList's `pagingEnabled` to CSS scroll-snap, but it only applies `scroll-snap-align: start` to the scroller's direct children. Our per-page <View> rendered inside `renderItem` is not a direct child of the FlatList's internal scroll container, so the carousel ended up with a single snap point. A fast fling on mWeb/desktop Chrome could coast past page 2 of 3 onto the last page. Add `scrollSnapAlign: 'start'` to each page wrapper via a web-gated style so every page becomes a snap point. Add `disableIntervalMomentum`, `snapToInterval`, and `decelerationRate="fast"` to the FlatList as defense in depth on native — no-ops on web, but they arrest momentum precisely at each page boundary on iOS/Android. Fixes Expensify#93970 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`AIFeaturesPromoGuard` had no copilot/delegate awareness. When the host user switched into another account via Copilot, `clearOnyxForDelegateTransition` wiped Onyx using `KEYS_TO_PRESERVE_DELEGATE_ACCESS` — which does NOT preserve `NVP_DISMISSED_PRODUCT_TRAINING`. The guard then saw the dismissed NVP as empty and re-prompted the host user's one-time AI promo for every copilot transition. Subscribe to `ONYXKEYS.ACCOUNT`, track `isActingAsDelegate` via `isActingAsDelegateSelector`, and gate `isEligibleToShowAIFeaturesPromoModal` on `!isActingAsDelegate`. Mirrors the same short-circuit in `ProductTrainingContext`. Fixes Expensify#93973 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`shouldBlockWhileModalActive` previously blocked every navigation action except DISMISS_MODAL and GO_BACK while the AI promo modal was on top. External top-level intents — native share targeting SHARE_MODAL_NAVIGATOR, report deep-links arriving as RESET via getAdaptedStateFromPath — were silently dropped, stranding the user on the modal until they manually closed it. The block's intent is only to prevent an in-app tab swipe from racing the modal overlay. Switch the logic accordingly: - Always allow DISMISS_MODAL / GO_BACK. - Always allow RESET (deep links / share intents). - For NAVIGATE / PUSH / REPLACE, only block when `payload.name` targets the AI promo navigator itself; targets pointing at any sibling navigator/screen proceed and the new screen overlays or replaces the modal naturally. Fixes Expensify#93976 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
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!
|
This reverts commit 6b39918.
|
🚧 tgolen has triggered a test Expensify/App build. You can view the workflow run here. |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/tgolen in version: 9.4.33-0 🚀
|
|
🤖 Help site review: no changes required. This PR (#94012) adds a frontend-only "Concierge AI Feature Promo Modal" that promotes three existing AI features and links each to a help site article via Verification of each linked article
Each URL slug maps 1:1 to an existing markdown file under If you believe any of these articles needs updating to reflect changes shipped alongside this modal (e.g. new copy, renamed features, or behavior differences), reply with the specifics and I'll open a draft docs PR. @gijoe0295, please confirm you agree no help site changes are needed here. If you'd like a docs PR for any of the linked articles, let me know what to update. |
|
@MelvinBot in as brief as a summary as you can, can you outline the logic for which users will see this and when once it goes to production. |
|
The modal auto-opens once per user, right after the app finishes loading, for essentially all signed-in NewDot users — except delegates and users who are mid-migration/onboarding. It's gated by an NVP, not a beta, so once dismissed it never shows again (synced across devices). A user sees it when ALL of these hold (logic in
When / where it fires: it waits for in-flight navigation transitions to settle and for protected routes to be ready, then navigates from whatever screen you're on — and it skips firing if you're currently on the Share modal or a Not-found page (L86-L109). No beta gate on seeing it. The only beta involvement is content: the 3rd "Build your own agents" page renders only for users with the |
|
Deploy Blocker #95853 was identified to be related to this PR. |
|
Deploy Blocker ##95856 was identified to be related to this PR |
|
Deploy Blocker #95869 was identified to be related to this PR. |
Explanation of Change
Second attempt to add promo modal for new AI features:
Fixed Issues
$ #90820
$ #93964
$ #93968
$ #93969
$ #93970
$ #93973
$ #93976
$ #93985
PROPOSAL:
Tests
BackandNextbuttons navigate through the pagesCMD + KLet's go/x/tapping outside dismisses the modalNative only:
19. Sign in another account that has not seen the modal
20. While the modal is showing, background the app
21. Open device gallery and share a photo to Expensify app
22. Verify promo modal is dismissed and share page appears
Offline tests
NA
QA Steps
Same as Tests
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, 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.ScrollViewcomponent to make it scrollable when more elements are added to the page.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
Screen.Recording.2026-07-02.at.22.37.22.mov
Android: mWeb Chrome
iOS: Native
Screen.Recording.2026-06-26.at.06.19.19.mov
iOS: mWeb Safari
Screen.Recording.2026-06-26.at.06.21.26.mov
MacOS: Chrome / Safari
Screen.Recording.2026-06-26.at.06.18.07.mov