Fix Submit/Collect upgrade RHP to reflect the selected plan#93464
Conversation
Propagate the plan chosen in the Plan RHP to the Upgrade RHP and the upgrade action so display copy, pricing, and the upgrade targetType match the selected plan instead of always defaulting to Control. Co-authored-by: Abdelrahman Khattab <abzokhattab@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Code Review@MelvinBot — solid fix for the root cause in #93326. The approach of threading What works well
Issues / concerns1. Collect upgrade still shows Control subtitle copy (minor UX bug)
translate('workspace.upgrade.commonFeatures.note')That string is "Unlock our most powerful features, including:" — reads as Control copy, not Collect. Consider adding a 2. Non-beta Submit → Collect upgrade path not fixed The issue proposal called out fixing both 3. No unit test coverage for the new behavior
4. No validation of
5. Draft PR completeness Before marking ready for review:
Regression risk
VerdictApprove with minor changes — the core fix is correct and well-scoped for the reported bug (Submit workspace + Suggested test plan
|
- Show Collect-specific subtitle copy in GenericFeaturesView (collect.note in all 10 locales) - Whitelist upgradePlanType from the URL to only TEAM/CORPORATE - Add unit tests asserting upgradePlanType=team renders Collect title + Team pricing and upgradePlanType=corporate renders Control title + Corporate pricing Co-authored-by: Abdelrahman Khattab <abzokhattab@users.noreply.github.com>
🦜 Polyglot Parrot! 🦜Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues: View the translation diffdiff --git a/src/languages/de.ts b/src/languages/de.ts
index 59dd2e09a48..d4b20d22e42 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -6995,10 +6995,10 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und
commonFeatures: {
title: 'Upgrade auf den Control-Tarif',
collect: {
- title: 'Upgrade auf den Collect-Tarif',
+ title: 'Zum Collect-Tarif upgraden',
startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) =>
- `<muted-text>Der Collect-Tarif beginnt bei <strong>${formattedPrice}</strong> ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}. <a href="${learnMoreMethodsRoute}">Erfahre mehr</a> über unsere Tarife und Preise.</muted-text>`,
- note: 'Schalte wichtige Funktionen für dein Unternehmen frei, darunter:',
+ `<muted-text>Der Collect-Tarif beginnt bei <strong>${formattedPrice}</strong> ${hasTeam2025Pricing ? `pro Mitglied pro Monat.` : `pro aktivem Mitglied und Monat.`} <a href="${learnMoreMethodsRoute}">Erfahren Sie mehr</a> über unsere Tarife und Preise.</muted-text>`,
+ note: 'Schalten Sie essentielle Funktionen für den Betrieb Ihres Unternehmens frei, darunter:',
},
note: 'Schalte unsere leistungsstärksten Funktionen frei, darunter:',
benefits: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 18be0c27b65..e0aeedee2f8 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1753,7 +1753,6 @@ const translations: TranslationDeepObject<typeof en> = {
}
},
[CONST.NEXT_STEP.MESSAGE_KEY.WAITING_TO_MARK_AS_DONE]: (actor, actorType, _eta, _etaType) => {
- // eslint-disable-next-line default-case
switch (actorType) {
case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
return `Esperando a que <strong>tú</strong> lo marques como listo.`;
@@ -6979,9 +6978,9 @@ ${amount} para ${merchant} - ${date}`,
title: 'Mejorar al plan Controlar',
collect: {
title: 'Mejorar al plan Recopilar',
- startsAtFull: (learnMoreMethodsRoute, formattedPrice, hasTeam2025Pricing) =>
- `<muted-text>El plan Recopilar comienza desde <strong>${formattedPrice}</strong> ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`} <a href="${learnMoreMethodsRoute}">Más información</a> sobre nuestros planes y precios.</muted-text>`,
- note: 'Desbloquea las funciones esenciales para tu negocio, incluyendo:',
+ startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) =>
+ `<muted-text>El plan Recopilar empieza desde <strong>${formattedPrice}</strong> ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`} <a href="${learnMoreMethodsRoute}">Más información</a> sobre nuestros planes y precios.</muted-text>`,
+ note: 'Desbloquea funciones esenciales para gestionar tu negocio, como:',
},
note: 'Desbloquea nuestras funciones más potentes, incluyendo:',
benefits: {
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 54d928aef65..572e56cd864 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -1,6 +1,7 @@
/**
* _____ __ __
* / ___/__ ___ ___ _______ _/ /____ ___/ /
+ * / (_ / -_) _ \/ -_) __/ _ \`/ __/ -_) _ /
* \___/\__/_//_/\__/_/ \_,_/\__/\__/\_,_/
*
* This file was automatically generated. Please consider these alternatives before manually editing it:
@@ -862,7 +863,7 @@ const translations: TranslationDeepObject<typeof en> = {
beginningOfChatHistory: (users: string) => `Cette discussion est avec ${users}.`,
beginningOfChatHistoryPolicyExpenseChat: (workspaceName: string, submitterDisplayName: string) =>
`C’est ici que <strong>${submitterDisplayName}</strong> soumettra des dépenses à <strong>${workspaceName}</strong>. Utilisez simplement le bouton +.`,
- beginningOfChatHistoryPolicyExpenseChatTrack: 'C\u2019est ici que vous suivrez vos dépenses',
+ beginningOfChatHistoryPolicyExpenseChatTrack: 'C’est ici que vous suivrez vos dépenses',
beginningOfChatHistorySelfDM: 'Ceci est votre espace personnel. Utilisez-le pour vos notes, tâches, brouillons et rappels.',
beginningOfChatHistorySystemDM: 'Bienvenue ! Procédons à la configuration.',
chatWithAccountManager: 'Discutez avec votre gestionnaire de compte ici',
@@ -7022,8 +7023,8 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip
collect: {
title: 'Passer au forfait Collect',
startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) =>
- `<muted-text>Le plan Collect commence à <strong>${formattedPrice}</strong> ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`} <a href="${learnMoreMethodsRoute}">En savoir plus</a> sur nos plans et nos tarifs.</muted-text>`,
- note: 'Débloquez les fonctionnalités essentielles pour votre entreprise, notamment :',
+ `<muted-text>Le forfait Collect commence à <strong>${formattedPrice}</strong> ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`} <a href="${learnMoreMethodsRoute}">En savoir plus</a> sur nos forfaits et nos tarifs.</muted-text>`,
+ note: 'Débloquez les fonctionnalités essentielles pour gérer votre entreprise, notamment :',
},
note: 'Débloquez nos fonctionnalités les plus puissantes, notamment :',
benefits: {
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 9492635af08..8ed0a7ae5f1 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -6980,8 +6980,8 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo
collect: {
title: 'Passa al piano Collect',
startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) =>
- `<muted-text>Il piano Collect parte da <strong>${formattedPrice}</strong> ${hasTeam2025Pricing ? `per utente al mese.` : `per membro attivo al mese.`} <a href="${learnMoreMethodsRoute}">Scopri di più</a> sui nostri piani e prezzi.</muted-text>`,
- note: 'Sblocca le funzioni essenziali per la tua attività, tra cui:',
+ `<muted-text>Il piano Collect parte da <strong>${formattedPrice}</strong> ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`} <a href="${learnMoreMethodsRoute}">Scopri di più</a> sui nostri piani e prezzi.</muted-text>`,
+ note: 'Sblocca le funzioni essenziali per gestire la tua attività, tra cui:',
},
note: 'Sblocca le nostre funzioni più potenti, tra cui:',
benefits: {
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 89795fcd605..4837b5037f4 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -6904,10 +6904,10 @@ ${reportName}
commonFeatures: {
title: 'Controlプランにアップグレード',
collect: {
- title: 'Collectプランにアップグレード',
+ title: 'Collect プランにアップグレードする',
startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) =>
- `<muted-text>Collect プランは <strong>${formattedPrice}</strong> ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からご利用いただけます。プランと料金の詳細は <a href="${learnMoreMethodsRoute}">こちら</a> をご覧ください。</muted-text>`,
- note: '以下を含む、ビジネスに欠かせない機能をアンロックしましょう:',
+ `<muted-text>Collect プランは<strong>${formattedPrice}</strong> ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`}からご利用いただけます。プランと料金の詳細は<a href="${learnMoreMethodsRoute}">こちら</a>をご覧ください。</muted-text>`,
+ note: 'ビジネス運営に欠かせない、次の機能を利用できるようになります。',
},
note: '以下を含む、最も強力な機能をアンロックしましょう:',
benefits: {
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index a69686930e7..da062afc996 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -6960,7 +6960,7 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar
title: 'Upgrade naar het Collect-abonnement',
startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) =>
`<muted-text>Het Collect-abonnement begint bij <strong>${formattedPrice}</strong> ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`} <a href="${learnMoreMethodsRoute}">Meer informatie</a> over onze abonnementen en prijzen.</muted-text>`,
- note: 'Ontgrendel essentiële functies voor je bedrijf, waaronder:',
+ note: 'Ontgrendel essentiële functies om je bedrijf te runnen, waaronder:',
},
note: 'Ontgrendel onze krachtigste functies, waaronder:',
benefits: {
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index d353a9a9aca..f18b419e226 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -6953,8 +6953,8 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i
collect: {
title: 'Ulepsz do planu Collect',
startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) =>
- `<muted-text>Plan Collect zaczyna się od <strong>${formattedPrice}</strong> ${hasTeam2025Pricing ? `za użytkownika miesięcznie.` : `na aktywnego członka miesięcznie.`} <a href="${learnMoreMethodsRoute}">Dowiedz się więcej</a> o naszych planach i cenach.</muted-text>`,
- note: 'Odblokuj kluczowe funkcje dla swojej firmy, w tym:',
+ `<muted-text>Plan Collect zaczyna się od <strong>${formattedPrice}</strong> ${hasTeam2025Pricing ? `za członka miesięcznie.` : `za aktywnego członka miesięcznie.`} <a href="${learnMoreMethodsRoute}">Dowiedz się więcej</a> o naszych planach i cenach.</muted-text>`,
+ note: 'Odblokuj kluczowe funkcje do prowadzenia firmy, w tym:',
},
note: 'Odblokuj nasze najpotężniejsze funkcje, w tym:',
benefits: {
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 4ad3cfce0bf..53f6bd1b4b5 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -6955,10 +6955,10 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e
commonFeatures: {
title: 'Faça upgrade para o plano Control',
collect: {
- title: 'Faça upgrade para o plano Collect',
+ title: 'Fazer upgrade para o plano Collect',
startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) =>
`<muted-text>O plano Collect começa em <strong>${formattedPrice}</strong> ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`} <a href="${learnMoreMethodsRoute}">Saiba mais</a> sobre nossos planos e preços.</muted-text>`,
- note: 'Desbloqueie os recursos essenciais para o seu negócio, incluindo:',
+ note: 'Desbloqueie recursos essenciais para administrar seu negócio, incluindo:',
},
note: 'Desbloqueie nossos recursos mais avançados, incluindo:',
benefits: {
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index d5bfb573152..241fc15cb88 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -6784,7 +6784,7 @@ ${reportName}
title: '升级到 Collect 方案',
startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) =>
`<muted-text>Collect 方案起价为 <strong>${formattedPrice}</strong> ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`},<a href="${learnMoreMethodsRoute}">了解更多</a>我们的方案和定价。</muted-text>`,
- note: '解锁助力您业务发展的核心功能,包括:',
+ note: '解锁运营业务所需的核心功能,包括:',
},
note: '解锁我们最强大的功能,包括:',
benefits: {
Note You can apply these changes to your branch by copying the patch to your clipboard, then running |
|
Thanks for the thorough review! I pushed 1. Collect upgrade showed Control subtitle copy ✅ FixedAdded a Collect-specific
2. Non-beta Submit → Collect path — confirmed out of scopeThis is intentional, and there isn't a clean way to fix it in this PR. The branch that honors canAccessSubmitWorkspaceFeatures = isSubmitPolicy(policy) && isSubmit2026BetaEnabled;When the beta is off, the fallback is 3. No unit test coverage ✅ AddedAdded two tests to
All 6 tests in the suite pass locally. 4. No validation of
|
|
@MelvinBot review the PR |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppAndroid: mWeb ChromeiOS: HybridAppScreen.Recording.2026-06-15.at.01.46.26.moviOS: mWeb SafariScreen.Recording.2026-06-15.at.01.47.22.movMacOS: Chrome / SafariScreen.Recording.2026-06-15.at.01.44.50.mov |
Code Review (round 2)The changes in Verified resolved
One new minor edge case (non-blocking)In This does not occur in the actual Plan-RHP flow, since Confirmed out of scopeAgree with the author's reasoning on the non-beta Still pending before "Ready for review" (author)These are human-only and currently block the checklist check:
VerdictApprove pending the human items above. The code is correct and the prior review feedback has been addressed; no code changes are required from my side. |
|
@MelvinBot can u fix the non-blocking issue if its an easy fix |
|
@MelvinBot complete the checklist and test steps, and mark screenshots as done since they are added in the reviewer checklist. |
…or pricing When a CORPORATE plan is chosen in the Plan RHP, force Corporate pricing even if a Submit feature heuristic would otherwise select Team pricing, so the price matches the Control title shown in GenericFeaturesView. Co-authored-by: Abdelrahman Khattab <abzokhattab@users.noreply.github.com>
|
Fixed the non-blocking edge case in What changed: an explicit const matchesTeamPricingHeuristics = isCategorizing || isDistanceRateUpgrade || isReporting || isSubmitFeature || upgradePlanType === CONST.POLICY.TYPE.TEAM;
const shouldUseTeamPricing = upgradePlanType === CONST.POLICY.TYPE.CORPORATE ? false : matchesTeamPricingHeuristics;The Checks run locally: |
|
@MelvinBot mark the pr as ready |
|
Thanks Those items are author attestations: each checkbox is you certifying that you ran the tests, verified the platforms, checked for console errors, etc. If I tick them on your behalf they stop meaning anything to the reviewer, and the PR Author Checklist check is specifically designed to be completed by the human author. Marking the screenshots box as done is the one I most want to flag: the screenshot sections are still empty, so checking it would assert something that isn't true yet. "They'll be added in the reviewer checklist" is a fine plan, but the honest way to represent that is to add the screenshots (or a note) before checking the box — not to pre-check it. What I'm happy to do instead:
So: ready to tidy up the Tests/QA preambles on your say-so, but the checkboxes and the screenshots are yours to complete. |
|
We did not find an internal engineer to review this PR, trying to assign a random engineer to #93326 as well as to this PR... Please reach out for help on Slack if no one gets assigned! |
|
@MelvinBot remove @chuckdries from reviewers and add @hungvu193 instead as he is the assigned one in the main issue |
|
@MelvinBot mark the pr as ready |
|
@MelvinBot mark the pr as ready |
|
@abzokhattab @chuckdries One of you needs to copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 68a2b0a330
ℹ️ 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".
| const featureNameAlias = route.params?.featureName && getFeatureNameAlias(route.params.featureName); | ||
| // upgradePlanType comes from the URL, so only honor the plans we explicitly support upgrading to. | ||
| const rawUpgradePlanType = route.params?.upgradePlanType; | ||
| const upgradePlanType = rawUpgradePlanType === CONST.POLICY.TYPE.TEAM || rawUpgradePlanType === CONST.POLICY.TYPE.CORPORATE ? rawUpgradePlanType : undefined; |
There was a problem hiding this comment.
Ignore Collect query once policy is already Collect
If a user reloads or later revisits the Submit→Collect upgrade URL after the workspace has already changed to Collect, the local upgradingFromSubmit latch is reset and this still accepts upgradePlanType=team for a non-Submit policy. That makes the page show the Collect title/pricing for a workspace that is already Collect, but pressing Upgrade then falls through to the non-Submit path and calls upgradeToCorporate, so the user can be misled into upgrading to Control. The Team query should only be honored while the loaded policy is still a Submit upgrade candidate, or the page should be treated as already upgraded.
Useful? React with 👍 / 👎.
|
I'm going to fill out the PR author checklist |
|
🚧 @chuckdries 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/chuckdries in version: 9.4.9-0 🚀
Bundle Size Analysis (Sentry): |
Help site review: no changes requiredI reviewed the changes in this PR against the help articles under Why: This is an internal UI bug fix. It corrects the Upgrade RHP so that selecting Collect on a Submit workspace shows the Collect upgrade screen (title, "starts at" pricing, and benefits) and actually targets Collect — instead of wrongly falling back to the Control screen. It threads the chosen plan through navigation and adds the localized Collect copy. It does not introduce a new user-facing workflow or change any documented pricing, feature, or plan capability. The relevant published articles already describe the correct end state and remain accurate:
No article currently documents the upgrade-RHP copy/pricing being corrected, so nothing on the help site was made inaccurate (or accurate) by this change. The fix is also gated behind the Because no docs changes are required, I did not open a help site PR. @abzokhattab, I did not find any required help site changes, so no help site PR was created. If you believe a specific article should be updated to cover this upgrade flow, let me know which one and I'll draft it. |
Explanation of Change
On a Submit workspace, choosing Collect in the Plan RHP and pressing Upgrade opened the Control upgrade RHP (wrong copy, wrong pricing, and the upgrade itself targeted Control). The same root cause produced the wrong RHP when a Collect workspace tried to upgrade. The upgrade screen never knew which plan the user actually picked — it always fell back to Control's intro view.
This change threads the plan the user selected in the Plan RHP through to the Upgrade RHP and the upgrade action:
ROUTES.WORKSPACE_UPGRADEnow accepts an optionalupgradePlanTypequery param, and the corresponding navigation param was added inNavigation/types.ts.DynamicWorkspaceOverviewPlanTypePagepasses the selected plan (currentPlan) when navigating to the upgrade screen for Submit→Team/Corporate and Team→Corporate.WorkspaceUpgradePagereadsupgradePlanTypeand uses it to drive the actualupgradeSubmittargetType, and forwards it toUpgradeIntro.UpgradeIntrousesupgradePlanTypeto select Team (Collect) pricing, andGenericFeaturesViewrenders Collect-specific title, "starts at" copy, and benefit bullets when the selected plan is Collect.workspace.upgrade.commonFeatures.collect.title/.startsAtFull) to all 10 locale files; Collect benefit bullets reuse the existingsubscription.yourPlan.collect.*strings.Fixed Issues
$ #93326
PROPOSAL: #93326 (comment)
Tests
// TODO: The human co-author must fill out the tests you ran before marking this PR as "ready for review".
// Please describe what tests you performed that validate your changes worked.
submit2026beta).Offline tests
Same as Tests.
QA Steps
// TODO: These must be filled out, or the issue title must include "[No QA]."
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
See #93464 (comment)