Skip to content

Fix Submit/Collect upgrade RHP to reflect the selected plan#93464

Merged
chuckdries merged 3 commits into
mainfrom
claude-submitUpgradeShowsCollectPlan
Jun 15, 2026
Merged

Fix Submit/Collect upgrade RHP to reflect the selected plan#93464
chuckdries merged 3 commits into
mainfrom
claude-submitUpgradeShowsCollectPlan

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

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_UPGRADE now accepts an optional upgradePlanType query param, and the corresponding navigation param was added in Navigation/types.ts.
  • DynamicWorkspaceOverviewPlanTypePage passes the selected plan (currentPlan) when navigating to the upgrade screen for Submit→Team/Corporate and Team→Corporate.
  • WorkspaceUpgradePage reads upgradePlanType and uses it to drive the actual upgradeSubmit targetType, and forwards it to UpgradeIntro.
  • UpgradeIntro uses upgradePlanType to select Team (Collect) pricing, and GenericFeaturesView renders Collect-specific title, "starts at" copy, and benefit bullets when the selected plan is Collect.
  • Added the two new Collect upgrade strings (workspace.upgrade.commonFeatures.collect.title / .startsAtFull) to all 10 locale files; Collect benefit bullets reuse the existing subscription.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.

  1. Open a Submit workspace (requires the submit2026 beta).
  2. Go to Workspace Overview → Plan type.
  3. Select Collect and press Save / Upgrade.
  4. Verify the Upgrade RHP shows the Collect title, Collect "starts at" pricing, and Collect benefit bullets (not Control), and that completing the upgrade moves the workspace to Collect.
  5. Repeat selecting Control and verify the Control upgrade RHP still shows correctly.
  • Verify that no errors appear in the JS console

Offline tests

Same as Tests.

QA Steps

// TODO: These must be filled out, or the issue title must include "[No QA]."

  1. On a Submit workspace, open Plan type, select Collect, press Upgrade.
  2. Verify the Collect upgrade RHP (title/pricing/benefits) is shown and the upgrade targets Collect.
  3. Verify selecting Control still shows the Control upgrade RHP.
  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

See #93464 (comment)

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>
@OSBotify

This comment has been minimized.

@abzokhattab

Copy link
Copy Markdown
Contributor

Code Review

@MelvinBot — solid fix for the root cause in #93326. The approach of threading upgradePlanType through the route → page → UI → upgrade action is correct and backward-compatible. A few items before marking ready for review:

What works well

  • Correct data flowDynamicWorkspaceOverviewPlanTypePage passes currentPlan, WorkspaceUpgradePage uses it for targetType, and UpgradeIntro / GenericFeaturesView use it for pricing and copy.
  • Backward compatible — existing WORKSPACE_UPGRADE.getRoute(...) callers are unaffected; the new param is optional.
  • URL construction is correctgetUrlWithBackToParam handles ?upgradePlanType=...&backTo=... properly when both params are present.
  • Localization — Collect title/pricing strings added across all 10 locales; benefit bullets reuse existing subscription.yourPlan.collect.* keys.
  • Focused diff — minimal, no unrelated refactors.

Issues / concerns

1. Collect upgrade still shows Control subtitle copy (minor UX bug)

GenericFeaturesView updates title, pricing, and benefits for Collect, but the note line is still hardcoded to Control marketing:

translate('workspace.upgrade.commonFeatures.note')

That string is "Unlock our most powerful features, including:" — reads as Control copy, not Collect. Consider adding a collect.note string (e.g. aligned with subscription.yourPlan.collect.description).

2. Non-beta Submit → Collect upgrade path not fixed

