diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index a340b5a44120..1d33b6892d51 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -424,6 +424,9 @@ const ONYXKEYS = { /** Stores the route to open after changing app permission from settings */ LAST_ROUTE: 'lastRoute', + /** Stores recently used currencies */ + RECENTLY_USED_CURRENCIES: 'nvp_recentlyUsedCurrencies', + /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -836,7 +839,7 @@ type OnyxValuesMapping = { // ONYXKEYS.NVP_TRYNEWDOT is HybridApp onboarding data [ONYXKEYS.NVP_TRYNEWDOT]: OnyxTypes.TryNewDot; - + [ONYXKEYS.RECENTLY_USED_CURRENCIES]: string[]; [ONYXKEYS.ACTIVE_CLIENTS]: string[]; [ONYXKEYS.DEVICE_ID]: string; [ONYXKEYS.IS_SIDEBAR_LOADED]: boolean; diff --git a/src/components/CurrencySelectionList/index.tsx b/src/components/CurrencySelectionList/index.tsx index 9cb0ba270aec..201ed7bab730 100644 --- a/src/components/CurrencySelectionList/index.tsx +++ b/src/components/CurrencySelectionList/index.tsx @@ -1,5 +1,5 @@ import {Str} from 'expensify-common'; -import React, {useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {withOnyx} from 'react-native-onyx'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -7,12 +7,21 @@ import SelectableListItem from '@components/SelectionList/SelectableListItem'; import useLocalize from '@hooks/useLocalize'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import ONYXKEYS from '@src/ONYXKEYS'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {CurrencyListItem, CurrencySelectionListOnyxProps, CurrencySelectionListProps} from './types'; -function CurrencySelectionList({searchInputLabel, initiallySelectedCurrencyCode, onSelect, currencyList, selectedCurrencies = [], canSelectMultiple = false}: CurrencySelectionListProps) { +function CurrencySelectionList({ + searchInputLabel, + initiallySelectedCurrencyCode, + onSelect, + currencyList, + selectedCurrencies = [], + canSelectMultiple = false, + recentlyUsedCurrencies, +}: CurrencySelectionListProps) { const [searchValue, setSearchValue] = useState(''); const {translate} = useLocalize(); - + const getUnselectedOptions = useCallback((options: CurrencyListItem[]) => options.filter((option) => !option.isSelected), []); const {sections, headerMessage} = useMemo(() => { const currencyOptions: CurrencyListItem[] = Object.entries(currencyList ?? {}).reduce((acc, [currencyCode, currencyInfo]) => { const isSelectedCurrency = currencyCode === initiallySelectedCurrencyCode || selectedCurrencies.includes(currencyCode); @@ -28,23 +37,56 @@ function CurrencySelectionList({searchInputLabel, initiallySelectedCurrencyCode, return acc; }, [] as CurrencyListItem[]); + const recentlyUsedCurrencyOptions: CurrencyListItem[] = Array.isArray(recentlyUsedCurrencies) + ? recentlyUsedCurrencies?.map((currencyCode) => { + const currencyInfo = currencyList?.[currencyCode]; + const isSelectedCurrency = currencyCode === initiallySelectedCurrencyCode; + return { + currencyName: currencyInfo?.name ?? '', + text: `${currencyCode} - ${CurrencyUtils.getCurrencySymbol(currencyCode)}`, + currencyCode, + keyForList: currencyCode, + isSelected: isSelectedCurrency, + }; + }) + : []; + const searchRegex = new RegExp(Str.escapeForRegExp(searchValue.trim()), 'i'); const filteredCurrencies = currencyOptions.filter((currencyOption) => searchRegex.test(currencyOption.text ?? '') || searchRegex.test(currencyOption.currencyName)); const isEmpty = searchValue.trim() && !filteredCurrencies.length; + const shouldDisplayRecentlyOptions = !isEmptyObject(recentlyUsedCurrencyOptions) && !searchValue; + const selectedOptions = filteredCurrencies.filter((option) => option.isSelected); + const shouldDisplaySelectedOptionOnTop = selectedOptions.length > 0; + const unselectedOptions = getUnselectedOptions(filteredCurrencies); + const result = []; - let computedSections: Array<{data: CurrencyListItem[]}> = []; + if (shouldDisplaySelectedOptionOnTop) { + result.push({ + title: '', + data: selectedOptions, + shouldShow: true, + }); + } - if (!isEmpty) { - computedSections = canSelectMultiple - ? [{data: filteredCurrencies.filter((currency) => currency.isSelected)}, {data: filteredCurrencies.filter((currency) => !currency.isSelected)}] - : [{data: filteredCurrencies}]; + if (shouldDisplayRecentlyOptions) { + if (!isEmpty) { + result.push( + { + title: translate('common.recents'), + data: shouldDisplaySelectedOptionOnTop ? getUnselectedOptions(recentlyUsedCurrencyOptions) : recentlyUsedCurrencyOptions, + shouldShow: shouldDisplayRecentlyOptions, + }, + {title: translate('common.all'), data: shouldDisplayRecentlyOptions ? unselectedOptions : filteredCurrencies}, + ); + } + } else if (!isEmpty) { + result.push({ + data: shouldDisplaySelectedOptionOnTop ? unselectedOptions : filteredCurrencies, + }); } - return { - sections: computedSections, - headerMessage: isEmpty ? translate('common.noResultsFound') : '', - }; - }, [currencyList, searchValue, canSelectMultiple, translate, initiallySelectedCurrencyCode, selectedCurrencies]); + return {sections: result, headerMessage: isEmpty ? translate('common.noResultsFound') : ''}; + }, [currencyList, searchValue, translate, initiallySelectedCurrencyCode, selectedCurrencies, getUnselectedOptions, recentlyUsedCurrencies]); return ( void; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5d2ab768ecd8..72d40b99b02b 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -523,6 +523,7 @@ function buildOnyxDataForMoneyRequest( optimisticNextStep?: OnyxTypes.ReportNextStep | null, isOneOnOneSplit = false, existingTransactionThreadReportID?: string, + optimisticRecentlyUsedCurrencies?: string[], ): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] { const isScanRequest = TransactionUtils.isScanRequest(transaction); const outstandingChildRequest = ReportUtils.getOutstandingChildRequest(iouReport); @@ -648,6 +649,14 @@ function buildOnyxDataForMoneyRequest( }); } + if (optimisticRecentlyUsedCurrencies?.length) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.RECENTLY_USED_CURRENCIES, + value: optimisticRecentlyUsedCurrencies, + }); + } + if (!isEmptyObject(optimisticPolicyRecentlyUsedTags)) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -919,6 +928,7 @@ function buildOnyxDataForInvoice( policy?: OnyxEntry, policyTagList?: OnyxEntry, policyCategories?: OnyxEntry, + optimisticRecentlyUsedCurrencies?: string[], companyName?: string, companyWebsite?: string, ): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] { @@ -1009,6 +1019,14 @@ function buildOnyxDataForInvoice( }); } + if (optimisticRecentlyUsedCurrencies?.length) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.RECENTLY_USED_CURRENCIES, + value: optimisticRecentlyUsedCurrencies, + }); + } + if (!isEmptyObject(optimisticPolicyRecentlyUsedTags)) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -1897,6 +1915,7 @@ function getSendInvoiceInformation( const optimisticPolicyRecentlyUsedCategories = Category.buildOptimisticPolicyRecentlyUsedCategories(optimisticInvoiceReport.policyID, category); const optimisticPolicyRecentlyUsedTags = Tag.buildOptimisticPolicyRecentlyUsedTags(optimisticInvoiceReport.policyID, tag); + const optimisticRecentlyUsedCurrencies = Policy.buildOptimisticRecentlyUsedCurrencies(currency); // STEP 4: Add optimistic personal details for participant const shouldCreateOptimisticPersonalDetails = isNewChatReport && !allPersonalDetails[receiverAccountID]; @@ -1950,6 +1969,7 @@ function getSendInvoiceInformation( policy, policyTagList, policyCategories, + optimisticRecentlyUsedCurrencies, companyName, companyWebsite, ); @@ -2076,6 +2096,7 @@ function getMoneyRequestInformation( const optimisticPolicyRecentlyUsedCategories = Category.buildOptimisticPolicyRecentlyUsedCategories(iouReport.policyID, category); const optimisticPolicyRecentlyUsedTags = Tag.buildOptimisticPolicyRecentlyUsedTags(iouReport.policyID, tag); + const optimisticPolicyRecentluUsedCurrencies = Policy.buildOptimisticRecentlyUsedCurrencies(currency); // If there is an existing transaction (which is the case for distance requests), then the data from the existing transaction // needs to be manually merged into the optimistic transaction. This is because buildOnyxDataForMoneyRequest() uses `Onyx.set()` for the transaction @@ -2162,6 +2183,9 @@ function getMoneyRequestInformation( policyTagList, policyCategories, optimisticNextStep, + undefined, + undefined, + optimisticPolicyRecentluUsedCurrencies, ); return { @@ -2667,6 +2691,18 @@ function getUpdateMoneyRequestParams( } } + // Update recently used currencies if the currency is changed + if ('currency' in transactionChanges) { + const optimisticRecentlyUsedCurrencies = Policy.buildOptimisticRecentlyUsedCurrencies(transactionChanges.currency); + if (optimisticRecentlyUsedCurrencies.length) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.RECENTLY_USED_CURRENCIES, + value: optimisticRecentlyUsedCurrencies, + }); + } + } + // Update recently used categories if the tag is changed if ('tag' in transactionChanges) { const optimisticPolicyRecentlyUsedTags = Tag.buildOptimisticPolicyRecentlyUsedTags(iouReport?.policyID, transactionChanges.tag); @@ -4216,6 +4252,8 @@ function createSplitsAndOnyxData( // Add category to optimistic policy recently used categories when a participant is a workspace const optimisticPolicyRecentlyUsedCategories = isPolicyExpenseChat ? Category.buildOptimisticPolicyRecentlyUsedCategories(participant.policyID, category) : []; + const optimisticRecentlyUsedCurrencies = Policy.buildOptimisticRecentlyUsedCurrencies(currency); + // Add tag to optimistic policy recently used tags when a participant is a workspace const optimisticPolicyRecentlyUsedTags = isPolicyExpenseChat ? Tag.buildOptimisticPolicyRecentlyUsedTags(participant.policyID, tag) : {}; @@ -4240,6 +4278,8 @@ function createSplitsAndOnyxData( null, null, true, + undefined, + optimisticRecentlyUsedCurrencies, ); const individualSplit = { @@ -4706,6 +4746,7 @@ function startSplitBill({ const optimisticPolicyRecentlyUsedCategories = Category.buildOptimisticPolicyRecentlyUsedCategories(participant.policyID, category); const optimisticPolicyRecentlyUsedTags = Tag.buildOptimisticPolicyRecentlyUsedTags(participant.policyID, tag); + const optimisticRecentlyUsedCurrencies = Policy.buildOptimisticRecentlyUsedCurrencies(currency); if (optimisticPolicyRecentlyUsedCategories.length > 0) { optimisticData.push({ @@ -4715,6 +4756,14 @@ function startSplitBill({ }); } + if (optimisticRecentlyUsedCurrencies.length > 0) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.RECENTLY_USED_CURRENCIES, + value: optimisticRecentlyUsedCurrencies, + }); + } + if (!isEmptyObject(optimisticPolicyRecentlyUsedTags)) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -5299,6 +5348,18 @@ function editRegularMoneyRequest( } } + // Update recently used currencies if the currency is changed + if ('currency' in transactionChanges) { + const optimisticRecentlyUsedCurrencies = Policy.buildOptimisticRecentlyUsedCurrencies(transactionChanges.currency); + if (optimisticRecentlyUsedCurrencies.length) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.RECENTLY_USED_CURRENCIES, + value: optimisticRecentlyUsedCurrencies, + }); + } + } + // Update recently used categories if the tag is changed if ('tag' in transactionChanges) { const optimisticPolicyRecentlyUsedTags = Tag.buildOptimisticPolicyRecentlyUsedTags(iouReport?.policyID, transactionChanges.tag); diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index cf20401644a7..b6a85aea256a 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1,6 +1,7 @@ import {PUBLIC_DOMAINS, Str} from 'expensify-common'; import {escapeRegExp} from 'lodash'; import lodashClone from 'lodash/clone'; +import lodashUnion from 'lodash/union'; import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; @@ -190,6 +191,12 @@ Onyx.connect({ callback: (val) => (reimbursementAccount = val), }); +let allRecentlyUsedCurrencies: string[]; +Onyx.connect({ + key: ONYXKEYS.RECENTLY_USED_CURRENCIES, + callback: (val) => (allRecentlyUsedCurrencies = val ?? []), +}); + /** * Stores in Onyx the policy ID of the last workspace that was accessed by the user */ @@ -2219,6 +2226,14 @@ function dismissAddedWithPrimaryLoginMessages(policyID: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {primaryLoginsInvited: null}); } +function buildOptimisticRecentlyUsedCurrencies(currency?: string) { + if (!currency) { + return []; + } + + return lodashUnion([currency], allRecentlyUsedCurrencies).slice(0, CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW); +} + /** * This flow is used for bottom up flow converting IOU report to an expense report. When user takes this action, * we create a Collect type workspace when the person taking the action becomes an owner and an admin, while we @@ -4651,6 +4666,7 @@ export { dismissAddedWithPrimaryLoginMessages, openDraftWorkspaceRequest, createDraftInitialWorkspace, + buildOptimisticRecentlyUsedCurrencies, setWorkspaceInviteMessageDraft, setWorkspaceApprovalMode, setWorkspaceAutoReportingFrequency, diff --git a/src/pages/iou/request/step/IOURequestStepCurrency.tsx b/src/pages/iou/request/step/IOURequestStepCurrency.tsx index 74600a5f3650..b51d6a8998a4 100644 --- a/src/pages/iou/request/step/IOURequestStepCurrency.tsx +++ b/src/pages/iou/request/step/IOURequestStepCurrency.tsx @@ -22,6 +22,8 @@ import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNo type IOURequestStepCurrencyOnyxProps = { /** The draft transaction object being modified in Onyx */ draftTransaction: OnyxEntry; + /** List of recently used currencies */ + recentlyUsedCurrencies: OnyxEntry; }; type IOURequestStepCurrencyProps = IOURequestStepCurrencyOnyxProps & WithFullTransactionOrNotFoundProps; @@ -31,6 +33,7 @@ function IOURequestStepCurrency({ params: {backTo, iouType, pageIndex, reportID, transactionID, action, currency: selectedCurrency = ''}, }, draftTransaction, + recentlyUsedCurrencies, }: IOURequestStepCurrencyProps) { const {translate} = useLocalize(); const {currency: originalCurrency = ''} = ReportUtils.getTransactionDetails(draftTransaction) ?? {}; @@ -75,6 +78,7 @@ function IOURequestStepCurrency({ > {({didScreenTransitionEnd}) => ( { if (!didScreenTransitionEnd) { @@ -98,6 +102,9 @@ const IOURequestStepCurrencyWithOnyx = withOnyx