From 04b0984bb38901c2bcc4bf150ce506270346c935 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Sun, 18 Jan 2026 13:57:49 +0500 Subject: [PATCH 01/20] Add API types and parameters for expensify code --- src/libs/API/parameters/ApplyExpensifyCodeParams.ts | 4 ++++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 ++ 3 files changed, 7 insertions(+) create mode 100644 src/libs/API/parameters/ApplyExpensifyCodeParams.ts diff --git a/src/libs/API/parameters/ApplyExpensifyCodeParams.ts b/src/libs/API/parameters/ApplyExpensifyCodeParams.ts new file mode 100644 index 000000000000..dced835736d7 --- /dev/null +++ b/src/libs/API/parameters/ApplyExpensifyCodeParams.ts @@ -0,0 +1,4 @@ +type ApplyExpensifyCodeParams = { + expensifyCode: string; +}; +export default ApplyExpensifyCodeParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index b65ee965c525..f55e2f8e0695 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -288,6 +288,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 ApplyExpensifyCodeParams} from './ApplyExpensifyCodeParams'; 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 fc338163b81f..64bdd959b0a4 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -440,6 +440,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', + APPLY_EXPENSIFY_CODE: 'ApplyExpensifyCode', REQUEST_TAX_EXEMPTION: 'RequestTaxExemption', EXPORT_SEARCH_ITEMS_TO_CSV: 'ExportSearchToCSV', QUEUE_EXPORT_SEARCH_ITEMS_TO_CSV: 'QueueExportSearchToCSV', @@ -901,6 +902,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.APPLY_EXPENSIFY_CODE]: Parameters.ApplyExpensifyCodeParams; [WRITE_COMMANDS.REQUEST_TAX_EXEMPTION]: null; [WRITE_COMMANDS.GET_CORPAY_BANK_ACCOUNT_FIELDS]: Parameters.GetCorpayBankAccountFieldsParams; [WRITE_COMMANDS.SAVE_CORPAY_ONBOARDING_BENEFICIAL_OWNER]: Parameters.SaveCorpayOnboardingBeneficialOwnerParams; From 7cb8d7cf1973d49a5ed160b8aebbdb45de51ded1 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Sun, 18 Jan 2026 13:58:06 +0500 Subject: [PATCH 02/20] Add Onyx keys and form types for subscription expensify code --- src/ONYXKEYS.ts | 3 +++ src/types/form/SubscriptionExpensifyCodeForm.ts | 13 +++++++++++++ src/types/form/index.ts | 1 + src/types/onyx/PrivateSubscription.ts | 3 +++ 4 files changed, 20 insertions(+) create mode 100644 src/types/form/SubscriptionExpensifyCodeForm.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 37cf5fb7cac2..0c31946195b5 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -917,6 +917,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', @@ -1065,6 +1067,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.WORKSPACE_INVOICES_COMPANY_WEBSITE_FORM]: FormTypes.WorkspaceInvoicesCompanyWebsiteForm; [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/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 cfb5cb4a795b..2f8a897a3631 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -66,6 +66,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..becb9c0351a1 100644 --- a/src/types/onyx/PrivateSubscription.ts +++ b/src/types/onyx/PrivateSubscription.ts @@ -27,6 +27,9 @@ type PrivateSubscription = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Subscription size */ userCount?: number; + /** Applied Expensify code */ + expensifyCode?: string; + /** An error message */ errors?: OnyxCommon.Errors; From 244e7a782ed8edea50d61c442d397c9c1f6dad80 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Sun, 18 Jan 2026 13:58:29 +0500 Subject: [PATCH 03/20] Add navigation routes and screens --- src/ROUTES.ts | 1 + src/SCREENS.ts | 1 + src/libs/Navigation/types.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index a0c07a34787d..698a965a8642 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -233,6 +233,7 @@ const ROUTES = { route: 'settings/subscription/subscription-size', getRoute: (canChangeSize: 0 | 1) => `settings/subscription/subscription-size?canChangeSize=${canChangeSize as number}` as const, }, + SETTINGS_SUBSCRIPTION_EXPENSIFY_CODE: 'settings/subscription/expensify-code', SETTINGS_SUBSCRIPTION_SETTINGS_DETAILS: 'settings/subscription/details', SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD: 'settings/subscription/add-payment-card', SETTINGS_SUBSCRIPTION_CHANGE_BILLING_CURRENCY: 'settings/subscription/change-billing-currency', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 9f0cd88b172b..ca24b876b989 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -200,6 +200,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/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 4ef171b360e9..dc608f6cdd4e 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -551,6 +551,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; From 52b487c1ffdfaae22a5654f67ba253d39eca3878 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Sun, 18 Jan 2026 13:58:47 +0500 Subject: [PATCH 04/20] Update navigation config --- .../Navigation/AppNavigator/ModalStackNavigators/index.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 6acc1efba7dd..0eeac96432c6 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -407,6 +407,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/config.ts b/src/libs/Navigation/linkingConfig/config.ts index c2d5bf03237e..1002bb1ce6e4 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -433,6 +433,9 @@ const config: LinkingOptions['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, }, From 95f1ac47f76cde2995dbed964891b3b6d49ecbbd Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Sun, 18 Jan 2026 13:59:24 +0500 Subject: [PATCH 05/20] Add applyExpensifyCode function --- src/libs/actions/Subscription.ts | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts index 679579b9d4ea..8a9433dc99c0 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.APPLY_EXPENSIFY_CODE, parameters, { + optimisticData, + successData, + failureData, + }); +} + export { openSubscriptionPage, updateSubscriptionAutoRenew, @@ -304,4 +362,5 @@ export { clearOutstandingBalance, cancelBillingSubscription, requestTaxExempt, + applyExpensifyCode, }; From 64522f1ee930a163da3516561f16eeef7b2557ee Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Sun, 18 Jan 2026 14:00:30 +0500 Subject: [PATCH 06/20] Update subscription settings to include Expensify code option --- .../SubscriptionPlanCardActionButton.tsx | 3 ++- .../SubscriptionSettings/index.tsx | 24 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCardActionButton.tsx b/src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCardActionButton.tsx index 45e5eb6c1910..775878411a3f 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?.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 a94990a345ab..e29e4c3cc37f 100644 --- a/src/pages/settings/Subscription/SubscriptionSettings/index.tsx +++ b/src/pages/settings/Subscription/SubscriptionSettings/index.tsx @@ -61,6 +61,7 @@ function SubscriptionSettings() { const {isActingAsDelegate, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); const isAnnual = privateSubscription?.type === CONST.SUBSCRIPTION.TYPE.ANNUAL; const [privateTaxExempt] = useOnyx(ONYXKEYS.NVP_PRIVATE_TAX_EXEMPT, {canBeMissing: true}); + const hasAppliedExpensifyCode = !!privateSubscription?.expensifyCode; 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), @@ -88,6 +89,18 @@ function SubscriptionSettings() { } Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_SIZE.getRoute(1)); }; + + const onExpensifyCodePress = () => { + if (hasAppliedExpensifyCode) { + return; + } + if (isActingAsDelegate) { + showDelegateNoAccessModal(); + return; + } + + Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_EXPENSIFY_CODE); + }; const illustrations = useMemoizedLazyIllustrations(['SubscriptionAnnual', 'SubscriptionPPU']); const options: Array> = [ @@ -255,6 +268,15 @@ function SubscriptionSettings() { ) : null} + From 29d804a8f6a24cf4a668f1c1375f6d45170adf11 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Sun, 18 Jan 2026 14:02:00 +0500 Subject: [PATCH 07/20] feat: add ExpensifyCodePage --- .../Subscription/ExpensifyCodePage/index.tsx | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/pages/settings/Subscription/ExpensifyCodePage/index.tsx diff --git a/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx b/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx new file mode 100644 index 000000000000..0613d9361399 --- /dev/null +++ b/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx @@ -0,0 +1,98 @@ +import React, {useCallback, useEffect, useState} from 'react'; +import {View} from 'react-native'; +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} from '@libs/actions/FormActions'; +import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {getFieldRequiredErrors} from '@libs/ValidationUtils'; +import {applyExpensifyCode} from '@userActions/Subscription'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/SubscriptionExpensifyCodeForm'; + +type ExpensifyCodePageProps = PlatformStackScreenProps; + +function ExpensifyCodePage({}: ExpensifyCodePageProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {inputCallbackRef} = useAutoFocusInput(); + const [hasSubmitted, setHasSubmitted] = useState(false); + const [subscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION); + + const defaultValues = { + [INPUT_IDS.EXPENSIFY_CODE]: '', + }; + + useEffect(() => { + if (hasSubmitted && subscription?.expensifyCode) { + Navigation.goBack(); + } + }, [hasSubmitted, subscription?.expensifyCode]); + + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + return getFieldRequiredErrors(values, [INPUT_IDS.EXPENSIFY_CODE]); + }, + [], + ); + + const handleSubmit = useCallback((values: FormOnyxValues) => { + const expensifyCode = values[INPUT_IDS.EXPENSIFY_CODE]; + applyExpensifyCode(expensifyCode); + clearDraftValues(ONYXKEYS.FORMS.SUBSCRIPTION_EXPENSIFY_CODE_FORM); + setHasSubmitted(true); + }, []); + + return ( + + + + + {translate('subscription.expensifyCode.enterCode')} + + + + + ); +} + +ExpensifyCodePage.displayName = 'ExpensifyCodePage'; + +export default ExpensifyCodePage; From 6b9f1d15baa5e08f57e6fd79442585c108cdc1b5 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Sun, 18 Jan 2026 14:02:42 +0500 Subject: [PATCH 08/20] Add translations in all locals --- src/languages/de.ts | 13 +++++++++++-- src/languages/en.ts | 13 +++++++++++-- src/languages/es.ts | 14 ++++++++++++-- src/languages/fr.ts | 13 +++++++++++-- src/languages/it.ts | 13 +++++++++++-- src/languages/ja.ts | 13 +++++++++++-- src/languages/nl.ts | 13 +++++++++++-- src/languages/params.ts | 2 +- src/languages/pl.ts | 13 +++++++++++-- src/languages/pt-BR.ts | 13 +++++++++++-- src/languages/zh-hans.ts | 13 +++++++++++-- 11 files changed, 112 insertions(+), 21 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index a22d5ea987c4..a618b1d79473 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7625,10 +7625,19 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard security: 'Expensify ist PCI-DSS-konform, verwendet eine Verschlüsselung auf Bankniveau und nutzt redundante Infrastruktur, 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: 'Ein', off: 'Aus', diff --git a/src/languages/en.ts b/src/languages/en.ts index 44c5b548c5c7..0177bc838897 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7552,10 +7552,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 028c1794638d..a4d910e3e8bf 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -8,6 +8,7 @@ import type { PaidElsewhereParams, RoutedDueToDEWParams, SplitDateRangeParams, + SubscriptionSettingsSummaryParams, ViolationsRterParams, } from './params'; import type {TranslationDeepObject} from './types'; @@ -7707,10 +7708,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 223cfc0dfb59..50e0b6bb5b59 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7633,10 +7633,19 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin security: 'Expensify est conforme à la norme PCI-DSS, utilise un chiffrement de niveau bancaire et s’appuie sur 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 sièges annuels : ${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 sièges annuels : ${autoIncrease}`, none: 'aucun', on: 'Activé', off: 'désactivé', diff --git a/src/languages/it.ts b/src/languages/it.ts index b0b6d39377d8..73e84fe0c7f8 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7609,10 +7609,19 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori 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 88848b5fec6f..221b2e81e27e 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7554,10 +7554,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/languages/nl.ts b/src/languages/nl.ts index 62f4fbfe572a..880ea857ca05 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7597,10 +7597,19 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten security: 'Expensify is PCI-DSS-conform, gebruikt versleuteling 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}, Abonnementsgrootte: ${subscriptionSize}, Automatisch verlengen: ${autoRenew}, Aantal jaarlijkse licenties automatisch verhogen: ${autoIncrease}`, + summary: ({subscriptionType, subscriptionSize, expensifyCode, autoRenew, autoIncrease}: SubscriptionSettingsSummaryParams) => + `Abonnementstype: ${subscriptionType}, Abonnementsgrootte: ${subscriptionSize}${expensifyCode ? `, Expensify-code: ${expensifyCode}` : ''}, Automatisch verlengen: ${autoRenew}, Aantal jaarlijkse licenties automatisch verhogen: ${autoIncrease}`, none: 'geen', on: 'aan', off: 'Uit', diff --git a/src/languages/params.ts b/src/languages/params.ts index cc69497e7434..2b0b6b1ddaeb 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -430,7 +430,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 e8a1cd1ef92b..6757a83b6392 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7581,10 +7581,19 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i 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 rocznej liczby 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 rocznej liczby miejsc: ${autoIncrease}`, none: 'brak', on: 'włączone', off: 'Wył.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index d0be8f8d4f53..d9f781755461 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7583,10 +7583,19 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe security: '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 licenças 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 licenças anuais: ${autoIncrease}`, none: 'nenhum', on: 'Ativado', off: 'Desativado', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 242c23c0652b..f59f4b2363c7 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7421,10 +7421,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: '关', From 241d95c42729b295960d59648ebcb15753b3f89f Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Wed, 4 Feb 2026 16:02:05 +0500 Subject: [PATCH 09/20] change API command to SET_PROMO_CODE --- src/libs/API/types.ts | 4 ++-- src/libs/actions/Subscription.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 963de92057df..d2c87995db1c 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -448,7 +448,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', - APPLY_EXPENSIFY_CODE: 'ApplyExpensifyCode', + SET_PROMO_CODE: 'User_SetPromoCode', REQUEST_TAX_EXEMPTION: 'RequestTaxExemption', EXPORT_SEARCH_ITEMS_TO_CSV: 'ExportSearchToCSV', QUEUE_EXPORT_SEARCH_ITEMS_TO_CSV: 'QueueExportSearchToCSV', @@ -923,7 +923,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.APPLY_EXPENSIFY_CODE]: Parameters.ApplyExpensifyCodeParams; + [WRITE_COMMANDS.SET_PROMO_CODE]: Parameters.ApplyExpensifyCodeParams; [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/actions/Subscription.ts b/src/libs/actions/Subscription.ts index 8a9433dc99c0..e838ea9d1481 100644 --- a/src/libs/actions/Subscription.ts +++ b/src/libs/actions/Subscription.ts @@ -345,7 +345,7 @@ function applyExpensifyCode(expensifyCode: string) { expensifyCode, }; - API.write(WRITE_COMMANDS.APPLY_EXPENSIFY_CODE, parameters, { + API.write(WRITE_COMMANDS.SET_PROMO_CODE, parameters, { optimisticData, successData, failureData, From 135ebb9f14cfc1d6b2d2fbdf55ce09075b7c792a Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Wed, 4 Feb 2026 16:57:29 +0500 Subject: [PATCH 10/20] replace ApplyExpensifyCodeParams with SetPromoCodeParams and update references --- src/languages/es.ts | 9 ++++++++- .../API/parameters/ApplyExpensifyCodeParams.ts | 4 ---- src/libs/API/parameters/SetPromoCodeParams.ts | 4 ++++ src/libs/API/parameters/index.ts | 2 +- src/libs/API/types.ts | 2 +- .../Subscription/ExpensifyCodePage/index.tsx | 14 +++++--------- 6 files changed, 19 insertions(+), 16 deletions(-) delete mode 100644 src/libs/API/parameters/ApplyExpensifyCodeParams.ts create mode 100644 src/libs/API/parameters/SetPromoCodeParams.ts diff --git a/src/languages/es.ts b/src/languages/es.ts index d491bd728337..62e25a9512af 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, RoutedDueToDEWParams, SplitDateRangeParams, ViolationsRterParams} from './params'; +import type { + CreatedReportForUnapprovedTransactionsParams, + PaidElsewhereParams, + RoutedDueToDEWParams, + SplitDateRangeParams, + SubscriptionSettingsSummaryParams, + ViolationsRterParams, +} from './params'; import type {TranslationDeepObject} from './types'; /* eslint-disable max-len */ diff --git a/src/libs/API/parameters/ApplyExpensifyCodeParams.ts b/src/libs/API/parameters/ApplyExpensifyCodeParams.ts deleted file mode 100644 index dced835736d7..000000000000 --- a/src/libs/API/parameters/ApplyExpensifyCodeParams.ts +++ /dev/null @@ -1,4 +0,0 @@ -type ApplyExpensifyCodeParams = { - expensifyCode: string; -}; -export default ApplyExpensifyCodeParams; 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 2c5a707063e2..bff133d563f8 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -290,7 +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 ApplyExpensifyCodeParams} from './ApplyExpensifyCodeParams'; +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 d2c87995db1c..b71506d8906e 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -923,7 +923,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.ApplyExpensifyCodeParams; + [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/pages/settings/Subscription/ExpensifyCodePage/index.tsx b/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx index 0613d9361399..80db55b2bbd9 100644 --- a/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx +++ b/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx @@ -13,32 +13,28 @@ import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearDraftValues} from '@libs/actions/FormActions'; import Navigation from '@libs/Navigation/Navigation'; -import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import {getFieldRequiredErrors} from '@libs/ValidationUtils'; import {applyExpensifyCode} from '@userActions/Subscription'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/SubscriptionExpensifyCodeForm'; -type ExpensifyCodePageProps = PlatformStackScreenProps; - -function ExpensifyCodePage({}: ExpensifyCodePageProps) { +function ExpensifyCodePage() { const {translate} = useLocalize(); const styles = useThemeStyles(); const {inputCallbackRef} = useAutoFocusInput(); const [hasSubmitted, setHasSubmitted] = useState(false); - const [subscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION); + const [subscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION, {canBeMissing: true}); const defaultValues = { [INPUT_IDS.EXPENSIFY_CODE]: '', }; useEffect(() => { - if (hasSubmitted && subscription?.expensifyCode) { - Navigation.goBack(); + if (!hasSubmitted || !subscription?.expensifyCode) { + return; } + Navigation.goBack(); }, [hasSubmitted, subscription?.expensifyCode]); const validate = useCallback( From c424621fb66063318f41e11fd9f50fb0263ba170 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Thu, 5 Feb 2026 01:39:22 +0500 Subject: [PATCH 11/20] Improve access control and update Expensify code handling --- .../Subscription/ExpensifyCodePage/index.tsx | 68 +++++++++++-------- .../SubscriptionSettings/index.tsx | 8 +-- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx b/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx index 80db55b2bbd9..4d6414de7f40 100644 --- a/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx +++ b/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx @@ -1,5 +1,6 @@ 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'; @@ -14,6 +15,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {clearDraftValues} 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'; @@ -25,17 +27,22 @@ function ExpensifyCodePage() { 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]: '', }; useEffect(() => { - if (!hasSubmitted || !subscription?.expensifyCode) { + if (!hasSubmitted || !isExpensifyCodeApplied) { return; } Navigation.goBack(); - }, [hasSubmitted, subscription?.expensifyCode]); + }, [hasSubmitted, isExpensifyCodeApplied]); + + if (isExpensifyCodeApplied) { + return ; + } const validate = useCallback( (values: FormOnyxValues): FormInputErrors => { @@ -58,33 +65,36 @@ function ExpensifyCodePage() { shouldShowOfflineIndicatorInWideScreen shouldEnableMaxHeight > - - - - {translate('subscription.expensifyCode.enterCode')} - - - + + + + + {translate('subscription.expensifyCode.enterCode')} + + + + ); } diff --git a/src/pages/settings/Subscription/SubscriptionSettings/index.tsx b/src/pages/settings/Subscription/SubscriptionSettings/index.tsx index 7d2ed705c1db..2b88d0d40ca5 100644 --- a/src/pages/settings/Subscription/SubscriptionSettings/index.tsx +++ b/src/pages/settings/Subscription/SubscriptionSettings/index.tsx @@ -62,7 +62,7 @@ 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 hasAppliedExpensifyCode = !!privateSubscription?.expensifyCode; + const isExpensifyCodeApplied = !!privateSubscription?.expensifyCode; 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), @@ -92,7 +92,7 @@ function SubscriptionSettings() { }; const onExpensifyCodePress = () => { - if (hasAppliedExpensifyCode) { + if (isExpensifyCodeApplied) { return; } if (isActingAsDelegate) { @@ -271,9 +271,9 @@ function SubscriptionSettings() { ) : null} Date: Thu, 5 Feb 2026 01:39:45 +0500 Subject: [PATCH 12/20] Add unit tests for ExpensifyCodePage functionality --- tests/ui/ExpensifyCodePageTest.tsx | 188 +++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 tests/ui/ExpensifyCodePageTest.tsx diff --git a/tests/ui/ExpensifyCodePageTest.tsx b/tests/ui/ExpensifyCodePageTest.tsx new file mode 100644 index 000000000000..268aa9a83679 --- /dev/null +++ b/tests/ui/ExpensifyCodePageTest.tsx @@ -0,0 +1,188 @@ +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('@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(), + }; +}); + +jest.mock('@pages/ErrorPage/NotFoundPage', () => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- typeof import() needed for jest.requireActual generic + 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 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(); + }); +}); From 5075bc46f3f2e402026d1d3512aa4238a239bc10 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Thu, 5 Feb 2026 02:32:55 +0500 Subject: [PATCH 13/20] fix failing checks --- .../Subscription/ExpensifyCodePage/index.tsx | 22 +++++++++---------- tests/ui/ExpensifyCodePageTest.tsx | 12 +++++----- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx b/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx index 4d6414de7f40..ab0fd51e067d 100644 --- a/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx +++ b/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx @@ -33,17 +33,6 @@ function ExpensifyCodePage() { [INPUT_IDS.EXPENSIFY_CODE]: '', }; - useEffect(() => { - if (!hasSubmitted || !isExpensifyCodeApplied) { - return; - } - Navigation.goBack(); - }, [hasSubmitted, isExpensifyCodeApplied]); - - if (isExpensifyCodeApplied) { - return ; - } - const validate = useCallback( (values: FormOnyxValues): FormInputErrors => { return getFieldRequiredErrors(values, [INPUT_IDS.EXPENSIFY_CODE]); @@ -58,6 +47,17 @@ function ExpensifyCodePage() { setHasSubmitted(true); }, []); + useEffect(() => { + if (!hasSubmitted || !isExpensifyCodeApplied) { + return; + } + Navigation.goBack(); + }, [hasSubmitted, isExpensifyCodeApplied]); + + if (isExpensifyCodeApplied) { + return ; + } + return ( jest.requireActual('@libs/Localize')); + jest.mock('@userActions/Subscription', () => ({ applyExpensifyCode: jest.fn(), })); @@ -33,7 +35,7 @@ jest.mock('@libs/Navigation/Navigation', () => { }); jest.mock('@pages/ErrorPage/NotFoundPage', () => { - // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- typeof import() needed for jest.requireActual generic + // eslint-disable-next-line @typescript-eslint/consistent-type-imports const {View, Text} = jest.requireActual('react-native'); function MockErrorPage() { return ( @@ -119,7 +121,7 @@ describe('ExpensifyCodePage', () => { await waitForBatchedUpdatesWithAct(); const input = screen.getByTestId('expensify-code-input'); - fireEvent.changeText(input, 'HAPPYDAYS100OFF'); + fireEvent.changeText(input, 'HappyDays100Off'); await waitForBatchedUpdatesWithAct(); const applyButton = screen.getByRole('button', {name: TestHelper.translateLocal('subscription.expensifyCode.apply')}); @@ -127,7 +129,7 @@ describe('ExpensifyCodePage', () => { await waitForBatchedUpdatesWithAct(); expect(applyExpensifyCode).toHaveBeenCalledTimes(1); - expect(applyExpensifyCode).toHaveBeenCalledWith('HAPPYDAYS100OFF'); + expect(applyExpensifyCode).toHaveBeenCalledWith('HappyDays100Off'); }); it('show NotFoundPage when subscription already has expensifyCode', async () => { @@ -175,12 +177,12 @@ describe('ExpensifyCodePage', () => { await waitForBatchedUpdatesWithAct(); const input = screen.getByTestId('expensify-code-input'); - fireEvent.changeText(input, 'INVALIDCODE'); + 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(applyExpensifyCode).toHaveBeenCalledWith('InvalidCode'); expect(screen.getByRole('button', {name: TestHelper.translateLocal('subscription.expensifyCode.apply')})).toBeOnTheScreen(); expect(Navigation.goBack).not.toHaveBeenCalled(); From 58f2826ad23d3795d758641172dbe5e6d590d5a0 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Thu, 5 Feb 2026 02:37:44 +0500 Subject: [PATCH 14/20] lint --- src/pages/settings/Subscription/ExpensifyCodePage/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx b/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx index ab0fd51e067d..071fa187b8fb 100644 --- a/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx +++ b/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx @@ -35,9 +35,9 @@ function ExpensifyCodePage() { const validate = useCallback( (values: FormOnyxValues): FormInputErrors => { - return getFieldRequiredErrors(values, [INPUT_IDS.EXPENSIFY_CODE]); + return getFieldRequiredErrors(values, [INPUT_IDS.EXPENSIFY_CODE], translate); }, - [], + [translate], ); const handleSubmit = useCallback((values: FormOnyxValues) => { From 5a01d6e358176b930fffdc86108d45eb70c3c820 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Thu, 5 Feb 2026 22:59:34 +0500 Subject: [PATCH 15/20] Clear invalid code error on mount if it already exists --- .../settings/Subscription/ExpensifyCodePage/index.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx b/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx index 071fa187b8fb..8c50b1e04f92 100644 --- a/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx +++ b/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx @@ -1,3 +1,4 @@ +import {useFocusEffect} from '@react-navigation/native'; import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; @@ -12,7 +13,7 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {clearDraftValues} from '@libs/actions/FormActions'; +import {clearDraftValues, clearErrorFields} from '@libs/actions/FormActions'; import Navigation from '@libs/Navigation/Navigation'; import {getFieldRequiredErrors} from '@libs/ValidationUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; @@ -47,6 +48,12 @@ function ExpensifyCodePage() { setHasSubmitted(true); }, []); + useFocusEffect( + useCallback(() => { + clearErrorFields(ONYXKEYS.FORMS.SUBSCRIPTION_EXPENSIFY_CODE_FORM); + }, []), + ); + useEffect(() => { if (!hasSubmitted || !isExpensifyCodeApplied) { return; From e2e1534d0ac70601e9cea6eee1a660a5fb1f8faa Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Thu, 5 Feb 2026 23:00:53 +0500 Subject: [PATCH 16/20] feat: display not found page on native devices --- .../ExpensifyCodePage/index.native.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/pages/settings/Subscription/ExpensifyCodePage/index.native.tsx 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; From 5ea835cf2891e540050151e7c7a1b9b41ff99edf Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Thu, 5 Feb 2026 23:17:29 +0500 Subject: [PATCH 17/20] Use web implementation for ExpensifyCodePage test --- tests/ui/ExpensifyCodePageTest.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/ui/ExpensifyCodePageTest.tsx b/tests/ui/ExpensifyCodePageTest.tsx index aa3b1b3e83d0..2a63da885ed5 100644 --- a/tests/ui/ExpensifyCodePageTest.tsx +++ b/tests/ui/ExpensifyCodePageTest.tsx @@ -20,6 +20,10 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' jest.mock('@libs/Localize', () => jest.requireActual('@libs/Localize')); +jest.mock('@pages/settings/Subscription/ExpensifyCodePage', () => + jest.requireActual('@pages/settings/Subscription/ExpensifyCodePage/index.tsx'), +); + jest.mock('@userActions/Subscription', () => ({ applyExpensifyCode: jest.fn(), })); From 2506457448b7c2fc42379699d9dda05a51f8a3a9 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Fri, 6 Feb 2026 00:27:17 +0500 Subject: [PATCH 18/20] fix failing checks --- tests/ui/ExpensifyCodePageTest.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/ui/ExpensifyCodePageTest.tsx b/tests/ui/ExpensifyCodePageTest.tsx index 2a63da885ed5..e7964540ffc0 100644 --- a/tests/ui/ExpensifyCodePageTest.tsx +++ b/tests/ui/ExpensifyCodePageTest.tsx @@ -18,9 +18,8 @@ import SCREENS from '@src/SCREENS'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; -jest.mock('@libs/Localize', () => jest.requireActual('@libs/Localize')); - jest.mock('@pages/settings/Subscription/ExpensifyCodePage', () => + // eslint-disable-next-line @typescript-eslint/no-unsafe-return jest.requireActual('@pages/settings/Subscription/ExpensifyCodePage/index.tsx'), ); @@ -35,6 +34,7 @@ jest.mock('@libs/Navigation/Navigation', () => { goBack: jest.fn(), getTopmostReportId: jest.fn(() => undefined), dismissModal: jest.fn(), + getActiveRoute: jest.fn(() => ''), }; }); @@ -85,6 +85,7 @@ describe('ExpensifyCodePage', () => { jest.clearAllMocks(); await act(async () => { await Onyx.clear(); + await Onyx.set(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION, {expensifyCode: null}); }); await waitForBatchedUpdatesWithAct(); }); From 044deb4b8f557dee3e097bc547e7b2de4bf8a683 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Thu, 12 Feb 2026 23:08:20 +0500 Subject: [PATCH 19/20] Update expensify code route in SETTINGS_TO_RHP and adjust path in ROUTES --- src/ROUTES.ts | 2 +- src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c8e0ed746667..69770cfc95e5 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -283,8 +283,8 @@ const ROUTES = { route: 'settings/subscription/subscription-size', getRoute: (canChangeSize: 0 | 1) => `settings/subscription/subscription-size?canChangeSize=${canChangeSize as number}` as const, }, - SETTINGS_SUBSCRIPTION_EXPENSIFY_CODE: 'settings/subscription/expensify-code', 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/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 Date: Thu, 12 Feb 2026 23:11:00 +0500 Subject: [PATCH 20/20] Add isSecretPromoCode handling in frontend --- .../Subscription/ExpensifyCodePage/index.tsx | 2 +- .../SubscriptionPlanCardActionButton.tsx | 2 +- .../SubscriptionSettings/index.tsx | 22 +++++++++++-------- src/types/onyx/PrivateSubscription.ts | 3 +++ 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx b/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx index 8c50b1e04f92..f7bf1770547e 100644 --- a/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx +++ b/src/pages/settings/Subscription/ExpensifyCodePage/index.tsx @@ -61,7 +61,7 @@ function ExpensifyCodePage() { Navigation.goBack(); }, [hasSubmitted, isExpensifyCodeApplied]); - if (isExpensifyCodeApplied) { + if (isExpensifyCodeApplied || subscription?.isSecretPromoCode) { return ; } diff --git a/src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCardActionButton.tsx b/src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCardActionButton.tsx index 775878411a3f..9fc273687d4f 100644 --- a/src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCardActionButton.tsx +++ b/src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCardActionButton.tsx @@ -136,7 +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?.expensifyCode ?? ''; + const expensifyCode = privateSubscription?.isSecretPromoCode ? '' : (privateSubscription?.expensifyCode ?? ''); return ( ) : null} - + + {shouldShowExpensifyCodeSection && ( + + )}