From 6c813922e970643774444de1e69054a0ef6460da Mon Sep 17 00:00:00 2001 From: "Abdelrahman Khattab (via MelvinBot)" Date: Fri, 12 Jun 2026 18:50:48 +0000 Subject: [PATCH 1/3] Fix: Submit/Collect upgrade RHP shows correct plan based on selection 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 --- src/ROUTES.ts | 6 ++-- src/languages/de.ts | 5 +++ src/languages/en.ts | 5 +++ src/languages/es.ts | 5 +++ src/languages/fr.ts | 5 +++ src/languages/it.ts | 5 +++ src/languages/ja.ts | 5 +++ src/languages/nl.ts | 5 +++ src/languages/pl.ts | 5 +++ src/languages/pt-BR.ts | 5 +++ src/languages/zh-hans.ts | 5 +++ src/libs/Navigation/types.ts | 1 + .../DynamicWorkspaceOverviewPlanTypePage.tsx | 4 +-- .../workspace/upgrade/GenericFeaturesView.tsx | 35 ++++++++++++++----- src/pages/workspace/upgrade/UpgradeIntro.tsx | 9 +++-- .../upgrade/WorkspaceUpgradePage.tsx | 4 ++- 16 files changed, 92 insertions(+), 17 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 56ae1a2adf4d..7ba40ac75120 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2470,8 +2470,10 @@ const ROUTES = { }, WORKSPACE_UPGRADE: { route: 'workspaces/:policyID?/upgrade/:featureName?', - getRoute: (policyID?: string, featureName?: string, backTo?: string) => - getUrlWithBackToParam(policyID ? (`workspaces/${policyID}/upgrade/${encodeURIComponent(featureName ?? '')}` as const) : (`workspaces/upgrade` as const), backTo), + getRoute: (policyID?: string, featureName?: string, backTo?: string, upgradePlanType?: string) => { + const baseRoute = policyID ? (`workspaces/${policyID}/upgrade/${encodeURIComponent(featureName ?? '')}` as const) : (`workspaces/upgrade` as const); + return getUrlWithBackToParam(upgradePlanType ? (`${baseRoute}?upgradePlanType=${upgradePlanType}` as const) : baseRoute, backTo); + }, }, WORKSPACE_DOWNGRADE: { route: 'workspaces/:policyID?/downgrade/', diff --git a/src/languages/de.ts b/src/languages/de.ts index 62f032f28bf5..d66a67c9f37e 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6994,6 +6994,11 @@ 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', + startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => + `Der Collect-Tarif beginnt bei ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}. Erfahre mehr über unsere Tarife und Preise.`, + }, note: 'Schalte unsere leistungsstärksten Funktionen frei, darunter:', benefits: { startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => diff --git a/src/languages/en.ts b/src/languages/en.ts index 99495c52e9d7..dc13f1ae92b9 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7183,6 +7183,11 @@ const translations = { }, commonFeatures: { title: 'Upgrade to the Control plan', + collect: { + title: 'Upgrade to the Collect plan', + startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => + `The Collect plan starts at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`} Learn more about our plans and pricing.`, + }, note: 'Unlock our most powerful features, including:', benefits: { startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => diff --git a/src/languages/es.ts b/src/languages/es.ts index dd886dd631c1..371d5350e93a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6977,6 +6977,11 @@ ${amount} para ${merchant} - ${date}`, }, commonFeatures: { title: 'Mejorar al plan Controlar', + collect: { + title: 'Mejorar al plan Recopilar', + startsAtFull: (learnMoreMethodsRoute, formattedPrice, hasTeam2025Pricing) => + `El plan Recopilar comienza desde ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`} Más información sobre nuestros planes y precios.`, + }, note: 'Desbloquea nuestras funciones más potentes, incluyendo:', benefits: { startsAtFull: (learnMoreMethodsRoute, formattedPrice, hasTeam2025Pricing) => diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 3fe50df37d3f..d73c15d57e92 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7019,6 +7019,11 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip }, commonFeatures: { title: 'Passer au forfait Contrôle', + collect: { + title: 'Passer au forfait Collect', + startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => + `Le plan Collect commence à ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`} En savoir plus sur nos plans et nos tarifs.`, + }, note: 'Débloquez nos fonctionnalités les plus puissantes, notamment :', benefits: { startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => diff --git a/src/languages/it.ts b/src/languages/it.ts index 23160e7232e6..c14ab78534fb 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6977,6 +6977,11 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo }, commonFeatures: { title: 'Passa al piano Control', + collect: { + title: 'Passa al piano Collect', + startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => + `Il piano Collect parte da ${formattedPrice} ${hasTeam2025Pricing ? `per utente al mese.` : `per membro attivo al mese.`} Scopri di più sui nostri piani e prezzi.`, + }, note: 'Sblocca le nostre funzioni più potenti, tra cui:', benefits: { startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 0c24a52ba93f..aab2628da085 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6903,6 +6903,11 @@ ${reportName} }, commonFeatures: { title: 'Controlプランにアップグレード', + collect: { + title: 'Collectプランにアップグレード', + startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => + `Collect プランは ${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からご利用いただけます。プランと料金の詳細は こちら をご覧ください。`, + }, note: '以下を含む、最も強力な機能をアンロックしましょう:', benefits: { startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => diff --git a/src/languages/nl.ts b/src/languages/nl.ts index bf53a8817f55..37162401390a 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6956,6 +6956,11 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar }, commonFeatures: { title: 'Upgrade naar het Control-abonnement', + collect: { + title: 'Upgrade naar het Collect-abonnement', + startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => + `Het Collect-abonnement begint bij ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`} Meer informatie over onze abonnementen en prijzen.`, + }, note: 'Ontgrendel onze krachtigste functies, waaronder:', benefits: { startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 8f47ceff3e59..8b0008dd05cc 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -6950,6 +6950,11 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i }, commonFeatures: { title: 'Ulepsz do planu Control', + collect: { + title: 'Ulepsz do planu Collect', + startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => + `Plan Collect zaczyna się od ${formattedPrice} ${hasTeam2025Pricing ? `za użytkownika miesięcznie.` : `na aktywnego członka miesięcznie.`} Dowiedz się więcej o naszych planach i cenach.`, + }, note: 'Odblokuj nasze najpotężniejsze funkcje, w tym:', benefits: { startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 9dde33da7528..5bc4808d8d05 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6954,6 +6954,11 @@ 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', + startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => + `O plano Collect começa em ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`} Saiba mais sobre nossos planos e preços.`, + }, note: 'Desbloqueie nossos recursos mais avançados, incluindo:', benefits: { startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 7773e3384e5e..7b817ab737c7 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6780,6 +6780,11 @@ ${reportName} }, commonFeatures: { title: '升级到 Control 方案', + collect: { + title: '升级到 Collect 方案', + startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => + `Collect 方案起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`},了解更多我们的方案和定价。`, + }, note: '解锁我们最强大的功能,包括:', benefits: { startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 7a8a9331de99..043baf1d6e60 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -429,6 +429,7 @@ type SettingsNavigatorParamList = { // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md backTo?: Routes; categoryId?: string; + upgradePlanType?: ValueOf; }; [SCREENS.WORKSPACE.DOWNGRADE]: { policyID?: string; diff --git a/src/pages/workspace/DynamicWorkspaceOverviewPlanTypePage.tsx b/src/pages/workspace/DynamicWorkspaceOverviewPlanTypePage.tsx index 2b787ebe6aba..a75d855a2edb 100644 --- a/src/pages/workspace/DynamicWorkspaceOverviewPlanTypePage.tsx +++ b/src/pages/workspace/DynamicWorkspaceOverviewPlanTypePage.tsx @@ -98,12 +98,12 @@ function DynamicWorkspaceOverviewPlanTypePage({policy}: WithPolicyProps) { // still pick Team/Corporate. Route any selection from a Submit policy to the // upgrade screen — the polished Submit-specific upgrade UX ships in #87263. if (policyID && policy?.type === CONST.POLICY.TYPE.SUBMIT && (currentPlan === CONST.POLICY.TYPE.TEAM || currentPlan === CONST.POLICY.TYPE.CORPORATE)) { - Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID)); + Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, undefined, undefined, currentPlan)); return; } if (policyID && policy?.type === CONST.POLICY.TYPE.TEAM && currentPlan === CONST.POLICY.TYPE.CORPORATE) { - Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID)); + Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, undefined, undefined, currentPlan)); return; } diff --git a/src/pages/workspace/upgrade/GenericFeaturesView.tsx b/src/pages/workspace/upgrade/GenericFeaturesView.tsx index c9ba7363a891..8a1cf49b9fba 100644 --- a/src/pages/workspace/upgrade/GenericFeaturesView.tsx +++ b/src/pages/workspace/upgrade/GenericFeaturesView.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; import Button from '@components/Button'; import Icon from '@components/Icon'; import {loadIllustration} from '@components/Icon/IllustrationLoader'; @@ -13,6 +14,7 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; @@ -23,9 +25,11 @@ type GenericFeaturesViewProps = { formattedPrice: string; policyID?: string; backTo?: Route; + /** Target plan the user chose to upgrade to. When Collect (TEAM), render Collect copy instead of the default Control copy */ + upgradePlanType?: ValueOf; }; -function GenericFeaturesView({onUpgrade, buttonDisabled, loading, formattedPrice, backTo, policyID}: GenericFeaturesViewProps) { +function GenericFeaturesView({onUpgrade, buttonDisabled, loading, formattedPrice, backTo, policyID, upgradePlanType}: GenericFeaturesViewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {environmentURL} = useEnvironment(); @@ -34,12 +38,25 @@ function GenericFeaturesView({onUpgrade, buttonDisabled, loading, formattedPrice const {isExtraSmallScreenWidth} = useResponsiveLayout(); const hasTeam2025Pricing = useHasTeam2025Pricing(); - const benefits = [ - translate('workspace.upgrade.commonFeatures.benefits.benefit1'), - translate('workspace.upgrade.commonFeatures.benefits.benefit2'), - translate('workspace.upgrade.commonFeatures.benefits.benefit3'), - translate('workspace.upgrade.commonFeatures.benefits.benefit4'), - ]; + const isCollectUpgrade = upgradePlanType === CONST.POLICY.TYPE.TEAM; + const title = isCollectUpgrade ? translate('workspace.upgrade.commonFeatures.collect.title') : translate('workspace.upgrade.commonFeatures.title'); + const startsAtFull = isCollectUpgrade + ? translate('workspace.upgrade.commonFeatures.collect.startsAtFull', learnMoreMethodsRoute, formattedPrice, hasTeam2025Pricing) + : translate('workspace.upgrade.commonFeatures.benefits.startsAtFull', learnMoreMethodsRoute, formattedPrice, hasTeam2025Pricing); + + const benefits = isCollectUpgrade + ? [ + translate('subscription.yourPlan.collect.benefit1'), + translate('subscription.yourPlan.collect.benefit2'), + translate('subscription.yourPlan.collect.benefit3'), + translate('subscription.yourPlan.collect.benefit4'), + ] + : [ + translate('workspace.upgrade.commonFeatures.benefits.benefit1'), + translate('workspace.upgrade.commonFeatures.benefits.benefit2'), + translate('workspace.upgrade.commonFeatures.benefits.benefit3'), + translate('workspace.upgrade.commonFeatures.benefits.benefit4'), + ]; return ( @@ -51,7 +68,7 @@ function GenericFeaturesView({onUpgrade, buttonDisabled, loading, formattedPrice /> - {translate('workspace.upgrade.commonFeatures.title')} + {title} {translate('workspace.upgrade.commonFeatures.note')} {benefits.map((benefit) => ( ))} - + {!policyID && ( diff --git a/src/pages/workspace/upgrade/UpgradeIntro.tsx b/src/pages/workspace/upgrade/UpgradeIntro.tsx index fb249e0e1a6c..4ba26a9b0d76 100644 --- a/src/pages/workspace/upgrade/UpgradeIntro.tsx +++ b/src/pages/workspace/upgrade/UpgradeIntro.tsx @@ -38,9 +38,11 @@ type Props = { isDistanceRateUpgrade?: boolean; policyID?: string; backTo?: Route; + /** Target plan the user chose to upgrade to (drives Collect vs Control copy/pricing in the generic view) */ + upgradePlanType?: ValueOf; }; -function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizing, isDistanceRateUpgrade, isReporting, policyID, backTo}: Props) { +function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizing, isDistanceRateUpgrade, isReporting, policyID, backTo, upgradePlanType}: Props) { const styles = useThemeStyles(); const {isExtraSmallScreenWidth} = useResponsiveLayout(); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); @@ -58,12 +60,12 @@ function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizi const formattedPrice = useMemo(() => { const upgradeCurrency = Object.hasOwn(CONST.SUBSCRIPTION_PRICES, preferredCurrency) ? preferredCurrency : CONST.PAYMENT_CARD_CURRENCY.USD; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const shouldUseTeamPricing = isCategorizing || isDistanceRateUpgrade || isReporting || isSubmitFeature; + const shouldUseTeamPricing = isCategorizing || isDistanceRateUpgrade || isReporting || isSubmitFeature || upgradePlanType === CONST.POLICY.TYPE.TEAM; return `${convertToShortDisplayString( CONST.SUBSCRIPTION_PRICES[upgradeCurrency][shouldUseTeamPricing ? CONST.POLICY.TYPE.TEAM : CONST.POLICY.TYPE.CORPORATE][CONST.SUBSCRIPTION.TYPE.ANNUAL], upgradeCurrency, )} `; - }, [preferredCurrency, isCategorizing, isDistanceRateUpgrade, isReporting, isSubmitFeature]); + }, [preferredCurrency, isCategorizing, isDistanceRateUpgrade, isReporting, isSubmitFeature, upgradePlanType]); const allIconNames = Object.values(CONST.UPGRADE_FEATURE_INTRO_MAPPING) .map((feat) => feat?.icon) @@ -117,6 +119,7 @@ function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizi loading={loading} policyID={policyID} backTo={backTo} + upgradePlanType={upgradePlanType} /> ); } diff --git a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx index 03024b295094..6a80193f6047 100644 --- a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx +++ b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx @@ -80,6 +80,7 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) { const isSubmit2026BetaEnabled = isBetaEnabled(CONST.BETAS.SUBMIT_2026); const canAccessSubmitWorkspaceFeatures = canAccessSubmitWorkspaceFeaturesUtils(policy, isSubmit2026BetaEnabled); const featureNameAlias = route.params?.featureName && getFeatureNameAlias(route.params.featureName); + const upgradePlanType = route.params?.upgradePlanType; const [upgradingFromSubmit, setUpgradingFromSubmit] = useState(undefined); useEffect(() => { @@ -173,7 +174,7 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) { } if (canAccessSubmitWorkspaceFeatures) { - const targetType = (feature && 'requiredPlan' in feature ? feature.requiredPlan : undefined) ?? CONST.POLICY.TYPE.TEAM; + const targetType = upgradePlanType ?? (feature && 'requiredPlan' in feature ? feature.requiredPlan : undefined) ?? CONST.POLICY.TYPE.TEAM; upgradeSubmit(policy, targetType, email, accountID, priorFirstDayFreeTrial, priorLastDayFreeTrial); return; } @@ -372,6 +373,7 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) { buttonDisabled={isOffline || !canPerformUpgrade} loading={policy?.isPendingUpgrade} backTo={route.params.backTo} + upgradePlanType={upgradePlanType} /> )} From de6d939555af90883d02d88204d1497bd0f08533 Mon Sep 17 00:00:00 2001 From: "Abdelrahman Khattab (via MelvinBot)" Date: Sat, 13 Jun 2026 20:08:51 +0000 Subject: [PATCH 2/3] Address review: Collect note copy, upgradePlanType guard, and unit tests - 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 --- src/languages/de.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + .../workspace/upgrade/GenericFeaturesView.tsx | 3 +- .../upgrade/WorkspaceUpgradePage.tsx | 4 +- tests/ui/WorkspaceUpgradeTest.tsx | 52 +++++++++++++++++++ 13 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index d66a67c9f37e..59dd2e09a48a 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6998,6 +6998,7 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und title: 'Upgrade auf den Collect-Tarif', startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => `Der Collect-Tarif beginnt bei ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}. Erfahre mehr über unsere Tarife und Preise.`, + note: 'Schalte wichtige Funktionen für dein Unternehmen frei, darunter:', }, note: 'Schalte unsere leistungsstärksten Funktionen frei, darunter:', benefits: { diff --git a/src/languages/en.ts b/src/languages/en.ts index dc13f1ae92b9..73a1aad39a6d 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7187,6 +7187,7 @@ const translations = { title: 'Upgrade to the Collect plan', startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => `The Collect plan starts at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`} Learn more about our plans and pricing.`, + note: 'Unlock essential features to run your business, including:', }, note: 'Unlock our most powerful features, including:', benefits: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 371d5350e93a..18be0c27b655 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6981,6 +6981,7 @@ ${amount} para ${merchant} - ${date}`, title: 'Mejorar al plan Recopilar', startsAtFull: (learnMoreMethodsRoute, formattedPrice, hasTeam2025Pricing) => `El plan Recopilar comienza desde ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`} Más información sobre nuestros planes y precios.`, + note: 'Desbloquea las funciones esenciales para tu negocio, incluyendo:', }, note: 'Desbloquea nuestras funciones más potentes, incluyendo:', benefits: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index d73c15d57e92..54d928aef65c 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7023,6 +7023,7 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip title: 'Passer au forfait Collect', startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => `Le plan Collect commence à ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`} En savoir plus sur nos plans et nos tarifs.`, + note: 'Débloquez les fonctionnalités essentielles pour 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 c14ab78534fb..9492635af08a 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6981,6 +6981,7 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo title: 'Passa al piano Collect', startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => `Il piano Collect parte da ${formattedPrice} ${hasTeam2025Pricing ? `per utente al mese.` : `per membro attivo al mese.`} Scopri di più sui nostri piani e prezzi.`, + note: 'Sblocca le funzioni essenziali per 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 aab2628da085..89795fcd605a 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6907,6 +6907,7 @@ ${reportName} title: 'Collectプランにアップグレード', startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => `Collect プランは ${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からご利用いただけます。プランと料金の詳細は こちら をご覧ください。`, + note: '以下を含む、ビジネスに欠かせない機能をアンロックしましょう:', }, note: '以下を含む、最も強力な機能をアンロックしましょう:', benefits: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 37162401390a..a69686930e75 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6960,6 +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) => `Het Collect-abonnement begint bij ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`} Meer informatie over onze abonnementen en prijzen.`, + note: 'Ontgrendel essentiële functies voor je bedrijf, waaronder:', }, note: 'Ontgrendel onze krachtigste functies, waaronder:', benefits: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 8b0008dd05cc..d353a9a9aca2 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -6954,6 +6954,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i title: 'Ulepsz do planu Collect', startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => `Plan Collect zaczyna się od ${formattedPrice} ${hasTeam2025Pricing ? `za użytkownika miesięcznie.` : `na aktywnego członka miesięcznie.`} Dowiedz się więcej o naszych planach i cenach.`, + note: 'Odblokuj kluczowe funkcje dla swojej 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 5bc4808d8d05..4ad3cfce0bfe 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6958,6 +6958,7 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e title: 'Faça upgrade para o plano Collect', startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => `O plano Collect começa em ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`} Saiba mais sobre nossos planos e preços.`, + note: 'Desbloqueie os recursos essenciais para o 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 7b817ab737c7..d5bfb573152c 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6784,6 +6784,7 @@ ${reportName} title: '升级到 Collect 方案', startsAtFull: (learnMoreMethodsRoute: string, formattedPrice: string, hasTeam2025Pricing: boolean) => `Collect 方案起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`},了解更多我们的方案和定价。`, + note: '解锁助力您业务发展的核心功能,包括:', }, note: '解锁我们最强大的功能,包括:', benefits: { diff --git a/src/pages/workspace/upgrade/GenericFeaturesView.tsx b/src/pages/workspace/upgrade/GenericFeaturesView.tsx index 8a1cf49b9fba..64ac018c9417 100644 --- a/src/pages/workspace/upgrade/GenericFeaturesView.tsx +++ b/src/pages/workspace/upgrade/GenericFeaturesView.tsx @@ -43,6 +43,7 @@ function GenericFeaturesView({onUpgrade, buttonDisabled, loading, formattedPrice const startsAtFull = isCollectUpgrade ? translate('workspace.upgrade.commonFeatures.collect.startsAtFull', learnMoreMethodsRoute, formattedPrice, hasTeam2025Pricing) : translate('workspace.upgrade.commonFeatures.benefits.startsAtFull', learnMoreMethodsRoute, formattedPrice, hasTeam2025Pricing); + const note = isCollectUpgrade ? translate('workspace.upgrade.commonFeatures.collect.note') : translate('workspace.upgrade.commonFeatures.note'); const benefits = isCollectUpgrade ? [ @@ -69,7 +70,7 @@ function GenericFeaturesView({onUpgrade, buttonDisabled, loading, formattedPrice {title} - {translate('workspace.upgrade.commonFeatures.note')} + {note} {benefits.map((benefit) => ( (undefined); useEffect(() => { diff --git a/tests/ui/WorkspaceUpgradeTest.tsx b/tests/ui/WorkspaceUpgradeTest.tsx index f0b943dcad99..0aa60d79af9a 100644 --- a/tests/ui/WorkspaceUpgradeTest.tsx +++ b/tests/ui/WorkspaceUpgradeTest.tsx @@ -162,6 +162,58 @@ describe('WorkspaceUpgrade', () => { await waitForBatchedUpdates(); }); + it('should render the Collect plan title and Team pricing when upgradePlanType is team', async () => { + const policy: Policy = LHNTestUtils.getFakePolicy(); + + // Given that a policy is initialized in Onyx + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + }); + + // When the upgrade page is opened with upgradePlanType set to the Collect (Team) plan + const {unmount} = renderPage(SCREENS.WORKSPACE.UPGRADE, {policyID: policy.id, upgradePlanType: CONST.POLICY.TYPE.TEAM}); + await waitForBatchedUpdatesWithAct(); + + // Then the Collect title is shown + expect(await screen.findByText(TestHelper.translateLocal('workspace.upgrade.commonFeatures.collect.title'))).toBeTruthy(); + + // And the Team (Collect) annual price is shown + const teamPrice = convertToShortDisplayString( + CONST.SUBSCRIPTION_PRICES[CONST.PAYMENT_CARD_CURRENCY.USD][CONST.POLICY.TYPE.TEAM][CONST.SUBSCRIPTION.TYPE.ANNUAL], + CONST.PAYMENT_CARD_CURRENCY.USD, + ); + expect(await screen.findByText(teamPrice, {exact: false})).toBeTruthy(); + + unmount(); + await waitForBatchedUpdatesWithAct(); + }); + + it('should render the Control plan title and Corporate pricing when upgradePlanType is corporate', async () => { + const policy: Policy = LHNTestUtils.getFakePolicy(); + + // Given that a policy is initialized in Onyx + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + }); + + // When the upgrade page is opened with upgradePlanType set to the Control (Corporate) plan + const {unmount} = renderPage(SCREENS.WORKSPACE.UPGRADE, {policyID: policy.id, upgradePlanType: CONST.POLICY.TYPE.CORPORATE}); + await waitForBatchedUpdatesWithAct(); + + // Then the Control title is shown + expect(await screen.findByText(TestHelper.translateLocal('workspace.upgrade.commonFeatures.title'))).toBeTruthy(); + + // And the Corporate (Control) annual price is shown + const corporatePrice = convertToShortDisplayString( + CONST.SUBSCRIPTION_PRICES[CONST.PAYMENT_CARD_CURRENCY.USD][CONST.POLICY.TYPE.CORPORATE][CONST.SUBSCRIPTION.TYPE.ANNUAL], + CONST.PAYMENT_CARD_CURRENCY.USD, + ); + expect(await screen.findByText(corporatePrice, {exact: false})).toBeTruthy(); + + unmount(); + await waitForBatchedUpdatesWithAct(); + }); + it("should show the upgrade corporate plan price is in the user's local currency", async () => { // Team policy which the user can upgrade to corporate const policy = LHNTestUtils.getFakePolicy(); From 68a2b0a3301d834ef0d978c6b872b505821942da Mon Sep 17 00:00:00 2001 From: "Abdelrahman Khattab (via MelvinBot)" Date: Mon, 15 Jun 2026 00:03:17 +0000 Subject: [PATCH 3/3] Make explicit upgradePlanType authoritative over feature heuristics for 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 --- src/pages/workspace/upgrade/UpgradeIntro.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/upgrade/UpgradeIntro.tsx b/src/pages/workspace/upgrade/UpgradeIntro.tsx index 4ba26a9b0d76..2d077c4970de 100644 --- a/src/pages/workspace/upgrade/UpgradeIntro.tsx +++ b/src/pages/workspace/upgrade/UpgradeIntro.tsx @@ -60,7 +60,9 @@ function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizi const formattedPrice = useMemo(() => { const upgradeCurrency = Object.hasOwn(CONST.SUBSCRIPTION_PRICES, preferredCurrency) ? preferredCurrency : CONST.PAYMENT_CARD_CURRENCY.USD; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const shouldUseTeamPricing = isCategorizing || isDistanceRateUpgrade || isReporting || isSubmitFeature || upgradePlanType === CONST.POLICY.TYPE.TEAM; + const matchesTeamPricingHeuristics = isCategorizing || isDistanceRateUpgrade || isReporting || isSubmitFeature || upgradePlanType === CONST.POLICY.TYPE.TEAM; + // An explicit upgradePlanType (chosen in the Plan RHP) is authoritative over the feature-based heuristics, so pricing matches the plan title shown in GenericFeaturesView. + const shouldUseTeamPricing = upgradePlanType === CONST.POLICY.TYPE.CORPORATE ? false : matchesTeamPricingHeuristics; return `${convertToShortDisplayString( CONST.SUBSCRIPTION_PRICES[upgradeCurrency][shouldUseTeamPricing ? CONST.POLICY.TYPE.TEAM : CONST.POLICY.TYPE.CORPORATE][CONST.SUBSCRIPTION.TYPE.ANNUAL], upgradeCurrency,