The issue proposal called out fixing both upgradeSubmit and the non-beta upgradeToCorporate fallback. This PR only addresses the beta path — when SUBMIT_2026 beta is off, selecting Collect still calls upgradeToCorporate, which forces CORPORATE. Tests require the beta so this may be acceptable, but worth confirming scope (also partially relates to #92863).

3. No unit test coverage for the new behavior

tests/ui/WorkspaceUpgradeTest.tsx covers generic upgrade pricing but not upgradePlanType. A test asserting upgradePlanType=team → Collect title + Team pricing and upgradePlanType=corporate → Control title + Corporate pricing would help prevent regressions.

4. No validation of upgradePlanType from the URL

upgradePlanType is passed straight from route params into upgradeSubmit. Low risk since values are internal (team / corporate), but a whitelist guard would be safer.

5. Draft PR completeness

Before marking ready for review:

  • Fill in Tests / QA steps (remove TODOs)
  • Add screenshots across platforms
  • Apply OSBotify's translation suggestions (de, fr, it, ja, nl, pl, pt-BR tweaks)
  • Complete the author checklist

Regression risk

Area Risk
Existing feature-gated upgrades (rules, approvals, etc.) Low — no upgradePlanType passed; existing fallbacks unchanged
Team → Control from Plan RHP Low — passes CORPORATE; Control copy/pricing shown correctly
Submit → Collect (beta on) Fixed — primary bug path
Submit → Collect (beta off) Still broken — see concern #2

Verdict

Approve with minor changes — the core fix is correct and well-scoped for the reported bug (Submit workspace + submit2026 beta). I'd hold full approval until the Collect note copy is addressed (or confirmed intentional with design), test/QA sections and screenshots are filled in, a unit test for upgradePlanType is added, and scope on non-beta Submit → Collect is explicitly confirmed in or out.

Suggested test plan

  1. Submit workspace + submit2026 beta → Plan type → Collect → Upgrade → verify Collect title, Team pricing, Collect benefits, and workspace upgrades to Collect
  2. Same flow → Control → verify Control RHP unchanged
  3. Collect workspace → Plan type → Control → verify Control RHP
  4. Feature-gated upgrade (e.g. Rules) from a Collect workspace → verify feature-specific intro still works
  5. Deep link: workspaces/{id}/upgrade?upgradePlanType=team → verify Collect RHP renders

- 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>
@OSBotify

Copy link
Copy Markdown
Contributor

🦜 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 diff
diff --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 pbpaste | git apply 😉

View workflow run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! I pushed de6d939 addressing the actionable code items (1, 3, 4). Details below, including scope confirmation for 2 and what remains for the author on 5.

1. Collect upgrade showed Control subtitle copy ✅ Fixed

Added a Collect-specific note string and wired it through GenericFeaturesView so the subtitle matches the selected plan:

  • GenericFeaturesView now picks the note by plan type — GenericFeaturesView.tsx#L46
  • Added workspace.upgrade.commonFeatures.collect.note to all 10 locales (en: "Unlock essential features to run your business, including:")

2. Non-beta Submit → Collect path — confirmed out of scope

This is intentional, and there isn't a clean way to fix it in this PR. The branch that honors upgradePlanType only runs when canAccessSubmitWorkspaceFeatures is true, which requires the submit2026 beta:

canAccessSubmitWorkspaceFeatures = isSubmitPolicy(policy) && isSubmit2026BetaEnabled;

When the beta is off, the fallback is upgradeToCorporate(policy, feature?.name) — that command takes no target-tier argument and always upgrades to Corporate by design. Routing a Collect upgrade through it would require a backend/API change (a non-beta command that accepts a target tier), which is outside the scope of this UI fix. Given Submit workspaces are themselves a submit2026 concept, the beta-gated path is the real-world path for the reported bug. I'd keep the non-beta change tracked separately (relates to #92863) rather than expand scope here.

3. No unit test coverage ✅ Added

Added two tests to tests/ui/WorkspaceUpgradeTest.tsx:

All 6 tests in the suite pass locally.

4. No validation of upgradePlanType from the URL ✅ Added

Added a whitelist guard so only the plans we explicitly support are honored; anything else falls back to the existing default — WorkspaceUpgradePage.tsx#L83:

const rawUpgradePlanType = route.params?.upgradePlanType;
const upgradePlanType = rawUpgradePlanType === CONST.POLICY.TYPE.TEAM || rawUpgradePlanType === CONST.POLICY.TYPE.CORPORATE ? rawUpgradePlanType : undefined;

5. Draft PR completeness — needs the author

These are human-only tasks I can't complete: filling in the Tests/QA sections, capturing cross-platform screenshots, applying OSBotify's translation suggestions, and the author checklist. Flagging for @abzokhattab to finish before marking ready for review.

Checks run locally

  • lint on the changed files: clean
  • npm test tests/ui/WorkspaceUpgradeTest.tsx: 6/6 pass
  • prettier: no changes needed
  • typecheck: the only errors are pre-existing duplicate-identifier noise in src/libs/API/types.ts (untouched by this PR)
  • react-compiler check: GenericFeaturesView compiles. WorkspaceUpgradePage reports the same 2 "Existing memoization could not be preserved" errors at the component declaration (line 75) independent of this PR's changes — they predate the upgradePlanType work and live upstream of every line touched here, so check-changed treats them as known-failing, not a regression.

@abzokhattab

Copy link
Copy Markdown
Contributor

@MelvinBot review the PR

@abzokhattab

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified there are no new alerts related to the canBeMissing param for useOnyx
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick).
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Android: mWeb Chrome
iOS: HybridApp
Screen.Recording.2026-06-15.at.01.46.26.mov
iOS: mWeb Safari
Screen.Recording.2026-06-15.at.01.47.22.mov
MacOS: Chrome / Safari
Screen.Recording.2026-06-15.at.01.44.50.mov

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Code Review (round 2)

