diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 9abf1a8d2639..8ee2b136ef21 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -942,6 +942,8 @@ const ONYXKEYS = { NEW_CHAT_NAME_FORM_DRAFT: 'newChatNameFormDraft', SUBSCRIPTION_SIZE_FORM: 'subscriptionSizeForm', SUBSCRIPTION_SIZE_FORM_DRAFT: 'subscriptionSizeFormDraft', + SUBSCRIPTION_EXPENSIFY_CODE_FORM: 'subscriptionExpensifyCodeForm', + SUBSCRIPTION_EXPENSIFY_CODE_FORM_DRAFT: 'subscriptionExpensifyCodeFormDraft', ISSUE_NEW_EXPENSIFY_CARD_FORM: 'issueNewExpensifyCard', ISSUE_NEW_EXPENSIFY_CARD_FORM_DRAFT: 'issueNewExpensifyCardDraft', ADD_NEW_CARD_FEED_FORM: 'addNewCardFeed', @@ -1106,6 +1108,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.WORKSPACE_TIME_TRACKING_DEFAULT_RATE_FORM]: FormTypes.WorkspaceTimeTrackingDefaultRateForm; [ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm; [ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm; + [ONYXKEYS.FORMS.SUBSCRIPTION_EXPENSIFY_CODE_FORM]: FormTypes.SubscriptionExpensifyCodeForm; [ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM]: FormTypes.IssueNewExpensifyCardForm; [ONYXKEYS.FORMS.ADD_NEW_CARD_FEED_FORM]: FormTypes.AddNewCardFeedForm; [ONYXKEYS.FORMS.ASSIGN_CARD_FORM]: FormTypes.AssignCardForm; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 4f22f01b50d4..69770cfc95e5 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -284,6 +284,7 @@ const ROUTES = { getRoute: (canChangeSize: 0 | 1) => `settings/subscription/subscription-size?canChangeSize=${canChangeSize as number}` as const, }, SETTINGS_SUBSCRIPTION_SETTINGS_DETAILS: 'settings/subscription/details', + SETTINGS_SUBSCRIPTION_EXPENSIFY_CODE: 'settings/subscription/details/expensify-code', SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD: 'settings/subscription/add-payment-card', SETTINGS_SUBSCRIPTION_CHANGE_BILLING_CURRENCY: 'settings/subscription/change-billing-currency', SETTINGS_SUBSCRIPTION_CHANGE_PAYMENT_CURRENCY: 'settings/subscription/add-payment-card/change-payment-currency', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 2f5062cfd97c..3c011ed78872 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -233,6 +233,7 @@ const SCREENS = { SUBSCRIPTION: { ROOT: 'Settings_Subscription', SIZE: 'Settings_Subscription_Size', + EXPENSIFY_CODE: 'Settings_Subscription_ExpensifyCode', SETTINGS_DETAILS: 'Settings_Subscription_Details', ADD_PAYMENT_CARD: 'Settings_Subscription_Add_Payment_Card', DISABLE_AUTO_RENEW_SURVEY: 'Settings_Subscription_DisableAutoRenewSurvey', diff --git a/src/languages/de.ts b/src/languages/de.ts index e1d5bb16996b..565efe274029 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -8058,10 +8058,19 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und security: 'Expensify ist PCI-DSS-konform, verwendet eine Verschlüsselung auf Bankniveau und setzt redundante Infrastruktur ein, um Ihre Daten zu schützen.', learnMoreAboutSecurity: 'Erfahren Sie mehr über unsere Sicherheit.', }, + expensifyCode: { + title: 'Expensify-Code', + discountCode: 'Rabattcode', + enterCode: 'Geben Sie einen Expensify-Code ein, um ihn auf Ihr Abonnement anzuwenden.', + apply: 'Anwenden', + error: { + invalid: 'Dieser Code ist ungültig', + }, + }, subscriptionSettings: { title: 'Abonnementeinstellungen', - summary: ({subscriptionType, subscriptionSize, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => - `Abonnementstyp: ${subscriptionType}, Abonnementgröße: ${subscriptionSize}, Automatische Verlängerung: ${autoRenew}, Automatische jährliche Sitzplatzerhöhung: ${autoIncrease}`, + summary: ({subscriptionType, subscriptionSize, expensifyCode, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => + `Abonnementstyp: ${subscriptionType}, Abonnementgröße: ${subscriptionSize}${expensifyCode ? `, Expensify-Code: ${expensifyCode}` : ''}, Automatische Verlängerung: ${autoRenew}, Automatische jährliche Sitzplatzerhöhung: ${autoIncrease}`, none: 'keine', on: 'an', off: 'aus', diff --git a/src/languages/en.ts b/src/languages/en.ts index cbb014028c6f..469cf30ce0d2 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7999,10 +7999,19 @@ const translations = { security: 'Expensify is PCI-DSS compliant, uses bank-level encryption, and utilizes redundant infrastructure to protect your data.', learnMoreAboutSecurity: 'Learn more about our security.', }, + expensifyCode: { + title: 'Expensify code', + discountCode: 'Discount code', + enterCode: 'Enter an Expensify code to apply to your subscription.', + apply: 'Apply', + error: { + invalid: 'This code is invalid', + }, + }, subscriptionSettings: { title: 'Subscription settings', - summary: ({subscriptionType, subscriptionSize, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => - `Subscription type: ${subscriptionType}, Subscription size: ${subscriptionSize}, Auto renew: ${autoRenew}, Auto increase annual seats: ${autoIncrease}`, + summary: ({subscriptionType, subscriptionSize, expensifyCode, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => + `Subscription type: ${subscriptionType}, Subscription size: ${subscriptionSize}${expensifyCode ? `, Expensify code: ${expensifyCode}` : ''}, Auto renew: ${autoRenew}, Auto increase annual seats: ${autoIncrease}`, none: 'none', on: 'on', off: 'off', diff --git a/src/languages/es.ts b/src/languages/es.ts index 5c02f832b30d..210b57a770ce 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4,7 +4,14 @@ import CONST from '@src/CONST'; import type {OriginalMessageSettlementAccountLocked, PolicyRulesModifiedFields} from '@src/types/onyx/OriginalMessage'; import ObjectUtils from '@src/types/utils/ObjectUtils'; import type en from './en'; -import type {CreatedReportForUnapprovedTransactionsParams, PaidElsewhereParams, SplitDateRangeParams, UpdatedPolicyTagParams, ViolationsRterParams} from './params'; +import type { + CreatedReportForUnapprovedTransactionsParams, + PaidElsewhereParams, + SplitDateRangeParams, + SubscriptionSettingsSummaryParams, + UpdatedPolicyTagParams, + ViolationsRterParams, +} from './params'; import type {TranslationDeepObject} from './types'; /* eslint-disable max-len */ @@ -8200,10 +8207,19 @@ ${amount} para ${merchant} - ${date}`, security: 'Expensify es PCI-DSS obediente, utiliza cifrado a nivel bancario, y emplea infraestructura redundante para proteger tus datos.', learnMoreAboutSecurity: 'Obtén más información sobre nuestra seguridad.', }, + expensifyCode: { + title: 'Código Expensify', + discountCode: 'Código de descuento', + enterCode: 'Introduce un código Expensify para aplicarlo a tu suscripción.', + apply: 'Aplicar', + error: { + invalid: 'Este código no es válido', + }, + }, subscriptionSettings: { title: 'Configuración de suscripción', - summary: ({subscriptionType, subscriptionSize, autoRenew, autoIncrease}) => - `Tipo de suscripción: ${subscriptionType}, Tamaño de suscripción: ${subscriptionSize}, Renovación automática: ${autoRenew}, Aumento automático de asientos anuales: ${autoIncrease}`, + summary: ({subscriptionType, subscriptionSize, expensifyCode, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => + `Tipo de suscripción: ${subscriptionType}, Tamaño de suscripción: ${subscriptionSize}${expensifyCode ? `, Código Expensify: ${expensifyCode}` : ''}, Renovación automática: ${autoRenew}, Aumento automático de asientos anuales: ${autoIncrease}`, none: 'ninguno', on: 'activado', off: 'desactivado', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index c4df5617f37d..8223391e1568 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -8077,10 +8077,19 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip security: 'Expensify est conforme à la norme PCI-DSS, utilise un chiffrement de niveau bancaire et une infrastructure redondante pour protéger vos données.', learnMoreAboutSecurity: 'En savoir plus sur notre sécurité.', }, + expensifyCode: { + title: 'Code Expensify', + discountCode: 'Code de réduction', + enterCode: 'Saisissez un code Expensify à appliquer à votre abonnement.', + apply: 'Appliquer', + error: { + invalid: "Ce code n'est pas valide", + }, + }, subscriptionSettings: { title: 'Paramètres d’abonnement', - summary: ({subscriptionType, subscriptionSize, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => - `Type d’abonnement : ${subscriptionType}, Taille de l’abonnement : ${subscriptionSize}, Renouvellement automatique : ${autoRenew}, Augmentation automatique des licences annuelles : ${autoIncrease}`, + summary: ({subscriptionType, subscriptionSize, expensifyCode, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => + `Type d’abonnement : ${subscriptionType}, Taille de l’abonnement : ${subscriptionSize}${expensifyCode ? `, Code Expensify : ${expensifyCode}` : ''}, Renouvellement automatique : ${autoRenew}, Augmentation automatique des licences annuelles : ${autoIncrease}`, none: 'aucun', on: 'activé', off: 'désactivé', diff --git a/src/languages/it.ts b/src/languages/it.ts index a6608ff71e75..882430e40a84 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -8043,10 +8043,19 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo security: 'Expensify è conforme allo standard PCI-DSS, utilizza la crittografia a livello bancario e si avvale di un’infrastruttura ridondante per proteggere i tuoi dati.', learnMoreAboutSecurity: 'Scopri di più sulla nostra sicurezza.', }, + expensifyCode: { + title: 'Codice Expensify', + discountCode: 'Codice sconto', + enterCode: 'Inserisci un codice Expensify da applicare al tuo abbonamento.', + apply: 'Applica', + error: { + invalid: 'Questo codice non è valido', + }, + }, subscriptionSettings: { title: 'Impostazioni abbonamento', - summary: ({subscriptionType, subscriptionSize, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => - `Tipo di abbonamento: ${subscriptionType}, Dimensione abbonamento: ${subscriptionSize}, Rinnovo automatico: ${autoRenew}, Aumento automatico dei posti annuali: ${autoIncrease}`, + summary: ({subscriptionType, subscriptionSize, expensifyCode, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => + `Tipo di abbonamento: ${subscriptionType}, Dimensione abbonamento: ${subscriptionSize}${expensifyCode ? `, Codice Expensify: ${expensifyCode}` : ''}, Rinnovo automatico: ${autoRenew}, Aumento automatico dei posti annuali: ${autoIncrease}`, none: 'nessuno', on: 'attivo', off: 'disattivato', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 468afebb2047..b4f11a2c5590 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7963,13 +7963,22 @@ ${reportName} paymentCard: { addPaymentCard: '支払カードを追加', enterPaymentCardDetails: '支払いカードの詳細を入力してください', - security: 'Expensify は PCI-DSS に準拠し、銀行レベルの暗号化を使用し、冗長化されたインフラストラクチャでお客様のデータを保護しています。', - learnMoreAboutSecurity: 'セキュリティの詳細をご覧ください。', + security: 'Expensify は PCI-DSS に準拠し、銀行レベルの暗号化を使用し、お客様のデータを保護するために冗長化されたインフラストラクチャを活用しています。', + learnMoreAboutSecurity: '当社のセキュリティについて詳しく見る', + }, + expensifyCode: { + title: 'Expensifyコード', + discountCode: '割引コード', + enterCode: 'サブスクリプションに適用するExpensifyコードを入力してください。', + apply: '適用', + error: { + invalid: 'このコードは無効です', + }, }, subscriptionSettings: { title: 'サブスクリプション設定', - summary: ({subscriptionType, subscriptionSize, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => - `サブスクリプションタイプ: ${subscriptionType}、サブスクリプション規模: ${subscriptionSize}、自動更新: ${autoRenew}、年間席数の自動増加: ${autoIncrease}`, + summary: ({subscriptionType, subscriptionSize, expensifyCode, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => + `サブスクリプションタイプ: ${subscriptionType}、サブスクリプション規模: ${subscriptionSize}${expensifyCode ? `、Expensifyコード: ${expensifyCode}` : ''}、自動更新: ${autoRenew}、年間席数の自動増加: ${autoIncrease}`, none: 'なし', on: 'オン', off: 'オフ', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index ae7d8119ec4c..37fcea98e987 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -8026,10 +8026,19 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar security: 'Expensify is PCI-DSS-conform, gebruikt encryptie op bankniveau en maakt gebruik van redundante infrastructuur om je gegevens te beschermen.', learnMoreAboutSecurity: 'Meer informatie over onze beveiliging.', }, + expensifyCode: { + title: 'Expensify-code', + discountCode: 'Kortingscode', + enterCode: 'Voer een Expensify-code in om toe te passen op je abonnement.', + apply: 'Toepassen', + error: { + invalid: 'Deze code is ongeldig', + }, + }, subscriptionSettings: { title: 'Abonnementsinstellingen', - summary: ({subscriptionType, subscriptionSize, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => - `Abonnementstype: ${subscriptionType}, Abonnementsomvang: ${subscriptionSize}, Automatisch verlengen: ${autoRenew}, Automatisch jaarlijkse seats verhogen: ${autoIncrease}`, + summary: ({subscriptionType, subscriptionSize, expensifyCode, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => + `Abonnementstype: ${subscriptionType}, Abonnementsomvang: ${subscriptionSize}${expensifyCode ? `, Expensify-code: ${expensifyCode}` : ''}, Automatisch verlengen: ${autoRenew}, Automatisch jaarlijkse seats verhogen: ${autoIncrease}`, none: 'geen', on: 'aan', off: 'uit', diff --git a/src/languages/params.ts b/src/languages/params.ts index 8eea93df1d9c..f6f0dc7c61bf 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -360,7 +360,7 @@ type SubscriptionCommitmentParams = {size: number; date: string}; type SubscriptionSettingsSaveUpToParams = {amountWithCurrency: string}; -type SubscriptionSettingsSummaryParams = {subscriptionType: string; subscriptionSize: string; autoRenew: string; autoIncrease: string}; +type SubscriptionSettingsSummaryParams = {subscriptionType: string; subscriptionSize: string; expensifyCode: string; autoRenew: string; autoIncrease: string}; type SubscriptionSettingsRenewsOnParams = {date: string}; diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 76640c6d6bc2..d5893e4f6653 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -8005,13 +8005,22 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i paymentCard: { addPaymentCard: 'Dodaj kartę płatniczą', enterPaymentCardDetails: 'Wprowadź dane swojej karty płatniczej', - security: 'Expensify jest zgodny ze standardem PCI-DSS, używa szyfrowania na poziomie bankowym i wykorzystuje redundantną infrastrukturę, aby chronić Twoje dane.', - learnMoreAboutSecurity: 'Dowiedz się więcej o naszym zabezpieczeniu.', + security: 'Expensify jest zgodny z PCI-DSS, używa szyfrowania na poziomie bankowym i wykorzystuje redundantną infrastrukturę, aby chronić Twoje dane.', + learnMoreAboutSecurity: 'Dowiedz się więcej o naszych zabezpieczeniach.', + }, + expensifyCode: { + title: 'Kod Expensify', + discountCode: 'Kod rabatowy', + enterCode: 'Wprowadź kod Expensify, aby zastosować go do swojej subskrypcji.', + apply: 'Zastosuj', + error: { + invalid: 'Ten kod jest nieprawidłowy', + }, }, subscriptionSettings: { title: 'Ustawienia subskrypcji', - summary: ({subscriptionType, subscriptionSize, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => - `Typ subskrypcji: ${subscriptionType}, Rozmiar subskrypcji: ${subscriptionSize}, Automatyczne odnawianie: ${autoRenew}, Automatyczne zwiększanie rocznych miejsc: ${autoIncrease}`, + summary: ({subscriptionType, subscriptionSize, expensifyCode, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => + `Typ subskrypcji: ${subscriptionType}, Rozmiar subskrypcji: ${subscriptionSize}${expensifyCode ? `, Kod Expensify: ${expensifyCode}` : ''}, Automatyczne odnawianie: ${autoRenew}, Automatyczne zwiększanie rocznych miejsc: ${autoIncrease}`, none: 'brak', on: 'włączony', off: 'wyłączone', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index bfbf252e2870..c0c9d9f796b7 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -8011,10 +8011,19 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e security: 'O Expensify é compatível com PCI-DSS, usa criptografia em nível bancário e utiliza infraestrutura redundante para proteger seus dados.', learnMoreAboutSecurity: 'Saiba mais sobre nossa segurança.', }, + expensifyCode: { + title: 'Código Expensify', + discountCode: 'Código de desconto', + enterCode: 'Insira um código Expensify para aplicar à sua assinatura.', + apply: 'Aplicar', + error: { + invalid: 'Este código é inválido', + }, + }, subscriptionSettings: { title: 'Configurações de assinatura', - summary: ({subscriptionType, subscriptionSize, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => - `Tipo de assinatura: ${subscriptionType}, Tamanho da assinatura: ${subscriptionSize}, Renovação automática: ${autoRenew}, Aumento automático de assentos anuais: ${autoIncrease}`, + summary: ({subscriptionType, subscriptionSize, expensifyCode, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => + `Tipo de assinatura: ${subscriptionType}, Tamanho da assinatura: ${subscriptionSize}${expensifyCode ? `, Código Expensify: ${expensifyCode}` : ''}, Renovação automática: ${autoRenew}, Aumento automático de assentos anuais: ${autoIncrease}`, none: 'nenhum', on: 'ativado', off: 'desligado', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index eeebdcd2f393..635dc5f524ab 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7848,10 +7848,19 @@ ${reportName} security: 'Expensify 符合 PCI-DSS 标准,使用银行级加密,并采用冗余基础设施来保护您的数据。', learnMoreAboutSecurity: '详细了解我们的安全措施。', }, + expensifyCode: { + title: 'Expensify代码', + discountCode: '折扣代码', + enterCode: '输入Expensify代码以应用于您的订阅。', + apply: '应用', + error: { + invalid: '此代码无效', + }, + }, subscriptionSettings: { title: '订阅设置', - summary: ({subscriptionType, subscriptionSize, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => - `订阅类型:${subscriptionType},订阅规模:${subscriptionSize},自动续订:${autoRenew},年度席位自动增加:${autoIncrease}`, + summary: ({subscriptionType, subscriptionSize, expensifyCode, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => + `订阅类型:${subscriptionType},订阅规模:${subscriptionSize}${expensifyCode ? `,Expensify代码:${expensifyCode}` : ''},自动续订:${autoRenew},年度席位自动增加:${autoIncrease}`, none: '无', on: '开在', off: '关关闭', diff --git a/src/libs/API/parameters/SetPromoCodeParams.ts b/src/libs/API/parameters/SetPromoCodeParams.ts new file mode 100644 index 000000000000..88e3a358f809 --- /dev/null +++ b/src/libs/API/parameters/SetPromoCodeParams.ts @@ -0,0 +1,4 @@ +type SetPromoCodeParams = { + expensifyCode: string; +}; +export default SetPromoCodeParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index eae058b6bd78..fa59e8ed087c 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -290,6 +290,7 @@ export type {default as UpdateSubscriptionAutoRenewParams} from './UpdateSubscri export type {default as UpdateSubscriptionAddNewUsersAutomaticallyParams} from './UpdateSubscriptionAddNewUsersAutomaticallyParams'; export type {default as GenerateSpotnanaTokenParams} from './GenerateSpotnanaTokenParams'; export type {default as UpdateSubscriptionSizeParams} from './UpdateSubscriptionSizeParams'; +export type {default as SetPromoCodeParams} from './SetPromoCodeParams'; export type {default as ReportExportParams} from './ReportExportParams'; export type {default as MarkAsExportedParams} from './MarkAsExportedParams'; export type {default as UpgradeToCorporateParams} from './UpgradeToCorporateParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index f3b7682cf052..99870c2d6c13 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -450,6 +450,7 @@ const WRITE_COMMANDS = { UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_CREDIT_CARD_CHARGE_EXPORT_DEFAULT_VENDOR: 'UpdateSageIntacctNonreimbursableExpensesCreditCardChargeExportDefaultVendor', UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_EXPORT_ACCOUNT: 'UpdateSageIntacctNonreimbursableExpensesExportAccount', UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_EXPORT_VENDOR: 'UpdateSageIntacctNonreimbursableExpensesExportVendor', + SET_PROMO_CODE: 'User_SetPromoCode', REQUEST_TAX_EXEMPTION: 'RequestTaxExemption', EXPORT_SEARCH_ITEMS_TO_CSV: 'ExportSearchToCSV', QUEUE_EXPORT_SEARCH_ITEMS_TO_CSV: 'QueueExportSearchToCSV', @@ -929,6 +930,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_AUTO_RENEW]: Parameters.UpdateSubscriptionAutoRenewParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_ADD_NEW_USERS_AUTOMATICALLY]: Parameters.UpdateSubscriptionAddNewUsersAutomaticallyParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_SIZE]: Parameters.UpdateSubscriptionSizeParams; + [WRITE_COMMANDS.SET_PROMO_CODE]: Parameters.SetPromoCodeParams; [WRITE_COMMANDS.REQUEST_TAX_EXEMPTION]: null; [WRITE_COMMANDS.GET_CORPAY_BANK_ACCOUNT_FIELDS]: Parameters.GetCorpayBankAccountFieldsParams; [WRITE_COMMANDS.SAVE_CORPAY_ONBOARDING_BENEFICIAL_OWNER]: Parameters.SaveCorpayOnboardingBeneficialOwnerParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index dc6f2dc04db2..523bb837eca0 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -466,6 +466,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Profile/CustomStatus/SetTimePage').default, [SCREENS.SETTINGS.PROFILE.VACATION_DELEGATE]: () => require('../../../../pages/settings/Profile/CustomStatus/VacationDelegatePage').default, [SCREENS.SETTINGS.SUBSCRIPTION.SIZE]: () => require('../../../../pages/settings/Subscription/SubscriptionSize').default, + [SCREENS.SETTINGS.SUBSCRIPTION.EXPENSIFY_CODE]: () => require('../../../../pages/settings/Subscription/ExpensifyCodePage').default, [SCREENS.SETTINGS.SUBSCRIPTION.SETTINGS_DETAILS]: () => require('../../../../pages/settings/Subscription/SubscriptionSettings').default, [SCREENS.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY]: () => require('../../../../pages/settings/Subscription/DisableAutoRenewSurveyPage').default, [SCREENS.SETTINGS.SUBSCRIPTION.REQUEST_EARLY_CANCELLATION]: () => require('../../../../pages/settings/Subscription/RequestEarlyCancellationPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts index e50234509d29..257209ed4ec4 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts @@ -108,6 +108,7 @@ const SETTINGS_TO_RHP: Partial['config'] = { canChangeSize: Number, }, }, + [SCREENS.SETTINGS.SUBSCRIPTION.EXPENSIFY_CODE]: { + path: ROUTES.SETTINGS_SUBSCRIPTION_EXPENSIFY_CODE, + }, [SCREENS.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY]: { path: ROUTES.SETTINGS_SUBSCRIPTION_DISABLE_AUTO_RENEW_SURVEY, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 38ea1b1f4047..6aea90b3c983 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -615,6 +615,7 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.SUBSCRIPTION.SIZE]: { canChangeSize: 0 | 1; }; + [SCREENS.SETTINGS.SUBSCRIPTION.EXPENSIFY_CODE]: undefined; [SCREENS.SETTINGS.SUBSCRIPTION.SETTINGS_DETAILS]: undefined; [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: undefined; [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY]: undefined; diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts index 679579b9d4ea..e838ea9d1481 100644 --- a/src/libs/actions/Subscription.ts +++ b/src/libs/actions/Subscription.ts @@ -3,6 +3,7 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import type {CancelBillingSubscriptionParams, UpdateSubscriptionAddNewUsersAutomaticallyParams, UpdateSubscriptionAutoRenewParams, UpdateSubscriptionTypeParams} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import CONST from '@src/CONST'; import type {FeedbackSurveyOptionID, SubscriptionType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -294,6 +295,63 @@ function requestTaxExempt() { API.write(WRITE_COMMANDS.REQUEST_TAX_EXEMPTION, null); } +function applyExpensifyCode(expensifyCode: string) { + const optimisticData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.SUBSCRIPTION_EXPENSIFY_CODE_FORM, + value: { + isLoading: true, + errors: null, + errorFields: { + expensifyCode: null, + }, + }, + }, + ]; + + const successData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.SUBSCRIPTION_EXPENSIFY_CODE_FORM, + value: { + isLoading: false, + expensifyCode: '', + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION, + value: { + expensifyCode, + }, + }, + ]; + + const failureData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.SUBSCRIPTION_EXPENSIFY_CODE_FORM, + value: { + isLoading: false, + errorFields: { + expensifyCode: getMicroSecondOnyxErrorWithTranslationKey('subscription.expensifyCode.error.invalid'), + }, + }, + }, + ]; + + const parameters = { + expensifyCode, + }; + + API.write(WRITE_COMMANDS.SET_PROMO_CODE, parameters, { + optimisticData, + successData, + failureData, + }); +} + export { openSubscriptionPage, updateSubscriptionAutoRenew, @@ -304,4 +362,5 @@ export { clearOutstandingBalance, cancelBillingSubscription, requestTaxExempt, + applyExpensifyCode, }; diff --git a/src/pages/settings/Subscription/ExpensifyCodePage/index.native.tsx b/src/pages/settings/Subscription/ExpensifyCodePage/index.native.tsx new file mode 100644 index 000000000000..2f6e89d74406 --- /dev/null +++ b/src/pages/settings/Subscription/ExpensifyCodePage/index.native.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import ScreenWrapper from '@components/ScreenWrapper'; + +function ExpensifyCodePage() { + return ( + + + + ); +} + +ExpensifyCodePage.displayName = 'ExpensifyCodePage'; + +export default ExpensifyCodePage; diff --git a/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx b/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx new file mode 100644 index 000000000000..f7bf1770547e --- /dev/null +++ b/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx @@ -0,0 +1,111 @@ +import {useFocusEffect} from '@react-navigation/native'; +import React, {useCallback, useEffect, useState} from 'react'; +import {View} from 'react-native'; +import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {clearDraftValues, clearErrorFields} from '@libs/actions/FormActions'; +import Navigation from '@libs/Navigation/Navigation'; +import {getFieldRequiredErrors} from '@libs/ValidationUtils'; +import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; +import {applyExpensifyCode} from '@userActions/Subscription'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/SubscriptionExpensifyCodeForm'; + +function ExpensifyCodePage() { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {inputCallbackRef} = useAutoFocusInput(); + const [hasSubmitted, setHasSubmitted] = useState(false); + const [subscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION, {canBeMissing: true}); + const isExpensifyCodeApplied = !!subscription?.expensifyCode; + + const defaultValues = { + [INPUT_IDS.EXPENSIFY_CODE]: '', + }; + + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + return getFieldRequiredErrors(values, [INPUT_IDS.EXPENSIFY_CODE], translate); + }, + [translate], + ); + + const handleSubmit = useCallback((values: FormOnyxValues) => { + const expensifyCode = values[INPUT_IDS.EXPENSIFY_CODE]; + applyExpensifyCode(expensifyCode); + clearDraftValues(ONYXKEYS.FORMS.SUBSCRIPTION_EXPENSIFY_CODE_FORM); + setHasSubmitted(true); + }, []); + + useFocusEffect( + useCallback(() => { + clearErrorFields(ONYXKEYS.FORMS.SUBSCRIPTION_EXPENSIFY_CODE_FORM); + }, []), + ); + + useEffect(() => { + if (!hasSubmitted || !isExpensifyCodeApplied) { + return; + } + Navigation.goBack(); + }, [hasSubmitted, isExpensifyCodeApplied]); + + if (isExpensifyCodeApplied || subscription?.isSecretPromoCode) { + return ; + } + + return ( + + + + + + {translate('subscription.expensifyCode.enterCode')} + + + + + + ); +} + +ExpensifyCodePage.displayName = 'ExpensifyCodePage'; + +export default ExpensifyCodePage; diff --git a/src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCardActionButton.tsx b/src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCardActionButton.tsx index 45e5eb6c1910..9fc273687d4f 100644 --- a/src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCardActionButton.tsx +++ b/src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCardActionButton.tsx @@ -136,6 +136,7 @@ function SubscriptionPlanCardActionButton({subscriptionPlan, isFromComparisonMod const subscriptionType = isAnnual ? translate('subscription.subscriptionSettings.annual') : translate('subscription.details.payPerUse'); const subscriptionSize = `${privateSubscription?.userCount ?? translate('subscription.subscriptionSettings.none')}`; const autoRenew = privateSubscription?.autoRenew ? translate('subscription.subscriptionSettings.on') : translate('subscription.subscriptionSettings.off'); + const expensifyCode = privateSubscription?.isSecretPromoCode ? '' : (privateSubscription?.expensifyCode ?? ''); return ( Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_SETTINGS_DETAILS)} numberOfLinesTitle={3} - title={translate('subscription.subscriptionSettings.summary', {subscriptionType, subscriptionSize, autoRenew, autoIncrease})} + title={translate('subscription.subscriptionSettings.summary', {subscriptionType, subscriptionSize, expensifyCode, autoRenew, autoIncrease})} /> ); } diff --git a/src/pages/settings/Subscription/SubscriptionSettings/index.tsx b/src/pages/settings/Subscription/SubscriptionSettings/index.tsx index bf9ab3ec4c76..ec4028e3082f 100644 --- a/src/pages/settings/Subscription/SubscriptionSettings/index.tsx +++ b/src/pages/settings/Subscription/SubscriptionSettings/index.tsx @@ -62,6 +62,8 @@ function SubscriptionSettings() { const isAnnual = privateSubscription?.type === CONST.SUBSCRIPTION.TYPE.ANNUAL; const [privateTaxExempt] = useOnyx(ONYXKEYS.NVP_PRIVATE_TAX_EXEMPT, {canBeMissing: true}); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID, {canBeMissing: true}); + const isExpensifyCodeApplied = !!privateSubscription?.expensifyCode; + const shouldShowExpensifyCodeSection = !privateSubscription?.isSecretPromoCode; const subscriptionPrice = getSubscriptionPrice(subscriptionPlan, preferredCurrency, privateSubscription?.type, hasTeam2025Pricing); const priceDetails = translate(`subscription.yourPlan.${subscriptionPlan === CONST.POLICY.TYPE.CORPORATE ? 'control' : 'collect'}.${isAnnual ? 'priceAnnual' : 'pricePayPerUse'}`, { lower: convertToShortDisplayString(subscriptionPrice, preferredCurrency), @@ -89,6 +91,18 @@ function SubscriptionSettings() { } Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_SIZE.getRoute(1)); }; + + const onExpensifyCodePress = () => { + if (isExpensifyCodeApplied) { + return; + } + if (isActingAsDelegate) { + showDelegateNoAccessModal(); + return; + } + + Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_EXPENSIFY_CODE); + }; const illustrations = useMemoizedLazyIllustrations(['SubscriptionAnnual', 'SubscriptionPPU']); const options: Array> = [ @@ -256,6 +270,18 @@ function SubscriptionSettings() { ) : null} + + {shouldShowExpensifyCodeSection && ( + + )} diff --git a/src/types/form/SubscriptionExpensifyCodeForm.ts b/src/types/form/SubscriptionExpensifyCodeForm.ts new file mode 100644 index 000000000000..1052eff8cd3b --- /dev/null +++ b/src/types/form/SubscriptionExpensifyCodeForm.ts @@ -0,0 +1,13 @@ +import type {ValueOf} from 'type-fest'; +import type Form from './Form'; + +const INPUT_IDS = { + EXPENSIFY_CODE: 'expensifyCode', +} as const; + +type InputID = ValueOf; + +type SubscriptionExpensifyCodeForm = Form; + +export type {SubscriptionExpensifyCodeForm}; +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index 81f43062ca42..bdda18247a43 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -68,6 +68,7 @@ export type {WalletAdditionalDetailsForm} from './WalletAdditionalDetailsForm'; export type {NewChatNameForm} from './NewChatNameForm'; export type {SageIntacctDimensionForm} from './SageIntacctDimensionsForm'; export type {SubscriptionSizeForm} from './SubscriptionSizeForm'; +export type {SubscriptionExpensifyCodeForm} from './SubscriptionExpensifyCodeForm'; export type {WorkspaceReportFieldForm} from './WorkspaceReportFieldForm'; export type {SageIntactCredentialsForm} from './SageIntactCredentialsForm'; export type {NetSuiteCustomFieldForm} from './NetSuiteCustomFieldForm'; diff --git a/src/types/onyx/PrivateSubscription.ts b/src/types/onyx/PrivateSubscription.ts index cd3282a87973..d7c68d994477 100644 --- a/src/types/onyx/PrivateSubscription.ts +++ b/src/types/onyx/PrivateSubscription.ts @@ -27,6 +27,12 @@ type PrivateSubscription = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Subscription size */ userCount?: number; + /** Applied Expensify code */ + expensifyCode?: string; + + /** Whether the promo code is secret and should be hidden from account settings */ + isSecretPromoCode?: boolean; + /** An error message */ errors?: OnyxCommon.Errors; diff --git a/tests/ui/ExpensifyCodePageTest.tsx b/tests/ui/ExpensifyCodePageTest.tsx new file mode 100644 index 000000000000..e7964540ffc0 --- /dev/null +++ b/tests/ui/ExpensifyCodePageTest.tsx @@ -0,0 +1,195 @@ +import {PortalProvider} from '@gorhom/portal'; +import {NavigationContainer} from '@react-navigation/native'; +import {act, fireEvent, render, screen, waitFor} from '@testing-library/react-native'; +import React from 'react'; +import Onyx from 'react-native-onyx'; +import ComposeProviders from '@components/ComposeProviders'; +import {LocaleContextProvider} from '@components/LocaleContextProvider'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; +import {CurrentReportIDContextProvider} from '@hooks/useCurrentReportID'; +import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; +import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import ExpensifyCodePage from '@pages/settings/Subscription/ExpensifyCodePage'; +import CONST from '@src/CONST'; +import IntlStore from '@src/languages/IntlStore'; +import ONYXKEYS from '@src/ONYXKEYS'; +import SCREENS from '@src/SCREENS'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; + +jest.mock('@pages/settings/Subscription/ExpensifyCodePage', () => + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + jest.requireActual('@pages/settings/Subscription/ExpensifyCodePage/index.tsx'), +); + +jest.mock('@userActions/Subscription', () => ({ + applyExpensifyCode: jest.fn(), +})); + +jest.mock('@libs/Navigation/Navigation', () => { + const actual = jest.requireActual('@libs/Navigation/Navigation'); + return { + ...actual, + goBack: jest.fn(), + getTopmostReportId: jest.fn(() => undefined), + dismissModal: jest.fn(), + getActiveRoute: jest.fn(() => ''), + }; +}); + +jest.mock('@pages/ErrorPage/NotFoundPage', () => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const {View, Text} = jest.requireActual('react-native'); + function MockErrorPage() { + return ( + + Not Found + + ); + } + return MockErrorPage; +}); + +const {applyExpensifyCode} = jest.requireMock<{applyExpensifyCode: jest.Mock}>('@userActions/Subscription'); + +const Stack = createPlatformStackNavigator(); + +const renderPage = () => + render( + + + + + + + + + , + ); + +describe('ExpensifyCodePage', () => { + beforeAll(async () => { + Onyx.init({keys: ONYXKEYS}); + await IntlStore.load(CONST.LOCALES.EN); + await act(async () => { + await Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.EN); + }); + await waitForBatchedUpdatesWithAct(); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + await act(async () => { + await Onyx.clear(); + await Onyx.set(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION, {expensifyCode: null}); + }); + await waitForBatchedUpdatesWithAct(); + }); + + it('renders the form with title, description and discount code input', async () => { + await TestHelper.signInWithTestUser(); + await waitForBatchedUpdatesWithAct(); + + renderPage(); + await waitForBatchedUpdatesWithAct(); + + expect(screen.getByText(TestHelper.translateLocal('subscription.expensifyCode.title'))).toBeOnTheScreen(); + expect(screen.getByText(TestHelper.translateLocal('subscription.expensifyCode.enterCode'))).toBeOnTheScreen(); + expect(screen.getByTestId('expensify-code-input')).toBeOnTheScreen(); + expect(screen.getByRole('button', {name: TestHelper.translateLocal('subscription.expensifyCode.apply')})).toBeOnTheScreen(); + }); + + it('show validation error when submitting empty code', async () => { + await TestHelper.signInWithTestUser(); + await waitForBatchedUpdatesWithAct(); + + renderPage(); + await waitForBatchedUpdatesWithAct(); + + const applyButton = screen.getByRole('button', {name: TestHelper.translateLocal('subscription.expensifyCode.apply')}); + fireEvent.press(applyButton); + await waitForBatchedUpdatesWithAct(); + + expect(screen.getByText(TestHelper.translateLocal('common.error.fieldRequired'))).toBeOnTheScreen(); + expect(applyExpensifyCode).not.toHaveBeenCalled(); + }); + + it('calls applyExpensifyCode when submitting valid code', async () => { + await TestHelper.signInWithTestUser(); + await waitForBatchedUpdatesWithAct(); + + renderPage(); + await waitForBatchedUpdatesWithAct(); + + const input = screen.getByTestId('expensify-code-input'); + fireEvent.changeText(input, 'HappyDays100Off'); + await waitForBatchedUpdatesWithAct(); + + const applyButton = screen.getByRole('button', {name: TestHelper.translateLocal('subscription.expensifyCode.apply')}); + fireEvent.press(applyButton); + await waitForBatchedUpdatesWithAct(); + + expect(applyExpensifyCode).toHaveBeenCalledTimes(1); + expect(applyExpensifyCode).toHaveBeenCalledWith('HappyDays100Off'); + }); + + it('show NotFoundPage when subscription already has expensifyCode', async () => { + await TestHelper.signInWithTestUser(); + await act(async () => { + await Onyx.merge(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION, {expensifyCode: 'APPLIED_CODE'}); + }); + await waitForBatchedUpdatesWithAct(); + + renderPage(); + await waitForBatchedUpdatesWithAct(); + + expect(screen.getByTestId('NotFoundPage')).toBeOnTheScreen(); + expect(screen.queryByRole('button', {name: TestHelper.translateLocal('subscription.expensifyCode.apply')})).not.toBeOnTheScreen(); + }); + + it('navigate back when expensifyCode is set in subscription after submit', async () => { + await TestHelper.signInWithTestUser(); + await waitForBatchedUpdatesWithAct(); + + renderPage(); + await waitForBatchedUpdatesWithAct(); + + const input = screen.getByTestId('expensify-code-input'); + fireEvent.changeText(input, '2026EarlyAdoption50'); + const applyButton = screen.getByRole('button', {name: TestHelper.translateLocal('subscription.expensifyCode.apply')}); + fireEvent.press(applyButton); + await waitForBatchedUpdatesWithAct(); + + expect(applyExpensifyCode).toHaveBeenCalledWith('2026EarlyAdoption50'); + + await act(async () => { + await Onyx.merge(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION, {expensifyCode: '2026EarlyAdoption50'}); + }); + await waitFor(() => { + expect(Navigation.goBack).toHaveBeenCalled(); + }); + }); + + it('stay on page when expensifyCode is invalid', async () => { + await TestHelper.signInWithTestUser(); + await waitForBatchedUpdatesWithAct(); + + renderPage(); + await waitForBatchedUpdatesWithAct(); + + const input = screen.getByTestId('expensify-code-input'); + fireEvent.changeText(input, 'InvalidCode'); + const applyButton = screen.getByRole('button', {name: TestHelper.translateLocal('subscription.expensifyCode.apply')}); + fireEvent.press(applyButton); + await waitForBatchedUpdatesWithAct(); + + expect(applyExpensifyCode).toHaveBeenCalledWith('InvalidCode'); + + expect(screen.getByRole('button', {name: TestHelper.translateLocal('subscription.expensifyCode.apply')})).toBeOnTheScreen(); + expect(Navigation.goBack).not.toHaveBeenCalled(); + }); +});