Skip to content

Commit

Permalink
Merge pull request Expensify#41834 from DylanDylann/fix/34874
Browse files Browse the repository at this point in the history
fix implement recently used currencies
  • Loading branch information
thienlnam authored Sep 12, 2024
2 parents 57e1313 + 1f7939c commit ef6a64c
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 14 deletions.
5 changes: 4 additions & 1 deletion src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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_',
Expand Down Expand Up @@ -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;
Expand Down
68 changes: 55 additions & 13 deletions src/components/CurrencySelectionList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
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';
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);
Expand All @@ -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 (
<SelectionList
Expand Down
3 changes: 3 additions & 0 deletions src/components/CurrencySelectionList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ type CurrencySelectionListProps = CurrencySelectionListOnyxProps & {
/** Currency item to be selected initially */
initiallySelectedCurrencyCode?: string;

/** List of recently used currencies */
recentlyUsedCurrencies?: string[];

/** Callback to fire when a currency is selected */
onSelect: (item: CurrencyListItem) => void;

Expand Down
61 changes: 61 additions & 0 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -919,6 +928,7 @@ function buildOnyxDataForInvoice(
policy?: OnyxEntry<OnyxTypes.Policy>,
policyTagList?: OnyxEntry<OnyxTypes.PolicyTagLists>,
policyCategories?: OnyxEntry<OnyxTypes.PolicyCategories>,
optimisticRecentlyUsedCurrencies?: string[],
companyName?: string,
companyWebsite?: string,
): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -1950,6 +1969,7 @@ function getSendInvoiceInformation(
policy,
policyTagList,
policyCategories,
optimisticRecentlyUsedCurrencies,
companyName,
companyWebsite,
);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2162,6 +2183,9 @@ function getMoneyRequestInformation(
policyTagList,
policyCategories,
optimisticNextStep,
undefined,
undefined,
optimisticPolicyRecentluUsedCurrencies,
);

return {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) : {};

Expand All @@ -4240,6 +4278,8 @@ function createSplitsAndOnyxData(
null,
null,
true,
undefined,
optimisticRecentlyUsedCurrencies,
);

const individualSplit = {
Expand Down Expand Up @@ -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({
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
16 changes: 16 additions & 0 deletions src/libs/actions/Policy/Policy.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -4651,6 +4666,7 @@ export {
dismissAddedWithPrimaryLoginMessages,
openDraftWorkspaceRequest,
createDraftInitialWorkspace,
buildOptimisticRecentlyUsedCurrencies,
setWorkspaceInviteMessageDraft,
setWorkspaceApprovalMode,
setWorkspaceAutoReportingFrequency,
Expand Down
7 changes: 7 additions & 0 deletions src/pages/iou/request/step/IOURequestStepCurrency.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNo
type IOURequestStepCurrencyOnyxProps = {
/** The draft transaction object being modified in Onyx */
draftTransaction: OnyxEntry<Transaction>;
/** List of recently used currencies */
recentlyUsedCurrencies: OnyxEntry<string[]>;
};

type IOURequestStepCurrencyProps = IOURequestStepCurrencyOnyxProps & WithFullTransactionOrNotFoundProps<typeof SCREENS.MONEY_REQUEST.STEP_CURRENCY>;
Expand All @@ -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) ?? {};
Expand Down Expand Up @@ -75,6 +78,7 @@ function IOURequestStepCurrency({
>
{({didScreenTransitionEnd}) => (
<CurrencySelectionList
recentlyUsedCurrencies={recentlyUsedCurrencies ?? []}
searchInputLabel={translate('common.search')}
onSelect={(option: CurrencyListItem) => {
if (!didScreenTransitionEnd) {
Expand All @@ -98,6 +102,9 @@ const IOURequestStepCurrencyWithOnyx = withOnyx<IOURequestStepCurrencyProps, IOU
return `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`;
},
},
recentlyUsedCurrencies: {
key: ONYXKEYS.RECENTLY_USED_CURRENCIES,
},
})(IOURequestStepCurrency);

/* eslint-disable rulesdir/no-negated-variables */
Expand Down

0 comments on commit ef6a64c

Please sign in to comment.