The changes in de6d939 resolve the actionable items from the prior review. The core fix — threading upgradePlanType through route → page → UI → upgrade action — is correct, backward-compatible, and well-scoped. All CI is green except the human-only author checklist.

Verified resolved

  • Collect subtitle copynote is now plan-aware in GenericFeaturesView.tsx:46, with collect.note added to all 10 locales.
  • URL validation — whitelist guard in WorkspaceUpgradePage.tsx:83-84 restricts the URL param to TEAM/CORPORATE before it reaches upgradeSubmit. Good — values from the URL shouldn't be trusted directly.
  • Unit tests — both upgradePlanType=team (Collect title + Team price) and =corporate (Control title + Corporate price) are covered in WorkspaceUpgradeTest.tsx:165-216.
  • Translation fidelity — the new collect.startsAtFull strings are faithful copies of the existing benefits.startsAtFull per locale (verified byte-for-byte in en/de), so the per-locale punctuation/typography is consistent with what's already shipped.

One new minor edge case (non-blocking)

In UpgradeIntro.tsx:63, shouldUseTeamPricing is true when either isSubmitFeature or upgradePlanType === TEAM. The title, however, is driven solely by upgradePlanType === TEAM (GenericFeaturesView.tsx:42). So a deeplink combining a Submit feature with ?upgradePlanType=corporate (e.g. workspaces/{id}/upgrade/{submitFeature}?upgradePlanType=corporate) would render the Control title but Team pricing — a mismatch.

This does not occur in the actual Plan-RHP flow, since DynamicWorkspaceOverviewPlanTypePage navigates with no featureName (so isSubmitFeature is false). It's only reachable via a hand-crafted deeplink, so it's low priority — worth a follow-up at most, not a blocker for this PR.

Confirmed out of scope

Agree with the author's reasoning on the non-beta upgradeToCorporate path — that command takes no target-tier argument, so fixing it needs a backend change. Tracking it separately (relates to #92863) rather than expanding this UI fix is the right call.


Still pending before "Ready for review" (author)

These are human-only and currently block the checklist check:

  • Fill in the Tests and QA Steps sections (remove the TODOs)
  • Add screenshots/videos across platforms (at minimum, Collect vs Control upgrade RHP on web)
  • Complete the author checklist

Verdict

Approve 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.

@abzokhattab

Copy link
Copy Markdown
Contributor

@MelvinBot can u fix the non-blocking issue if its an easy fix

@abzokhattab

abzokhattab commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

@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>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Fixed the non-blocking edge case in 68a2b0a.

What changed: an explicit upgradePlanType (the plan picked in the Plan RHP) is now authoritative over the feature-based pricing heuristics. When upgradePlanType === CORPORATE, pricing is forced to Corporate even if a Submit feature would otherwise select Team pricing — so the price always matches the Control title shown in GenericFeaturesView.

UpgradeIntro.tsx:62-65:

