From 03bc82f9ff7f3a382ac989320378b4f1e1d9c572 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Tue, 2 Jun 2026 12:53:53 -0400 Subject: [PATCH 01/38] add the new routes --- src/CONST/index.ts | 1 + src/ROUTES.ts | 4 ++ src/SCREENS.ts | 1 + .../configuration/SpendRulesCurrencyBase.tsx | 13 ++++++ src/languages/en.ts | 1 + .../ModalStackNavigators/index.tsx | 1 + .../RELATIONS/WORKSPACE_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 ++ src/libs/Navigation/types.ts | 4 ++ .../SpendRules/SpendRuleCurrenciesPage.tsx | 5 +++ .../rules/SpendRules/SpendRulePageBase.tsx | 40 ++++++++++++------- 11 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 src/components/SpendRules/configuration/SpendRulesCurrencyBase.tsx create mode 100644 src/pages/workspace/rules/SpendRules/SpendRuleCurrenciesPage.tsx diff --git a/src/CONST/index.ts b/src/CONST/index.ts index e0a12e750777..d71c46700650 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -9841,6 +9841,7 @@ const CONST = { MERCHANT_RULE_PREVIEW_MATCHES: 'WorkspaceRules-MerchantRulePreviewMatches', MERCHANT_RULE_DELETE: 'WorkspaceRules-MerchantRuleDelete', CATEGORY_SELECTOR: 'WorkspaceRules-CategorySelector', + CURRENCY_SELECTOR: 'WorkspaceRules-CurrencySelector', SPEND_RULE_SECTION_ITEM: 'WorkspaceRules-SpendRuleSectionItem', SPEND_RULE_SAVE: 'WorkspaceRules-SpendRuleSave', SPEND_RULE_RESTRICTION_TYPE: 'WorkspaceRules-SpendRuleRestrictionType', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2cb9d3cb3d1b..a5ff5794173a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3185,6 +3185,10 @@ const ROUTES = { route: 'workspaces/:policyID/rules/spend-rules/:ruleID/merchants', getRoute: (policyID: string, ruleID?: string) => `workspaces/${policyID}/rules/spend-rules/${ruleID ?? ROUTES.NEW}/merchants` as const, }, + RULES_SPEND_CURRENCIES: { + route: 'workspaces/:policyID/rules/spend-rules/:ruleID/currencies', + getRoute: (policyID: string, ruleID?: string) => `workspaces/${policyID}/rules/spend-rules/${ruleID ?? ROUTES.NEW}/currencies` as const, + }, RULES_SPEND_MERCHANT_EDIT: { route: 'workspaces/:policyID/rules/spend-rules/:ruleID/merchants/:merchantIndex', getRoute: (policyID: string, ruleID: string, merchantIndex: string) => `workspaces/${policyID}/rules/spend-rules/${ruleID}/merchants/${merchantIndex}` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index c541dd306aaa..db10436cfc36 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -896,6 +896,7 @@ const SCREENS = { RULES_SPEND_CARD: 'Rules_Spend_Card', RULES_SPEND_CATEGORY: 'Rules_Spend_Category', RULES_SPEND_MAX_AMOUNT: 'Rules_Spend_Max_Amount', + RULES_SPEND_CURRENCIES: 'Rules_Spend_Currencies', RULES_MERCHANT_MERCHANT_TO_MATCH: 'Rules_Merchant_Merchant_To_Match', RULES_MERCHANT_MATCH_TYPE: 'Rules_Merchant_Match_Type', RULES_MERCHANT_MERCHANT: 'Rules_Merchant_Merchant', diff --git a/src/components/SpendRules/configuration/SpendRulesCurrencyBase.tsx b/src/components/SpendRules/configuration/SpendRulesCurrencyBase.tsx new file mode 100644 index 000000000000..c10deeed51d0 --- /dev/null +++ b/src/components/SpendRules/configuration/SpendRulesCurrencyBase.tsx @@ -0,0 +1,13 @@ +import {useCurrencyListState} from '@hooks/useCurrencyList'; +import useOnyx from '@hooks/useOnyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {CurrencyList} from '@src/types/onyx'; +import {getEmptyObject} from '@src/types/utils/EmptyObject'; + +type SpendRulesCurrencyBaseProps = {}; + +export default function SpendRulesCurrencyBase({}: SpendRulesCurrencyBaseProps) { + const [currencyList = getEmptyObject()] = useOnyx(ONYXKEYS.CURRENCY_LIST); + + return <>; +} diff --git a/src/languages/en.ts b/src/languages/en.ts index f9f5f69df835..5a69b8789c6f 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7267,6 +7267,7 @@ const translations = { confirmErrorApplyAtLeastOneSpendRule: 'Apply at least one spend rule', categories: 'Categories', merchants: 'Merchants', + permittedCurrencies: 'Permitted currencies', noAvailableCards: 'All cards already have a rule', noAvailableCardsSubtitle: 'Edit an existing card rule to make changes', noCardsIssuedTitle: 'No Expensify Cards issued', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index a3f39ee3bdaf..3d595c78aa37 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -1074,6 +1074,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/rules/SpendRules/SpendRuleCardPage').default, [SCREENS.WORKSPACE.RULES_SPEND_CATEGORY]: () => require('../../../../pages/workspace/rules/SpendRules/SpendRuleCategoryPage').default, [SCREENS.WORKSPACE.RULES_SPEND_MAX_AMOUNT]: () => require('../../../../pages/workspace/rules/SpendRules/SpendRuleMaxAmountPage').default, + [SCREENS.WORKSPACE.RULES_SPEND_CURRENCIES]: () => require('../../../../pages/workspace/rules/SpendRules/SpendRuleCurrenciesPage').default, [SCREENS.WORKSPACE.RULES_SPEND_MERCHANTS]: () => require('../../../../pages/workspace/rules/SpendRules/SpendRuleMerchantsPage').default, [SCREENS.WORKSPACE.RULES_SPEND_MERCHANT_EDIT]: () => require('../../../../pages/workspace/rules/SpendRules/SpendRuleMerchantEditPage').default, [SCREENS.WORKSPACE.RULES_MERCHANT_MERCHANT_TO_MATCH]: () => require('../../../../pages/workspace/rules/MerchantRules/AddMerchantToMatchPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index 9592240bad4e..8ba1094fbcaa 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -336,6 +336,7 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.RULES_SPEND_MAX_AMOUNT]: { path: ROUTES.RULES_SPEND_MAX_AMOUNT.route, }, + [SCREENS.WORKSPACE.RULES_SPEND_CURRENCIES]: { + path: ROUTES.RULES_SPEND_CURRENCIES.route, + }, [SCREENS.WORKSPACE.RULES_SPEND_MERCHANTS]: { path: ROUTES.RULES_SPEND_MERCHANTS.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index dcd5071a87e0..3d7dea0b3bfa 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1503,6 +1503,10 @@ type SettingsNavigatorParamList = { policyID: string; ruleID: string; }; + [SCREENS.WORKSPACE.RULES_SPEND_CURRENCIES]: { + policyID: string; + ruleID: string; + }; [SCREENS.WORKSPACE.RULES_SPEND_MERCHANTS]: { policyID: string; ruleID: string; diff --git a/src/pages/workspace/rules/SpendRules/SpendRuleCurrenciesPage.tsx b/src/pages/workspace/rules/SpendRules/SpendRuleCurrenciesPage.tsx new file mode 100644 index 000000000000..00f8b3c5be91 --- /dev/null +++ b/src/pages/workspace/rules/SpendRules/SpendRuleCurrenciesPage.tsx @@ -0,0 +1,5 @@ +type SpendRuleCurrenciesPageProps = {}; + +export default function SpendRuleCurrenciesPage({}: SpendRuleCurrenciesPageProps) { + return <>; +} diff --git a/src/pages/workspace/rules/SpendRules/SpendRulePageBase.tsx b/src/pages/workspace/rules/SpendRules/SpendRulePageBase.tsx index 54bc5f09c9af..b934babd46b4 100644 --- a/src/pages/workspace/rules/SpendRules/SpendRulePageBase.tsx +++ b/src/pages/workspace/rules/SpendRules/SpendRulePageBase.tsx @@ -218,6 +218,31 @@ function SpendRulePageBase({policyID, ruleID, titleKey, testID}: SpendRulePageBa sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.MERCHANT_RULE_SECTION_ITEM} /> {translate('workspace.rules.spendRules.spendRuleSectionTitle')} + + { + clearError(); + if (!selectedCurrency) { + openCurrencyMismatchModal(); + return; + } + navigation.navigate(SCREENS.WORKSPACE.RULES_SPEND_MAX_AMOUNT, {policyID, ruleID: currentRuleID}); + }} + shouldShowRightIcon + title={maxAmountMenuTitle} + titleStyle={styles.flex1} + sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.MERCHANT_RULE_SECTION_ITEM} + /> + {}} + shouldShowRightIcon + title={maxAmountMenuTitle} + titleStyle={styles.flex1} + sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.CURRENCY_SELECTOR} + /> + - { - clearError(); - if (!selectedCurrency) { - openCurrencyMismatchModal(); - return; - } - navigation.navigate(SCREENS.WORKSPACE.RULES_SPEND_MAX_AMOUNT, {policyID, ruleID: currentRuleID}); - }} - shouldShowRightIcon - title={maxAmountMenuTitle} - titleStyle={styles.flex1} - sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.MERCHANT_RULE_SECTION_ITEM} - /> Date: Tue, 2 Jun 2026 12:56:53 -0400 Subject: [PATCH 02/38] add page content --- src/CONST/index.ts | 1 + .../configuration/SpendRulesCurrencyBase.tsx | 9 +++++--- .../SpendRules/SpendRuleCurrenciesPage.tsx | 21 ++++++++++++++++--- .../rules/SpendRules/SpendRulePageBase.tsx | 5 ++++- src/types/form/SpendRuleForm.ts | 1 + 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index d71c46700650..e3b4c291c9e6 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -4680,6 +4680,7 @@ const CONST = { MERCHANT_MATCH_TYPES: 'merchantMatchTypes', CATEGORIES: 'categories', MAX_AMOUNT: 'maxAmount', + CURRENCIES: 'currencies', }, }, ACTION: { diff --git a/src/components/SpendRules/configuration/SpendRulesCurrencyBase.tsx b/src/components/SpendRules/configuration/SpendRulesCurrencyBase.tsx index c10deeed51d0..66302ad35a9c 100644 --- a/src/components/SpendRules/configuration/SpendRulesCurrencyBase.tsx +++ b/src/components/SpendRules/configuration/SpendRulesCurrencyBase.tsx @@ -1,12 +1,15 @@ -import {useCurrencyListState} from '@hooks/useCurrencyList'; +import React from 'react'; import useOnyx from '@hooks/useOnyx'; import ONYXKEYS from '@src/ONYXKEYS'; import {CurrencyList} from '@src/types/onyx'; import {getEmptyObject} from '@src/types/utils/EmptyObject'; -type SpendRulesCurrencyBaseProps = {}; +type SpendRulesCurrencyBaseProps = { + currencies: string[]; + onCurrenciesChange: (currencies: string[]) => void; +}; -export default function SpendRulesCurrencyBase({}: SpendRulesCurrencyBaseProps) { +export default function SpendRulesCurrencyBase({currencies, onCurrenciesChange}: SpendRulesCurrencyBaseProps) { const [currencyList = getEmptyObject()] = useOnyx(ONYXKEYS.CURRENCY_LIST); return <>; diff --git a/src/pages/workspace/rules/SpendRules/SpendRuleCurrenciesPage.tsx b/src/pages/workspace/rules/SpendRules/SpendRuleCurrenciesPage.tsx index 00f8b3c5be91..2d79e3f63935 100644 --- a/src/pages/workspace/rules/SpendRules/SpendRuleCurrenciesPage.tsx +++ b/src/pages/workspace/rules/SpendRules/SpendRuleCurrenciesPage.tsx @@ -1,5 +1,20 @@ -type SpendRuleCurrenciesPageProps = {}; +import React from 'react'; +import SpendRulesCurrencyBase from '@components/SpendRules/configuration/SpendRulesCurrencyBase'; +import useOnyx from '@hooks/useOnyx'; +import {updateDraftSpendRule} from '@libs/actions/User'; +import ONYXKEYS from '@src/ONYXKEYS'; -export default function SpendRuleCurrenciesPage({}: SpendRuleCurrenciesPageProps) { - return <>; +export default function SpendRuleCurrenciesPage() { + const [spendRuleForm] = useOnyx(ONYXKEYS.FORMS.SPEND_RULE_FORM); + + const onCurrenciesChange = (currencies: string[]) => { + updateDraftSpendRule({currencies}); + }; + + return ( + + ); } diff --git a/src/pages/workspace/rules/SpendRules/SpendRulePageBase.tsx b/src/pages/workspace/rules/SpendRules/SpendRulePageBase.tsx index b934babd46b4..8dda4aec3152 100644 --- a/src/pages/workspace/rules/SpendRules/SpendRulePageBase.tsx +++ b/src/pages/workspace/rules/SpendRules/SpendRulePageBase.tsx @@ -236,7 +236,10 @@ function SpendRulePageBase({policyID, ruleID, titleKey, testID}: SpendRulePageBa /> {}} + onPress={() => { + clearError(); + navigation.navigate(SCREENS.WORKSPACE.RULES_SPEND_CURRENCIES, {policyID, ruleID: currentRuleID}); + }} shouldShowRightIcon title={maxAmountMenuTitle} titleStyle={styles.flex1} diff --git a/src/types/form/SpendRuleForm.ts b/src/types/form/SpendRuleForm.ts index e1d9568b64cc..a35a17ced33e 100644 --- a/src/types/form/SpendRuleForm.ts +++ b/src/types/form/SpendRuleForm.ts @@ -20,6 +20,7 @@ type SpendRuleForm = Form< [INPUT_IDS.MERCHANT_NAMES]: string[]; [INPUT_IDS.MERCHANT_MATCH_TYPES]: Array>; [INPUT_IDS.CATEGORIES]: SpendRuleCategory[]; + [INPUT_IDS.CURRENCIES]: string[]; [INPUT_IDS.MAX_AMOUNT]: string; } >; From 04ae9274d70391ef653f1e5b820d0ea10cb3dc04 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Tue, 2 Jun 2026 14:19:57 -0400 Subject: [PATCH 03/38] create the page --- .../configuration/SpendRulesCurrencyBase.tsx | 138 +++++++++++++++++- 1 file changed, 131 insertions(+), 7 deletions(-) diff --git a/src/components/SpendRules/configuration/SpendRulesCurrencyBase.tsx b/src/components/SpendRules/configuration/SpendRulesCurrencyBase.tsx index 66302ad35a9c..7adb4a2ac47c 100644 --- a/src/components/SpendRules/configuration/SpendRulesCurrencyBase.tsx +++ b/src/components/SpendRules/configuration/SpendRulesCurrencyBase.tsx @@ -1,16 +1,140 @@ -import React from 'react'; -import useOnyx from '@hooks/useOnyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import {CurrencyList} from '@src/types/onyx'; -import {getEmptyObject} from '@src/types/utils/EmptyObject'; +import React, {useState} from 'react'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import {useCurrencyListActions, useCurrencyListState} from '@components/CurrencyListContextProvider'; +import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import SelectionList from '@components/SelectionList'; +import MultiSelectListItem from '@components/SelectionList/ListItem/MultiSelectListItem'; +import {ListItem} from '@components/SelectionList/types'; +import useLocalize from '@hooks/useLocalize'; +import useSearchResults from '@hooks/useSearchResults'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; +import {getCurrencyOptions} from '@libs/SearchUIUtils'; +import variables from '@styles/variables'; +import Navigation from '@src/Navigation/Navigation'; type SpendRulesCurrencyBaseProps = { currencies: string[]; onCurrenciesChange: (currencies: string[]) => void; }; +type CurrencyListItem = ListItem & { + value: string; +}; + export default function SpendRulesCurrencyBase({currencies, onCurrenciesChange}: SpendRulesCurrencyBaseProps) { - const [currencyList = getEmptyObject()] = useOnyx(ONYXKEYS.CURRENCY_LIST); + const styles = useThemeStyles(); + const {translate, localeCompare} = useLocalize(); + const [selectedCurrencies, setSelectedCurrencies] = useState(currencies); + + const {currencyList} = useCurrencyListState(); + const {getCurrencySymbol} = useCurrencyListActions(); + const currencyOptions = getCurrencyOptions(currencyList, getCurrencySymbol); + + const currencyItems: CurrencyListItem[] = currencyOptions.map((currencyDetails) => ({ + keyForList: currencyDetails.value, + text: currencyDetails.text, + value: currencyDetails.value, + isSelected: selectedCurrencies.includes(currencyDetails.value), + })); + + const filterCurrency = (item: CurrencyListItem, searchInput: string) => { + return (item.text ?? '').toLowerCase().includes(searchInput.toLowerCase()); + }; + + const sortCurrencies = (items: CurrencyListItem[]) => { + return items.sort((a, b) => localeCompare(a.text ?? '', b.text ?? '')); + }; + + const [inputValue, setInputValue, filteredCategoryItems] = useSearchResults(currencyItems, filterCurrency, sortCurrencies); + + const toggleCurrency = (item: CurrencyListItem) => { + setSelectedCurrencies((prev) => { + if (prev.includes(item.value)) { + return prev.filter((currency) => currency !== item.value); + } + return [...prev, item.value]; + }); + }; + + const toggleSelectAll = () => { + const visibleValues = filteredCategoryItems.map((item) => item.value); + const allVisibleSelected = visibleValues.length > 0 && visibleValues.every((value) => selectedCurrencies.includes(value)); + + if (allVisibleSelected) { + const visibleSet = new Set(visibleValues); + setSelectedCurrencies((prev) => prev.filter((currency) => !visibleSet.has(currency))); + return; + } + + setSelectedCurrencies((prev) => { + const next = new Set([...prev, ...visibleValues]); + return Array.from(next); + }); + }; + + const goBack = () => { + Navigation.goBack(); + }; + + const handleSave = () => { + onCurrenciesChange(selectedCurrencies); + goBack(); + }; - return <>; + return ( + + + 0 ? toggleSelectAll : undefined} + textInputOptions={{ + value: inputValue, + label: translate('common.search'), + onChangeText: setInputValue, + }} + style={{ + listHeaderWrapperStyle: [styles.pt5, styles.pb2], + listHeaderSelectAllTextStyle: [styles.textLabelSupporting], + }} + listEmptyContent={ + + + + } + footerContent={ + + } + /> + + ); } From a473ea57d8cbcc25e444e153743ec7538f44c3ff Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Mon, 8 Jun 2026 14:30:06 -0400 Subject: [PATCH 04/38] cleanup types --- .../SpendRuleRestrictionTypeToggle.tsx | 10 ++++++++++ .../configuration/SpendRulesCurrencyBase.tsx | 17 ++--------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/components/SpendRules/SpendRuleRestrictionTypeToggle.tsx b/src/components/SpendRules/SpendRuleRestrictionTypeToggle.tsx index 24199964cbb3..4b4c9e3d33e3 100644 --- a/src/components/SpendRules/SpendRuleRestrictionTypeToggle.tsx +++ b/src/components/SpendRules/SpendRuleRestrictionTypeToggle.tsx @@ -26,6 +26,16 @@ function SpendRuleRestrictionTypeToggle({restrictionAction, onSelect}: SpendRule {translate('workspace.rules.spendRules.restrictionType')} +