From da7a07adabdac567159cc2deced8c19f55173447 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 19 May 2026 14:13:00 -0700 Subject: [PATCH 1/7] Migrate PaymentCardCurrencyModal to a @react-navigation modal screen Replace the inline react-native-modal currency picker on the change-billing- currency RHP with a dedicated @react-navigation modal screen registered as a dynamic route, following the pattern established for ExpenseLimitTypeSelector in #88915. - Add DYNAMIC_ROUTES.PAYMENT_CARD_CURRENCY_SELECTOR (path 'payment-card-currency') with the change-billing-currency screen as its only entry, register SETTINGS.SUBSCRIPTION.DYNAMIC_PAYMENT_CARD_CURRENCY_SELECTOR in SCREENS, the SettingsNavigatorParamList, the SettingsModalStackNavigator (gated by withAgentAccessDenied, matching the rest of the billing flow), and the linkingConfig. - Add pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage, which reads the selected currency from the CHANGE_BILLING_CURRENCY_FORM draft (falling back to the billing card currency from FUND_LIST, then USD), writes the chosen value to the draft via setDraftValues, navigates back via useDynamicBackPath, and applies the EUR_BILLING beta filter that previously lived in the modal. - Lift the selected currency in PaymentCardChangeCurrencyForm from local useState to the CHANGE_BILLING_CURRENCY_FORM draft, navigate to the dynamic route on press, drop the inline modal, and clear the draft on unmount. The non-security-code branch (used by the change-payment-currency screen) is unchanged in behavior - it still commits via changeBillingCurrency on selection. - Delete src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx; it has no remaining importers. - Add a regression test covering the EUR_BILLING beta filter, draft-vs-fund-list selection precedence, and the setDraftValues + goBack behavior on row select. Fixed Issues: #90470 --- src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + .../PaymentCardChangeCurrencyForm.tsx | 44 +++--- .../PaymentCardCurrencyModal.tsx | 82 ---------- .../ModalStackNavigators/index.tsx | 3 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/Navigation/types.ts | 1 + ...DynamicPaymentCardCurrencySelectorPage.tsx | 75 +++++++++ ...micPaymentCardCurrencySelectorPageTest.tsx | 148 ++++++++++++++++++ 9 files changed, 255 insertions(+), 104 deletions(-) delete mode 100644 src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx create mode 100644 src/pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage.tsx create mode 100644 tests/unit/pages/settings/DynamicPaymentCardCurrencySelectorPageTest.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f6f0d34b6ae5..421c98402561 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -108,6 +108,10 @@ const DYNAMIC_ROUTES = { path: 'imported-members-role', entryScreens: [SCREENS.WORKSPACE.MEMBERS_IMPORTED_CONFIRMATION], }, + PAYMENT_CARD_CURRENCY_SELECTOR: { + path: 'payment-card-currency', + entryScreens: [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY], + }, REPORT_SETTINGS_NAME: { path: 'settings/name', entryScreens: [SCREENS.REPORT_DETAILS.ROOT], diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 16f9aaf4beef..083b096a5dbd 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -277,6 +277,7 @@ const SCREENS = { ADD_PAYMENT_CARD: 'Settings_Subscription_Add_Payment_Card', DISABLE_AUTO_RENEW_SURVEY: 'Settings_Subscription_DisableAutoRenewSurvey', CHANGE_BILLING_CURRENCY: 'Settings_Subscription_Change_Billing_Currency', + DYNAMIC_PAYMENT_CARD_CURRENCY_SELECTOR: 'Dynamic_Settings_Subscription_Payment_Card_Currency_Selector', CHANGE_PAYMENT_CURRENCY: 'Settings_Subscription_Change_Payment_Currency', CANCEL_SUBSCRIPTION: 'Settings_Subscription_CancelSubscription', SUBSCRIPTION_DOWNGRADE_BLOCKED: 'Settings_Subscription_DowngradeBlocked', diff --git a/src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx b/src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx index d24480f11373..5cb7deceded2 100644 --- a/src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx +++ b/src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo} from 'react'; import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; import FormProvider from '@components/Form/FormProvider'; @@ -9,14 +9,18 @@ import SelectionList from '@components/SelectionList'; import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; +import {clearDraftValues} from '@libs/actions/FormActions'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; +import Navigation from '@libs/Navigation/Navigation'; import {getFieldRequiredErrors, isValidSecurityCode} from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/ChangeBillingCurrencyForm'; import PaymentCardCurrencyHeader from './PaymentCardCurrencyHeader'; -import PaymentCardCurrencyModal from './PaymentCardCurrencyModal'; type PaymentCardFormProps = { initialCurrency?: ValueOf; @@ -31,8 +35,21 @@ function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeReq const {translate} = useLocalize(); const {isBetaEnabled} = usePermissions(); - const [isCurrencyModalVisible, setIsCurrencyModalVisible] = useState(false); - const [currency, setCurrency] = useState>(initialCurrency ?? CONST.PAYMENT_CARD_CURRENCY.USD); + // Only the subscription billing flow (security-code branch) pushes to the currency selector screen, + // so only it needs to round-trip the selection through the form draft. The inline picker flow + // pops back on selection and must not read or clear the shared draft. + const [formDraft] = useOnyx(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM_DRAFT); + const draftCurrency = isSecurityCodeRequired ? formDraft?.[INPUT_IDS.CURRENCY] : undefined; + const currency = draftCurrency ?? initialCurrency ?? CONST.PAYMENT_CARD_CURRENCY.USD; + + useEffect(() => { + if (!isSecurityCodeRequired) { + return undefined; + } + return () => { + clearDraftValues(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM); + }; + }, [isSecurityCodeRequired]); const validate = (values: FormOnyxValues): FormInputErrors => { const errors = getFieldRequiredErrors(values, REQUIRED_FIELDS, translate); @@ -67,18 +84,8 @@ function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeReq [availableCurrencies, currency], ); - const showCurrenciesModal = useCallback(() => { - setIsCurrencyModalVisible(true); - }, []); - - const changeCurrency = useCallback((selectedCurrency: ValueOf) => { - setCurrency(selectedCurrency); - setIsCurrencyModalVisible(false); - }, []); - const selectCurrency = useCallback( (selectedCurrency: ValueOf) => { - setCurrency(selectedCurrency); changeBillingCurrency(selectedCurrency); }, [changeBillingCurrency], @@ -103,7 +110,7 @@ function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeReq title={currency} descriptionTextStyle={styles.textNormal} description={translate('common.currency')} - onPress={showCurrenciesModal} + onPress={() => Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.PAYMENT_CARD_CURRENCY_SELECTOR.path))} /> - setIsCurrencyModalVisible(false)} - /> ); } diff --git a/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx b/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx deleted file mode 100644 index b32a7f930446..000000000000 --- a/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, {useMemo} from 'react'; -import type {ValueOf} from 'type-fest'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import Modal from '@components/Modal'; -import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; -import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import Navigation from '@libs/Navigation/Navigation'; -import CONST from '@src/CONST'; - -type PaymentCardCurrencyModalProps = { - /** Whether the modal is visible */ - isVisible: boolean; - - /** The list of currencies to render */ - currencies: Array>; - - /** Currently selected currency */ - currentCurrency: ValueOf; - - /** Function to call when the user selects a currency */ - onCurrencyChange: (currency: ValueOf) => void; - - /** Function to call when the user closes the currency picker */ - onClose: () => void; -}; - -function PaymentCardCurrencyModal({isVisible, currencies, currentCurrency = CONST.PAYMENT_CARD_CURRENCY.USD, onCurrencyChange, onClose}: PaymentCardCurrencyModalProps) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const currencyOptions = useMemo( - () => - currencies.map( - (currency) => ({ - text: currency, - value: currency, - keyForList: currency, - isSelected: currency === currentCurrency, - }), - [], - ), - [currencies, currentCurrency], - ); - - return ( - { - onClose(); - Navigation.dismissModal(); - }} - > - - - { - onCurrencyChange(option.value); - }} - initiallyFocusedItemKey={currentCurrency} - showScrollIndicator - /> - - - ); -} - -export default PaymentCardCurrencyModal; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 7e96049388d2..004f2068801c 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -957,6 +957,9 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Subscription/PaymentCard/ChangeBillingCurrency').default, ), + [SCREENS.SETTINGS.SUBSCRIPTION.DYNAMIC_PAYMENT_CARD_CURRENCY_SELECTOR]: withAgentAccessDenied( + () => require('../../../../pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage').default, + ), [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: withAgentAccessDenied(() => require('../../../../pages/settings/Subscription/PaymentCard').default), [SCREENS.SETTINGS.ADD_PAYMENT_CARD_CHANGE_CURRENCY]: () => require('../../../../pages/settings/PaymentCard/ChangeCurrency').default, [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: () => require('../../../../pages/workspace/reports/CreateReportFieldsPage').default, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 4328017fda28..d0fe8aef7bcb 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -468,6 +468,7 @@ const config: LinkingOptions['config'] = { }, [SCREENS.SETTINGS.DYNAMIC_VERIFY_ACCOUNT]: DYNAMIC_ROUTES.VERIFY_ACCOUNT.path, [SCREENS.SETTINGS.DYNAMIC_ADD_BANK_ACCOUNT_VERIFY_ACCOUNT]: DYNAMIC_ROUTES.ADD_BANK_ACCOUNT_VERIFY_ACCOUNT.path, + [SCREENS.SETTINGS.SUBSCRIPTION.DYNAMIC_PAYMENT_CARD_CURRENCY_SELECTOR]: DYNAMIC_ROUTES.PAYMENT_CARD_CURRENCY_SELECTOR.path, [SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: { path: ROUTES.SETTINGS_CONTACT_METHODS.route, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index e36072ac31fc..c90926877712 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -615,6 +615,7 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.SUBSCRIPTION.SETTINGS_DETAILS]: undefined; [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: undefined; [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY]: undefined; + [SCREENS.SETTINGS.SUBSCRIPTION.DYNAMIC_PAYMENT_CARD_CURRENCY_SELECTOR]: undefined; [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_PAYMENT_CURRENCY]: undefined; [SCREENS.WORKSPACE.TAXES_SETTINGS]: { policyID: string; diff --git a/src/pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage.tsx b/src/pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage.tsx new file mode 100644 index 000000000000..213c62dcd2ae --- /dev/null +++ b/src/pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage.tsx @@ -0,0 +1,75 @@ +import React, {useMemo} from 'react'; +import type {ValueOf} from 'type-fest'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {setDraftValues} from '@libs/actions/FormActions'; +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; +import INPUT_IDS from '@src/types/form/ChangeBillingCurrencyForm'; + +type Currency = ValueOf; + +function DynamicPaymentCardCurrencySelectorPage() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {isBetaEnabled} = usePermissions(); + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.PAYMENT_CARD_CURRENCY_SELECTOR.path); + const [formDraft] = useOnyx(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM_DRAFT); + const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); + + // Mirrors the `initialCurrency` resolution in ChangeBillingCurrency: the billing card's currency. + const fallbackCurrency = useMemo( + () => Object.values(fundList ?? {}).find((card) => card.accountData?.additionalData?.isBillingCard)?.accountData?.currency ?? CONST.PAYMENT_CARD_CURRENCY.USD, + [fundList], + ); + const currentCurrency = (formDraft?.[INPUT_IDS.CURRENCY] ?? fallbackCurrency) as Currency; + + const currencyOptions = useMemo(() => { + const canUseEurBilling = isBetaEnabled(CONST.BETAS.EUR_BILLING); + return (Object.keys(CONST.PAYMENT_CARD_CURRENCY) as Currency[]) + .filter((currency) => currency !== CONST.PAYMENT_CARD_CURRENCY.EUR || canUseEurBilling) + .map((currency) => ({ + text: currency, + value: currency, + keyForList: currency, + isSelected: currency === currentCurrency, + })); + }, [currentCurrency, isBetaEnabled]); + + return ( + + Navigation.goBack(backPath)} + /> + { + setDraftValues(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM, {[INPUT_IDS.CURRENCY]: option.value}); + Navigation.goBack(backPath); + }} + shouldSingleExecuteRowSelect + initiallyFocusedItemKey={currentCurrency} + showScrollIndicator + addBottomSafeAreaPadding + /> + + ); +} + +export default DynamicPaymentCardCurrencySelectorPage; diff --git a/tests/unit/pages/settings/DynamicPaymentCardCurrencySelectorPageTest.tsx b/tests/unit/pages/settings/DynamicPaymentCardCurrencySelectorPageTest.tsx new file mode 100644 index 000000000000..8c7dc0667ca6 --- /dev/null +++ b/tests/unit/pages/settings/DynamicPaymentCardCurrencySelectorPageTest.tsx @@ -0,0 +1,148 @@ +import {render} from '@testing-library/react-native'; +import React from 'react'; +import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; +import {setDraftValues} from '@libs/actions/FormActions'; +import Navigation from '@libs/Navigation/Navigation'; +import DynamicPaymentCardCurrencySelectorPage from '@pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type CurrencyOption = {text: string; value: string; keyForList: string; isSelected: boolean}; + +let capturedData: CurrencyOption[] = []; +let capturedOnSelectRow: ((option: CurrencyOption) => void) | undefined; + +jest.mock('@hooks/usePermissions', () => jest.fn(() => ({isBetaEnabled: () => false}))); + +jest.mock('@hooks/useLocalize', () => + jest.fn(() => ({ + translate: (key: string) => key, + })), +); + +jest.mock('@hooks/useThemeStyles', () => + jest.fn( + () => + new Proxy( + {}, + { + get: () => ({}), + }, + ), + ), +); + +jest.mock('@hooks/useOnyx', () => jest.fn()); + +jest.mock('@hooks/useDynamicBackPath', () => jest.fn(() => 'settings/subscription/change-billing-currency')); + +jest.mock('@libs/actions/FormActions', () => ({ + setDraftValues: jest.fn(), +})); + +jest.mock('@libs/Navigation/Navigation', () => ({ + goBack: jest.fn(), +})); + +jest.mock('@components/ScreenWrapper', () => { + function MockScreenWrapper({children}: {children: React.ReactNode}) { + return children; + } + return MockScreenWrapper; +}); + +jest.mock('@components/HeaderWithBackButton', () => { + function MockHeader({title}: {title: string}) { + return title; + } + return MockHeader; +}); + +jest.mock('@components/SelectionList', () => { + function MockSelectionList({data, onSelectRow}: {data: CurrencyOption[]; onSelectRow: (option: CurrencyOption) => void}) { + capturedData = data ?? []; + capturedOnSelectRow = onSelectRow; + return (data ?? []).map((item) => item.text).join(','); + } + return MockSelectionList; +}); + +jest.mock('@components/SelectionList/ListItem/SingleSelectListItem', () => 'SingleSelectListItem'); + +const mockUsePermissions = jest.mocked(usePermissions); +const mockUseOnyx = jest.mocked(useOnyx); +const mockSetDraftValues = jest.mocked(setDraftValues); +const mockGoBack = jest.mocked(Navigation.goBack); + +/** + * The page reads two Onyx keys in order: the CHANGE_BILLING_CURRENCY form draft, then the fund list. + */ +const mockOnyx = (formDraftCurrency?: string, billingCardCurrency?: string) => { + mockUseOnyx.mockImplementation((key: string) => { + if (key === ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM_DRAFT) { + return [formDraftCurrency ? {currency: formDraftCurrency} : undefined, {status: 'loaded'}] as ReturnType; + } + if (key === ONYXKEYS.FUND_LIST) { + return [billingCardCurrency ? {card1: {accountData: {additionalData: {isBillingCard: true}, currency: billingCardCurrency}}} : undefined, {status: 'loaded'}] as ReturnType< + typeof useOnyx + >; + } + return [undefined, {status: 'loaded'}] as ReturnType; + }); +}; + +describe('DynamicPaymentCardCurrencySelectorPage', () => { + beforeEach(() => { + jest.clearAllMocks(); + capturedData = []; + capturedOnSelectRow = undefined; + mockUsePermissions.mockReturnValue({isBetaEnabled: () => false}); + mockOnyx(); + }); + + it('hides EUR when the EUR billing beta is disabled', () => { + render(); + + const currencies = capturedData.map((option) => option.value); + expect(currencies).toEqual(['USD', 'AUD', 'GBP', 'NZD']); + expect(currencies).not.toContain('EUR'); + }); + + it('shows EUR when the EUR billing beta is enabled', () => { + mockUsePermissions.mockReturnValue({isBetaEnabled: () => true}); + + render(); + + expect(capturedData.map((option) => option.value)).toContain('EUR'); + }); + + it('marks the form draft currency as selected', () => { + mockOnyx('AUD'); + + render(); + + const selected = capturedData.filter((option) => option.isSelected); + expect(selected).toHaveLength(1); + expect(selected.at(0)?.value).toBe('AUD'); + }); + + it('falls back to the billing card currency when the draft is empty', () => { + mockOnyx(undefined, 'GBP'); + + render(); + + expect(capturedData.find((option) => option.isSelected)?.value).toBe('GBP'); + }); + + it('writes the chosen currency to the form draft and navigates back on select', () => { + render(); + + const aud = capturedData.find((option) => option.value === 'AUD'); + expect(aud).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + capturedOnSelectRow?.(aud!); + + expect(mockSetDraftValues).toHaveBeenCalledWith(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM, {currency: 'AUD'}); + expect(mockGoBack).toHaveBeenCalledWith('settings/subscription/change-billing-currency'); + }); +}); From c3bf47bd3abe418da2dca93296ebf64ec88e9356 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 26 May 2026 14:13:15 -0700 Subject: [PATCH 2/7] Remove PaymentCardChangeCurrencyForm + ChangeCurrency intermediaries Route CurrencySelector and the billing-card flow directly to DynamicPaymentCardCurrencySelectorPage. The ChangeBillingCurrency page now owns its own CVV input via FormProvider, so the isSecurityCodeRequired flag and the PaymentCardChangeCurrencyForm wrapper are no longer needed. Moved DynamicPaymentCardCurrencySelectorPageTest from tests/unit to tests/ui to match the rendering-based test pattern. --- src/ROUTES.ts | 4 +- src/SCREENS.ts | 2 - .../PaymentCardChangeCurrencyForm.tsx | 147 ------------------ .../AddPaymentCard/PaymentCardForm.tsx | 4 - src/components/CurrencySelector.tsx | 22 +-- .../ModalStackNavigators/index.tsx | 2 - .../RELATIONS/SETTINGS_TO_RHP.ts | 1 - src/libs/Navigation/linkingConfig/config.ts | 8 - src/libs/Navigation/types.ts | 1 - .../PaymentCard/ChangeCurrency/index.tsx | 41 ----- .../ChangeBillingCurrency/index.tsx | 85 ++++++++-- ...DynamicPaymentCardCurrencySelectorPage.tsx | 6 +- .../Subscription/PaymentCard/index.tsx | 2 - ...micPaymentCardCurrencySelectorPageTest.tsx | 29 +++- 14 files changed, 106 insertions(+), 248 deletions(-) delete mode 100644 src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx delete mode 100644 src/pages/settings/PaymentCard/ChangeCurrency/index.tsx rename tests/{unit/pages/settings => ui}/DynamicPaymentCardCurrencySelectorPageTest.tsx (78%) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 421c98402561..da293167a76f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -110,7 +110,7 @@ const DYNAMIC_ROUTES = { }, PAYMENT_CARD_CURRENCY_SELECTOR: { path: 'payment-card-currency', - entryScreens: [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY], + entryScreens: [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY, SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD, SCREENS.WORKSPACE.OWNER_CHANGE_CHECK], }, REPORT_SETTINGS_NAME: { path: 'settings/name', @@ -851,7 +851,6 @@ const ROUTES = { getRoute: (backTo?: string) => getUrlWithBackToParam('settings/profile', backTo), }, - SETTINGS_CHANGE_CURRENCY: 'settings/add-payment-card/change-currency', SETTINGS_SHARE_CODE: 'settings/shareCode', SETTINGS_DISPLAY_NAME: 'settings/profile/display-name', SETTINGS_AVATAR: 'settings/profile/avatar', @@ -872,7 +871,6 @@ const ROUTES = { 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', SETTINGS_SUBSCRIPTION_DISABLE_AUTO_RENEW_SURVEY: 'settings/subscription/disable-auto-renew-survey', SETTINGS_SUBSCRIPTION_CANCEL_SUBSCRIPTION: 'settings/subscription/cancel-subscription-survey', SETTINGS_SUBSCRIPTION_DOWNGRADE_BLOCKED: { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 083b096a5dbd..40a7eec14665 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -121,7 +121,6 @@ const SCREENS = { SAVE_THE_WORLD: 'Settings_TeachersUnite', APP_DOWNLOAD_LINKS: 'Settings_App_Download_Links', ADD_DEBIT_CARD: 'Settings_Add_Debit_Card', - ADD_PAYMENT_CARD_CHANGE_CURRENCY: 'Settings_Add_Payment_Card_Change_Currency', ADD_BANK_ACCOUNT: 'Settings_Add_Bank_Account', ADD_US_BANK_ACCOUNT: 'Settings_Add_US_Bank_Account', ADD_US_BANK_ACCOUNT_ENTRY_POINT: 'Settings_Add_US_Bank_Account_Entry_Point', @@ -278,7 +277,6 @@ const SCREENS = { DISABLE_AUTO_RENEW_SURVEY: 'Settings_Subscription_DisableAutoRenewSurvey', CHANGE_BILLING_CURRENCY: 'Settings_Subscription_Change_Billing_Currency', DYNAMIC_PAYMENT_CARD_CURRENCY_SELECTOR: 'Dynamic_Settings_Subscription_Payment_Card_Currency_Selector', - CHANGE_PAYMENT_CURRENCY: 'Settings_Subscription_Change_Payment_Currency', CANCEL_SUBSCRIPTION: 'Settings_Subscription_CancelSubscription', SUBSCRIPTION_DOWNGRADE_BLOCKED: 'Settings_Subscription_DowngradeBlocked', }, diff --git a/src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx b/src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx deleted file mode 100644 index 5cb7deceded2..000000000000 --- a/src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React, {useCallback, useEffect, useMemo} from 'react'; -import {View} from 'react-native'; -import type {ValueOf} from 'type-fest'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; -import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; -import SelectionList from '@components/SelectionList'; -import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; -import TextInput from '@components/TextInput'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import usePermissions from '@hooks/usePermissions'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {clearDraftValues} from '@libs/actions/FormActions'; -import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; -import Navigation from '@libs/Navigation/Navigation'; -import {getFieldRequiredErrors, isValidSecurityCode} from '@libs/ValidationUtils'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import {DYNAMIC_ROUTES} from '@src/ROUTES'; -import INPUT_IDS from '@src/types/form/ChangeBillingCurrencyForm'; -import PaymentCardCurrencyHeader from './PaymentCardCurrencyHeader'; - -type PaymentCardFormProps = { - initialCurrency?: ValueOf; - isSecurityCodeRequired?: boolean; - changeBillingCurrency: (currency?: ValueOf, values?: FormOnyxValues) => void; -}; - -const REQUIRED_FIELDS = [INPUT_IDS.SECURITY_CODE]; - -function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeRequired, initialCurrency}: PaymentCardFormProps) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const {isBetaEnabled} = usePermissions(); - - // Only the subscription billing flow (security-code branch) pushes to the currency selector screen, - // so only it needs to round-trip the selection through the form draft. The inline picker flow - // pops back on selection and must not read or clear the shared draft. - const [formDraft] = useOnyx(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM_DRAFT); - const draftCurrency = isSecurityCodeRequired ? formDraft?.[INPUT_IDS.CURRENCY] : undefined; - const currency = draftCurrency ?? initialCurrency ?? CONST.PAYMENT_CARD_CURRENCY.USD; - - useEffect(() => { - if (!isSecurityCodeRequired) { - return undefined; - } - return () => { - clearDraftValues(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM); - }; - }, [isSecurityCodeRequired]); - - const validate = (values: FormOnyxValues): FormInputErrors => { - const errors = getFieldRequiredErrors(values, REQUIRED_FIELDS, translate); - - if (values.securityCode && !isValidSecurityCode(values.securityCode)) { - errors.securityCode = translate('addPaymentCardPage.error.securityCode'); - } - - return errors; - }; - - const availableCurrencies = useMemo(() => { - const canUseEurBilling = isBetaEnabled(CONST.BETAS.EUR_BILLING); - const allCurrencies = Object.keys(CONST.PAYMENT_CARD_CURRENCY) as Array>; - // Filter out EUR if user doesn't have EUR billing beta - return allCurrencies.filter((currencyItem) => { - if (currencyItem === CONST.PAYMENT_CARD_CURRENCY.EUR && !canUseEurBilling) { - return false; - } - return true; - }); - }, [isBetaEnabled]); - - const currencyOptions = useMemo( - () => - availableCurrencies.map((currencyItem) => ({ - text: currencyItem, - value: currencyItem, - keyForList: currencyItem, - isSelected: currencyItem === currency, - })), - [availableCurrencies, currency], - ); - - const selectCurrency = useCallback( - (selectedCurrency: ValueOf) => { - changeBillingCurrency(selectedCurrency); - }, - [changeBillingCurrency], - ); - - if (isSecurityCodeRequired) { - return ( - changeBillingCurrency(currency, values)} - submitButtonText={translate('common.save')} - scrollContextEnabled - style={[styles.mh5, styles.flexGrow1]} - shouldHideFixErrorsAlert - > - - <> - - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.PAYMENT_CARD_CURRENCY_SELECTOR.path))} - /> - - - - - ); - } - - return ( - - { - selectCurrency(option.value); - }} - style={{containerStyle: styles.mhn5}} - initiallyFocusedItemKey={currency} - customListHeader={} - /> - - ); -} - -export default PaymentCardChangeCurrencyForm; diff --git a/src/components/AddPaymentCard/PaymentCardForm.tsx b/src/components/AddPaymentCard/PaymentCardForm.tsx index 78af529f747c..5388f9353a30 100644 --- a/src/components/AddPaymentCard/PaymentCardForm.tsx +++ b/src/components/AddPaymentCard/PaymentCardForm.tsx @@ -38,8 +38,6 @@ type PaymentCardFormProps = { footerContent?: ReactNode; /** Custom content to display in the header before card form */ headerContent?: ReactNode; - /** object to get currency route details from */ - currencySelectorRoute?: typeof ROUTES.SETTINGS_SUBSCRIPTION_CHANGE_PAYMENT_CURRENCY; }; function IAcceptTheLabel() { @@ -123,7 +121,6 @@ function PaymentCardForm({ showStateSelector, footerContent, headerContent, - currencySelectorRoute, }: PaymentCardFormProps) { const styles = useThemeStyles(); const [data, metadata] = useOnyx(ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM); @@ -358,7 +355,6 @@ function PaymentCardForm({ {!!showCurrencyField && ( void; - /** object to get route details from */ - currencySelectorRoute?: typeof ROUTES.SETTINGS_SUBSCRIPTION_CHANGE_PAYMENT_CURRENCY | typeof ROUTES.SETTINGS_CHANGE_CURRENCY | typeof ROUTES.CURRENCY_SELECTION; + /** Optional route override; when omitted the selector opens the dynamic payment-card currency picker. */ + currencySelectorRoute?: typeof ROUTES.CURRENCY_SELECTION; /** Label for the input */ label?: string; @@ -40,16 +41,7 @@ type CurrencySelectorProps = { ref: ForwardedRef; }; -function CurrencySelector({ - errorText = '', - value: currency, - onInputChange = () => {}, - onBlur, - currencySelectorRoute = ROUTES.SETTINGS_CHANGE_CURRENCY, - label, - shouldShowCurrencySymbol = false, - ref, -}: CurrencySelectorProps) { +function CurrencySelector({errorText = '', value: currency, onInputChange = () => {}, onBlur, currencySelectorRoute, label, shouldShowCurrencySymbol = false, ref}: CurrencySelectorProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {getCurrencySymbol} = useCurrencyListActions(); @@ -86,9 +78,9 @@ function CurrencySelector({ didOpenCurrencySelector.current = true; if (currencySelectorRoute === ROUTES.CURRENCY_SELECTION) { Navigation.navigate(currencySelectorRoute.getRoute(Navigation.getActiveRoute())); - } else { - Navigation.navigate(currencySelectorRoute as typeof ROUTES.SETTINGS_SUBSCRIPTION_CHANGE_PAYMENT_CURRENCY | typeof ROUTES.SETTINGS_CHANGE_CURRENCY); + return; } + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.PAYMENT_CARD_CURRENCY_SELECTOR.path)); }} /> ); diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 004f2068801c..dc796a94ae2d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -953,7 +953,6 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardStatementCloseDatePage').default, [SCREENS.SETTINGS.SAVE_THE_WORLD]: () => require('../../../../pages/TeachersUnite/SaveTheWorldPage').default, - [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_PAYMENT_CURRENCY]: withAgentAccessDenied(() => require('../../../../pages/settings/PaymentCard/ChangeCurrency').default), [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY]: withAgentAccessDenied( () => require('../../../../pages/settings/Subscription/PaymentCard/ChangeBillingCurrency').default, ), @@ -961,7 +960,6 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage').default, ), [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: withAgentAccessDenied(() => require('../../../../pages/settings/Subscription/PaymentCard').default), - [SCREENS.SETTINGS.ADD_PAYMENT_CARD_CHANGE_CURRENCY]: () => require('../../../../pages/settings/PaymentCard/ChangeCurrency').default, [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: () => require('../../../../pages/workspace/reports/CreateReportFieldsPage').default, [SCREENS.WORKSPACE.REPORT_FIELDS_SETTINGS]: () => require('../../../../pages/workspace/reports/ReportFieldsSettingsPage').default, [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: () => require('../../../../pages/workspace/reports/ReportFieldsListValuesPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts index bd4b95724c4e..20c0991b9628 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts @@ -137,7 +137,6 @@ const SETTINGS_TO_RHP: Partial['config'] = { path: ROUTES.SETTINGS_SUBSCRIPTION_CHANGE_BILLING_CURRENCY, exact: true, }, - [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_PAYMENT_CURRENCY]: { - path: ROUTES.SETTINGS_SUBSCRIPTION_CHANGE_PAYMENT_CURRENCY, - exact: true, - }, - [SCREENS.SETTINGS.ADD_PAYMENT_CARD_CHANGE_CURRENCY]: { - path: ROUTES.SETTINGS_CHANGE_CURRENCY, - exact: true, - }, [SCREENS.SETTINGS.PREFERENCES.THEME]: { path: ROUTES.SETTINGS_THEME, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index c90926877712..d02b334f1f48 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -616,7 +616,6 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: undefined; [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY]: undefined; [SCREENS.SETTINGS.SUBSCRIPTION.DYNAMIC_PAYMENT_CARD_CURRENCY_SELECTOR]: undefined; - [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_PAYMENT_CURRENCY]: undefined; [SCREENS.WORKSPACE.TAXES_SETTINGS]: { policyID: string; }; diff --git a/src/pages/settings/PaymentCard/ChangeCurrency/index.tsx b/src/pages/settings/PaymentCard/ChangeCurrency/index.tsx deleted file mode 100644 index 00a44a309e00..000000000000 --- a/src/pages/settings/PaymentCard/ChangeCurrency/index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, {useCallback} from 'react'; -import {View} from 'react-native'; -import type {ValueOf} from 'type-fest'; -import PaymentCardChangeCurrencyForm from '@components/AddPaymentCard/PaymentCardChangeCurrencyForm'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import useThemeStyles from '@hooks/useThemeStyles'; -import Navigation from '@navigation/Navigation'; -import * as PaymentMethods from '@userActions/PaymentMethods'; -import type CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; - -function ChangeCurrency() { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const [debitCardForm] = useOnyx(ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM); - - const changeCurrency = useCallback((currency?: ValueOf) => { - if (currency) { - PaymentMethods.setPaymentMethodCurrency(currency); - } - - Navigation.goBack(); - }, []); - - return ( - - - - - - - ); -} - -export default ChangeCurrency; diff --git a/src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx b/src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx index 1e1d823b31fc..f6a87c109542 100644 --- a/src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx +++ b/src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx @@ -1,26 +1,43 @@ import React, {useCallback, useEffect, useMemo} from 'react'; import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; -import PaymentCardChangeCurrencyForm from '@components/AddPaymentCard/PaymentCardChangeCurrencyForm'; -import type {FormOnyxValues} from '@components/Form/types'; +import PaymentCardCurrencyHeader from '@components/AddPaymentCard/PaymentCardCurrencyHeader'; +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 MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; +import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; -import Navigation from '@navigation/Navigation'; +import {clearDraftValues} from '@libs/actions/FormActions'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; +import Navigation from '@libs/Navigation/Navigation'; +import {getFieldRequiredErrors, isValidSecurityCode} from '@libs/ValidationUtils'; import * as PaymentMethods from '@userActions/PaymentMethods'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; +import INPUT_IDS from '@src/types/form/ChangeBillingCurrencyForm'; + +type Currency = ValueOf; + +const REQUIRED_FIELDS = [INPUT_IDS.SECURITY_CODE]; function ChangeBillingCurrency() { const styles = useThemeStyles(); const {translate} = useLocalize(); const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); + const [formDraft] = useOnyx(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM_DRAFT); + const [formData] = useOnyx(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM); + const defaultCard = useMemo(() => Object.values(fundList ?? {}).find((card) => card.accountData?.additionalData?.isBillingCard), [fundList]); + const initialCurrency = defaultCard?.accountData?.currency; + const currency = (formDraft?.[INPUT_IDS.CURRENCY] ?? initialCurrency ?? CONST.PAYMENT_CARD_CURRENCY.USD) as Currency; - const [formData] = useOnyx(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM); const formDataComplete = formData?.isLoading === false && !formData.errors; const prevIsLoading = usePrevious(formData?.isLoading); const prevFormDataComplete = usePrevious(formDataComplete); @@ -32,23 +49,61 @@ function ChangeBillingCurrency() { Navigation.goBack(); }, [formDataComplete, prevFormDataComplete, prevIsLoading]); - const changeBillingCurrency = useCallback((currency?: ValueOf, values?: FormOnyxValues) => { - if (!values?.securityCode) { - Navigation.goBack(); - return; + useEffect(() => () => clearDraftValues(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM), []); + + const validate = (values: FormOnyxValues): FormInputErrors => { + const errors = getFieldRequiredErrors(values, REQUIRED_FIELDS, translate); + if (values.securityCode && !isValidSecurityCode(values.securityCode)) { + errors.securityCode = translate('addPaymentCardPage.error.securityCode'); } - PaymentMethods.updateBillingCurrency(currency ?? CONST.PAYMENT_CARD_CURRENCY.USD, values.securityCode); - }, []); + return errors; + }; + + const onSubmit = useCallback( + (values: FormOnyxValues) => { + if (!values?.securityCode) { + Navigation.goBack(); + return; + } + PaymentMethods.updateBillingCurrency(currency, values.securityCode); + }, + [currency], + ); return ( - + + + + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.PAYMENT_CARD_CURRENCY_SELECTOR.path))} + /> + + + ); diff --git a/src/pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage.tsx b/src/pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage.tsx index 213c62dcd2ae..7776729bbe5b 100644 --- a/src/pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage.tsx +++ b/src/pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage.tsx @@ -11,6 +11,7 @@ import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import {setDraftValues} from '@libs/actions/FormActions'; import Navigation from '@libs/Navigation/Navigation'; +import {setPaymentMethodCurrency} from '@userActions/PaymentMethods'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; @@ -24,14 +25,14 @@ function DynamicPaymentCardCurrencySelectorPage() { const {isBetaEnabled} = usePermissions(); const backPath = useDynamicBackPath(DYNAMIC_ROUTES.PAYMENT_CARD_CURRENCY_SELECTOR.path); const [formDraft] = useOnyx(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM_DRAFT); + const [addCardForm] = useOnyx(ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM); const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); - // Mirrors the `initialCurrency` resolution in ChangeBillingCurrency: the billing card's currency. const fallbackCurrency = useMemo( () => Object.values(fundList ?? {}).find((card) => card.accountData?.additionalData?.isBillingCard)?.accountData?.currency ?? CONST.PAYMENT_CARD_CURRENCY.USD, [fundList], ); - const currentCurrency = (formDraft?.[INPUT_IDS.CURRENCY] ?? fallbackCurrency) as Currency; + const currentCurrency = (formDraft?.[INPUT_IDS.CURRENCY] ?? addCardForm?.currency ?? fallbackCurrency) as Currency; const currencyOptions = useMemo(() => { const canUseEurBilling = isBetaEnabled(CONST.BETAS.EUR_BILLING); @@ -61,6 +62,7 @@ function DynamicPaymentCardCurrencySelectorPage() { ListItem={SingleSelectListItem} onSelectRow={(option) => { setDraftValues(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM, {[INPUT_IDS.CURRENCY]: option.value}); + setPaymentMethodCurrency(option.value); Navigation.goBack(backPath); }} shouldSingleExecuteRowSelect diff --git a/src/pages/settings/Subscription/PaymentCard/index.tsx b/src/pages/settings/Subscription/PaymentCard/index.tsx index 6d2bd12a1a4c..245084fc2f0e 100644 --- a/src/pages/settings/Subscription/PaymentCard/index.tsx +++ b/src/pages/settings/Subscription/PaymentCard/index.tsx @@ -31,7 +31,6 @@ import CardAuthenticationModal from '@pages/settings/Subscription/CardAuthentica import {addSubscriptionPaymentCard, clearPaymentCardFormErrorAndSubmit} from '@userActions/PaymentMethods'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; function AddPaymentCard() { @@ -121,7 +120,6 @@ function AddPaymentCard() { addPaymentCard={addPaymentCard} showAcceptTerms showCurrencyField - currencySelectorRoute={ROUTES.SETTINGS_SUBSCRIPTION_CHANGE_PAYMENT_CURRENCY} submitButtonText={translate('subscription.paymentCard.addPaymentCard')} headerContent={{translate('subscription.paymentCard.enterPaymentCardDetails')}} footerContent={ diff --git a/tests/unit/pages/settings/DynamicPaymentCardCurrencySelectorPageTest.tsx b/tests/ui/DynamicPaymentCardCurrencySelectorPageTest.tsx similarity index 78% rename from tests/unit/pages/settings/DynamicPaymentCardCurrencySelectorPageTest.tsx rename to tests/ui/DynamicPaymentCardCurrencySelectorPageTest.tsx index 8c7dc0667ca6..a303a1c0bbae 100644 --- a/tests/unit/pages/settings/DynamicPaymentCardCurrencySelectorPageTest.tsx +++ b/tests/ui/DynamicPaymentCardCurrencySelectorPageTest.tsx @@ -5,6 +5,7 @@ import usePermissions from '@hooks/usePermissions'; import {setDraftValues} from '@libs/actions/FormActions'; import Navigation from '@libs/Navigation/Navigation'; import DynamicPaymentCardCurrencySelectorPage from '@pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage'; +import {setPaymentMethodCurrency} from '@userActions/PaymentMethods'; import ONYXKEYS from '@src/ONYXKEYS'; type CurrencyOption = {text: string; value: string; keyForList: string; isSelected: boolean}; @@ -40,6 +41,10 @@ jest.mock('@libs/actions/FormActions', () => ({ setDraftValues: jest.fn(), })); +jest.mock('@userActions/PaymentMethods', () => ({ + setPaymentMethodCurrency: jest.fn(), +})); + jest.mock('@libs/Navigation/Navigation', () => ({ goBack: jest.fn(), })); @@ -72,16 +77,21 @@ jest.mock('@components/SelectionList/ListItem/SingleSelectListItem', () => 'Sing const mockUsePermissions = jest.mocked(usePermissions); const mockUseOnyx = jest.mocked(useOnyx); const mockSetDraftValues = jest.mocked(setDraftValues); +const mockSetPaymentMethodCurrency = jest.mocked(setPaymentMethodCurrency); const mockGoBack = jest.mocked(Navigation.goBack); /** - * The page reads two Onyx keys in order: the CHANGE_BILLING_CURRENCY form draft, then the fund list. + * The page reads three Onyx keys in order: the CHANGE_BILLING_CURRENCY form draft, the in-flight + * ADD_PAYMENT_CARD form, and the fund list (for the billing card fallback). */ -const mockOnyx = (formDraftCurrency?: string, billingCardCurrency?: string) => { +const mockOnyx = (formDraftCurrency?: string, addCardCurrency?: string, billingCardCurrency?: string) => { mockUseOnyx.mockImplementation((key: string) => { if (key === ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM_DRAFT) { return [formDraftCurrency ? {currency: formDraftCurrency} : undefined, {status: 'loaded'}] as ReturnType; } + if (key === ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM) { + return [addCardCurrency ? {currency: addCardCurrency} : undefined, {status: 'loaded'}] as ReturnType; + } if (key === ONYXKEYS.FUND_LIST) { return [billingCardCurrency ? {card1: {accountData: {additionalData: {isBillingCard: true}, currency: billingCardCurrency}}} : undefined, {status: 'loaded'}] as ReturnType< typeof useOnyx @@ -126,15 +136,23 @@ describe('DynamicPaymentCardCurrencySelectorPage', () => { expect(selected.at(0)?.value).toBe('AUD'); }); - it('falls back to the billing card currency when the draft is empty', () => { - mockOnyx(undefined, 'GBP'); + it('falls back to the add-payment-card form currency when the draft is empty', () => { + mockOnyx(undefined, 'NZD'); + + render(); + + expect(capturedData.find((option) => option.isSelected)?.value).toBe('NZD'); + }); + + it('falls back to the billing card currency when both the draft and the add-card form are empty', () => { + mockOnyx(undefined, undefined, 'GBP'); render(); expect(capturedData.find((option) => option.isSelected)?.value).toBe('GBP'); }); - it('writes the chosen currency to the form draft and navigates back on select', () => { + it('writes the chosen currency to both flows and navigates back on select', () => { render(); const aud = capturedData.find((option) => option.value === 'AUD'); @@ -143,6 +161,7 @@ describe('DynamicPaymentCardCurrencySelectorPage', () => { capturedOnSelectRow?.(aud!); expect(mockSetDraftValues).toHaveBeenCalledWith(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM, {currency: 'AUD'}); + expect(mockSetPaymentMethodCurrency).toHaveBeenCalledWith('AUD'); expect(mockGoBack).toHaveBeenCalledWith('settings/subscription/change-billing-currency'); }); }); From 07f0394d90ffd985ee4e4b9bf14f56e112a0dd1e Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 29 May 2026 04:39:45 -0700 Subject: [PATCH 3/7] Clear stale billing currency error and show currency note on the selector - Clear the Change billing currency form error on mount so reopening the page after a failed submission no longer shows the previous security-code error. - Show the currency note on the payment-card currency selector for the add-payment-card and workspace-owner flows. The change-billing-currency screen still shows the note on its own form, so it isn't duplicated there. --- .../ChangeBillingCurrency/index.tsx | 9 ++++-- ...DynamicPaymentCardCurrencySelectorPage.tsx | 7 ++++- ...micPaymentCardCurrencySelectorPageTest.tsx | 28 ++++++++++++++++++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx b/src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx index f6a87c109542..650fb5bd3a5b 100644 --- a/src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx +++ b/src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx @@ -13,7 +13,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; -import {clearDraftValues} from '@libs/actions/FormActions'; +import {clearDraftValues, clearErrors} from '@libs/actions/FormActions'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import {getFieldRequiredErrors, isValidSecurityCode} from '@libs/ValidationUtils'; @@ -49,7 +49,12 @@ function ChangeBillingCurrency() { Navigation.goBack(); }, [formDataComplete, prevFormDataComplete, prevIsLoading]); - useEffect(() => () => clearDraftValues(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM), []); + useEffect(() => { + // Clear any stale submission error (e.g. an incorrect security code from a previous attempt) so reopening the page starts clean, + // and drop the currency draft when leaving the flow. + clearErrors(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM); + return () => clearDraftValues(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM); + }, []); const validate = (values: FormOnyxValues): FormInputErrors => { const errors = getFieldRequiredErrors(values, REQUIRED_FIELDS, translate); diff --git a/src/pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage.tsx b/src/pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage.tsx index 7776729bbe5b..49f0b2783a95 100644 --- a/src/pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage.tsx +++ b/src/pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage.tsx @@ -1,5 +1,6 @@ import React, {useMemo} from 'react'; import type {ValueOf} from 'type-fest'; +import PaymentCardCurrencyHeader from '@components/AddPaymentCard/PaymentCardCurrencyHeader'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; @@ -14,7 +15,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {setPaymentMethodCurrency} from '@userActions/PaymentMethods'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {DYNAMIC_ROUTES} from '@src/ROUTES'; +import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/ChangeBillingCurrencyForm'; type Currency = ValueOf; @@ -24,6 +25,9 @@ function DynamicPaymentCardCurrencySelectorPage() { const {translate} = useLocalize(); const {isBetaEnabled} = usePermissions(); const backPath = useDynamicBackPath(DYNAMIC_ROUTES.PAYMENT_CARD_CURRENCY_SELECTOR.path); + // The change-billing-currency screen already renders this note above its own form, so we only show it on the + // selector for the other entry points (add payment card / workspace owner change) where it isn't displayed yet. + const shouldShowCurrencyNote = backPath !== ROUTES.SETTINGS_SUBSCRIPTION_CHANGE_BILLING_CURRENCY; const [formDraft] = useOnyx(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM_DRAFT); const [addCardForm] = useOnyx(ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM); const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); @@ -60,6 +64,7 @@ function DynamicPaymentCardCurrencySelectorPage() { : undefined} onSelectRow={(option) => { setDraftValues(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM, {[INPUT_IDS.CURRENCY]: option.value}); setPaymentMethodCurrency(option.value); diff --git a/tests/ui/DynamicPaymentCardCurrencySelectorPageTest.tsx b/tests/ui/DynamicPaymentCardCurrencySelectorPageTest.tsx index a303a1c0bbae..8a55874565c2 100644 --- a/tests/ui/DynamicPaymentCardCurrencySelectorPageTest.tsx +++ b/tests/ui/DynamicPaymentCardCurrencySelectorPageTest.tsx @@ -1,5 +1,6 @@ import {render} from '@testing-library/react-native'; import React from 'react'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import {setDraftValues} from '@libs/actions/FormActions'; @@ -12,6 +13,7 @@ type CurrencyOption = {text: string; value: string; keyForList: string; isSelect let capturedData: CurrencyOption[] = []; let capturedOnSelectRow: ((option: CurrencyOption) => void) | undefined; +let capturedCustomListHeader: React.ReactNode; jest.mock('@hooks/usePermissions', () => jest.fn(() => ({isBetaEnabled: () => false}))); @@ -64,9 +66,10 @@ jest.mock('@components/HeaderWithBackButton', () => { }); jest.mock('@components/SelectionList', () => { - function MockSelectionList({data, onSelectRow}: {data: CurrencyOption[]; onSelectRow: (option: CurrencyOption) => void}) { + function MockSelectionList({data, onSelectRow, customListHeader}: {data: CurrencyOption[]; onSelectRow: (option: CurrencyOption) => void; customListHeader?: React.ReactNode}) { capturedData = data ?? []; capturedOnSelectRow = onSelectRow; + capturedCustomListHeader = customListHeader; return (data ?? []).map((item) => item.text).join(','); } return MockSelectionList; @@ -74,8 +77,11 @@ jest.mock('@components/SelectionList', () => { jest.mock('@components/SelectionList/ListItem/SingleSelectListItem', () => 'SingleSelectListItem'); +jest.mock('@components/AddPaymentCard/PaymentCardCurrencyHeader', () => 'PaymentCardCurrencyHeader'); + const mockUsePermissions = jest.mocked(usePermissions); const mockUseOnyx = jest.mocked(useOnyx); +const mockUseDynamicBackPath = jest.mocked(useDynamicBackPath); const mockSetDraftValues = jest.mocked(setDraftValues); const mockSetPaymentMethodCurrency = jest.mocked(setPaymentMethodCurrency); const mockGoBack = jest.mocked(Navigation.goBack); @@ -106,7 +112,9 @@ describe('DynamicPaymentCardCurrencySelectorPage', () => { jest.clearAllMocks(); capturedData = []; capturedOnSelectRow = undefined; + capturedCustomListHeader = undefined; mockUsePermissions.mockReturnValue({isBetaEnabled: () => false}); + mockUseDynamicBackPath.mockReturnValue('settings/subscription/change-billing-currency'); mockOnyx(); }); @@ -164,4 +172,22 @@ describe('DynamicPaymentCardCurrencySelectorPage', () => { expect(mockSetPaymentMethodCurrency).toHaveBeenCalledWith('AUD'); expect(mockGoBack).toHaveBeenCalledWith('settings/subscription/change-billing-currency'); }); + + it('shows the currency note when opened from a flow that does not already display it (e.g. add payment card)', () => { + mockUseDynamicBackPath.mockReturnValue('settings/subscription/add-payment-card'); + + render(); + + const header = capturedCustomListHeader as React.ReactElement<{isSectionList?: boolean}>; + expect(header).toBeTruthy(); + expect(header.type).toBe('PaymentCardCurrencyHeader'); + expect(header.props.isSectionList).toBe(true); + }); + + it('hides the currency note when opened from change billing currency, which already shows it on the form', () => { + // The default mocked back path is the change-billing-currency screen. + render(); + + expect(capturedCustomListHeader).toBeUndefined(); + }); }); From 5a8d0b3e08418639bab6bd8459de522eb5a24291 Mon Sep 17 00:00:00 2001 From: Tom Date: Sat, 30 May 2026 04:19:58 -0700 Subject: [PATCH 4/7] Rename CurrencySelection route and page to WorkspaceCurrencySelection --- src/ROUTES.ts | 2 +- src/components/CurrencySelector.tsx | 4 ++-- src/components/WorkspaceConfirmationForm.tsx | 2 +- .../AppNavigator/ModalStackNavigators/index.tsx | 2 +- src/libs/Navigation/linkingConfig/config.ts | 2 +- ...lectionPage.tsx => WorkspaceCurrencySelectionPage.tsx} | 8 ++++---- 6 files changed, 10 insertions(+), 10 deletions(-) rename src/pages/{CurrencySelectionPage.tsx => WorkspaceCurrencySelectionPage.tsx} (89%) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index da293167a76f..99d2e2dd88b0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3292,7 +3292,7 @@ const ROUTES = { getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding/workspace-currency`, backTo), }, - CURRENCY_SELECTION: { + WORKSPACE_CURRENCY_SELECTION: { route: 'workspace/confirmation/currency', getRoute: (backTo?: string) => getUrlWithBackToParam(`workspace/confirmation/currency`, backTo), diff --git a/src/components/CurrencySelector.tsx b/src/components/CurrencySelector.tsx index 3c78d7d36ecd..b28b210a5d0d 100644 --- a/src/components/CurrencySelector.tsx +++ b/src/components/CurrencySelector.tsx @@ -29,7 +29,7 @@ type CurrencySelectorProps = { onBlur?: () => void; /** Optional route override; when omitted the selector opens the dynamic payment-card currency picker. */ - currencySelectorRoute?: typeof ROUTES.CURRENCY_SELECTION; + currencySelectorRoute?: typeof ROUTES.WORKSPACE_CURRENCY_SELECTION; /** Label for the input */ label?: string; @@ -76,7 +76,7 @@ function CurrencySelector({errorText = '', value: currency, onInputChange = () = errorText={errorText} onPress={() => { didOpenCurrencySelector.current = true; - if (currencySelectorRoute === ROUTES.CURRENCY_SELECTION) { + if (currencySelectorRoute === ROUTES.WORKSPACE_CURRENCY_SELECTION) { Navigation.navigate(currencySelectorRoute.getRoute(Navigation.getActiveRoute())); return; } diff --git a/src/components/WorkspaceConfirmationForm.tsx b/src/components/WorkspaceConfirmationForm.tsx index 39d6915db492..e6433fbe2982 100644 --- a/src/components/WorkspaceConfirmationForm.tsx +++ b/src/components/WorkspaceConfirmationForm.tsx @@ -251,7 +251,7 @@ function WorkspaceConfirmationForm({onSubmit, policyOwnerEmail = '', onBackButto label={translate('workspace.editor.currencyInputLabel')} value={userCurrency} shouldShowCurrencySymbol - currencySelectorRoute={ROUTES.CURRENCY_SELECTION} + currencySelectorRoute={ROUTES.WORKSPACE_CURRENCY_SELECTION} /> {isApprovedAccountant && ( diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index dc796a94ae2d..b2e5d33bc397 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -289,7 +289,7 @@ const WorkspaceConfirmationModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/WorkspaceConfirmationPage').default, [SCREENS.WORKSPACE_CONFIRMATION.OWNER_SELECTOR]: () => require('../../../../pages/workspace/WorkspaceConfirmationOwnerSelectorPage').default, [SCREENS.WORKSPACE_CONFIRMATION.SUCCESS]: () => require('../../../../pages/workspace/WorkspaceConfirmationSuccessPage').default, - [SCREENS.CURRENCY.SELECTION]: () => require('../../../../pages/CurrencySelectionPage').default, + [SCREENS.CURRENCY.SELECTION]: () => require('../../../../pages/WorkspaceCurrencySelectionPage').default, }); const WorkspaceDuplicateModalStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 3962e03bacc8..e1c2681d9220 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1619,7 +1619,7 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: ROUTES.WORKSPACE_CONFIRMATION.route, [SCREENS.WORKSPACE_CONFIRMATION.OWNER_SELECTOR]: ROUTES.WORKSPACE_CONFIRMATION_OWNER_SELECTOR, [SCREENS.WORKSPACE_CONFIRMATION.SUCCESS]: ROUTES.WORKSPACE_CONFIRMATION_SUCCESS, - [SCREENS.CURRENCY.SELECTION]: ROUTES.CURRENCY_SELECTION.route, + [SCREENS.CURRENCY.SELECTION]: ROUTES.WORKSPACE_CURRENCY_SELECTION.route, }, }, [SCREENS.RIGHT_MODAL.WORKSPACE_DUPLICATE]: { diff --git a/src/pages/CurrencySelectionPage.tsx b/src/pages/WorkspaceCurrencySelectionPage.tsx similarity index 89% rename from src/pages/CurrencySelectionPage.tsx rename to src/pages/WorkspaceCurrencySelectionPage.tsx index 423b49b6d863..d28c51ff8ad8 100644 --- a/src/pages/CurrencySelectionPage.tsx +++ b/src/pages/WorkspaceCurrencySelectionPage.tsx @@ -14,7 +14,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; -type CurrencySelectionPageProps = { +type WorkspaceCurrencySelectionPageProps = { route: { params: { backTo?: Route; @@ -22,7 +22,7 @@ type CurrencySelectionPageProps = { }; }; -function CurrencySelectionPage({route}: CurrencySelectionPageProps) { +function WorkspaceCurrencySelectionPage({route}: WorkspaceCurrencySelectionPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -45,7 +45,7 @@ function CurrencySelectionPage({route}: CurrencySelectionPageProps) { ); return ( - + Date: Mon, 1 Jun 2026 20:52:23 -0700 Subject: [PATCH 5/7] Fix doubled horizontal padding on the change-owner payment card form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WorkspaceOwnerChangeWrapperPage wrapped the payment card form in ph5 (20px) on the non-NO_BILLING_CARD branch, which stacked on PaymentCardForm's own mh5 (20px) for a 40px inset — double the add-payment-card page. Key the wrapper padding off shouldShowPaymentCardForm so every form path renders at the same 20px inset, while non-form content keeps ph5. --- src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx b/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx index 8039d361929b..b0f4fc1877e2 100644 --- a/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx +++ b/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx @@ -100,7 +100,7 @@ function WorkspaceOwnerChangeWrapperPage({route, policy, isLoadingPolicy}: Works } }} /> - + {isLoading && ( Date: Tue, 2 Jun 2026 06:49:54 -0700 Subject: [PATCH 6/7] Trigger CI re-run for flaky UnreadIndicators/GroupChatName test job From 0fc04530796c268bdcd2a221906fe5ad2346978d Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 2 Jun 2026 18:36:02 -0700 Subject: [PATCH 7/7] Reset mirrored currency selection when leaving the change-billing-currency flow The shared currency selector mirrors the picked currency into ADD_PAYMENT_CARD_FORM via setPaymentMethodCurrency. The change-billing-currency row reads the billing draft, but the selector falls back to ADD_PAYMENT_CARD_FORM.currency, so after a failed save + exit the selector still showed the previously-picked currency (e.g. NZD) while the row correctly reset to the card's currency (e.g. USD). Reset the mirrored currency to the card's actual currency on flow exit so reopening the selector matches the row. --- .../PaymentCard/ChangeBillingCurrency/index.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx b/src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx index 650fb5bd3a5b..1da1362e434a 100644 --- a/src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx +++ b/src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useMemo} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; import PaymentCardCurrencyHeader from '@components/AddPaymentCard/PaymentCardCurrencyHeader'; @@ -38,6 +38,12 @@ function ChangeBillingCurrency() { const initialCurrency = defaultCard?.accountData?.currency; const currency = (formDraft?.[INPUT_IDS.CURRENCY] ?? initialCurrency ?? CONST.PAYMENT_CARD_CURRENCY.USD) as Currency; + // Keep the latest card currency available to the unmount cleanup below without re-running it on every change. + const initialCurrencyRef = useRef(initialCurrency); + useEffect(() => { + initialCurrencyRef.current = initialCurrency; + }, [initialCurrency]); + const formDataComplete = formData?.isLoading === false && !formData.errors; const prevIsLoading = usePrevious(formData?.isLoading); const prevFormDataComplete = usePrevious(formDataComplete); @@ -53,7 +59,12 @@ function ChangeBillingCurrency() { // Clear any stale submission error (e.g. an incorrect security code from a previous attempt) so reopening the page starts clean, // and drop the currency draft when leaving the flow. clearErrors(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM); - return () => clearDraftValues(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM); + return () => { + clearDraftValues(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM); + // The currency selector mirrors the picked currency into ADD_PAYMENT_CARD_FORM; reset it on exit so reopening + // the flow doesn't show a stale selection that no longer matches the card's actual currency. + PaymentMethods.setPaymentMethodCurrency((initialCurrencyRef.current ?? CONST.PAYMENT_CARD_CURRENCY.USD) as Currency); + }; }, []); const validate = (values: FormOnyxValues): FormInputErrors => {