const matchesTeamPricingHeuristics = isCategorizing || isDistanceRateUpgrade || isReporting || isSubmitFeature || upgradePlanType === CONST.POLICY.TYPE.TEAM;
const shouldUseTeamPricing = upgradePlanType === CONST.POLICY.TYPE.CORPORATE ? false : matchesTeamPricingHeuristics;

The team path is unchanged (still Team pricing), and undefined falls through to the existing heuristics — so no behavior change for any current flow; this only resolves the title/pricing mismatch reachable via a ?upgradePlanType=corporate deeplink on a Submit feature.

Checks run locally: lint on the changed file ✅, react-compiler check → COMPILED ✅, npm test tests/ui/WorkspaceUpgradeTest.tsx → 6/6 ✅, prettier → no changes. typecheck shows only the pre-existing src/libs/API/types.ts duplicate-identifier errors, untouched by this change.

@abzokhattab

Copy link
Copy Markdown
Contributor

@MelvinBot mark the pr as ready

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Thanks abzokhattab — but I'm going to leave the author checklist, Tests/QA sign-off, and the screenshots line for you to complete rather than fill them in myself.

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:

  • The Tests and QA Steps bodies already contain concrete, accurate steps — they just have the // TODO preamble lines on top. I can strip those TODO lines so the sections read cleanly, if you'd like. That's editorial cleanup, not an attestation — just reply and I'll do it.
  • If you run the flow and tell me the results, I can help draft a testing summary in your words.

So: ready to tidy up the Tests/QA preambles on your say-so, but the checkboxes and the screenshots are yours to complete.

@melvin-bot

melvin-bot Bot commented Jun 15, 2026

Copy link
Copy Markdown

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!

@melvin-bot melvin-bot Bot requested a review from chuckdries June 15, 2026 00:07
@abzokhattab

Copy link
Copy Markdown
Contributor

@MelvinBot remove @chuckdries from reviewers and add @hungvu193 instead as he is the assigned one in the main issue

@abzokhattab

Copy link
Copy Markdown
Contributor

@MelvinBot mark the pr as ready

@MelvinBot MelvinBot requested review from hungvu193 and removed request for chuckdries June 15, 2026 00:10
@abzokhattab

Copy link
Copy Markdown
Contributor

@MelvinBot mark the pr as ready

@chuckdries chuckdries marked this pull request as ready for review June 15, 2026 22:23
@chuckdries chuckdries requested a review from a team as a code owner June 15, 2026 22:23
@melvin-bot melvin-bot Bot requested review from abzokhattab and chuckdries June 15, 2026 22:23
@melvin-bot melvin-bot Bot requested review from flaviadefaria and removed request for a team June 15, 2026 22:23
@melvin-bot

melvin-bot Bot commented Jun 15, 2026

Copy link
Copy Markdown

@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]

@melvin-bot melvin-bot Bot removed the request for review from a team June 15, 2026 22:23
@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/ROUTES.ts 20.45% <100.00%> (+0.14%) ⬆️
...workspace/DynamicWorkspaceOverviewPlanTypePage.tsx 0.00% <ø> (ø)
...rc/pages/workspace/upgrade/GenericFeaturesView.tsx 96.29% <100.00%> (ø)
src/pages/workspace/upgrade/UpgradeIntro.tsx 94.91% <100.00%> (+0.08%) ⬆️
...c/pages/workspace/upgrade/WorkspaceUpgradePage.tsx 40.93% <100.00%> (+0.61%) ⬆️
... and 70 files with indirect coverage changes

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@chuckdries

Copy link
Copy Markdown
Contributor

I'm going to fill out the PR author checklist

@chuckdries chuckdries merged commit 483e912 into main Jun 15, 2026
41 of 42 checks passed
@chuckdries chuckdries deleted the claude-submitUpgradeShowsCollectPlan branch June 15, 2026 22:37
@github-actions

Copy link
Copy Markdown
Contributor

🚧 @chuckdries has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/chuckdries in version: 9.4.9-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Help site review: no changes required

I reviewed the changes in this PR against the help articles under docs/articles and no HelpDot updates are needed.

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 submit2026 beta, so it doesn't change generally-available behavior. Since no documentation reflects the old (buggy) behavior, there is nothing to update.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants