diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 10da4ca095a1..df38084fc2a2 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -590,6 +590,9 @@ const ONYXKEYS = { /** Currently displaying feed */ LAST_SELECTED_FEED: 'lastSelectedFeed_', + /** Currently displaying Expensify Card feed */ + LAST_SELECTED_EXPENSIFY_CARD_FEED: 'lastSelectedExpensifyCardFeed_', + /** Whether the bank account chosen for Expensify Card in on verification waitlist */ NVP_EXPENSIFY_ON_CARD_WAITLIST: 'nvp_expensify_onCardWaitlist_', @@ -946,6 +949,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.PolicyConnectionName; [ONYXKEYS.COLLECTION.EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION]: boolean; [ONYXKEYS.COLLECTION.LAST_SELECTED_FEED]: OnyxTypes.CompanyCardFeed; + [ONYXKEYS.COLLECTION.LAST_SELECTED_EXPENSIFY_CARD_FEED]: OnyxTypes.FundID; [ONYXKEYS.COLLECTION.NVP_EXPENSIFY_ON_CARD_WAITLIST]: OnyxTypes.CardOnWaitlist; [ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard; }; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d8cd860f6a27..c5fc1db57e30 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1645,6 +1645,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/expensify-card/settings/account', getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/expensify-card/settings/account`, backTo), }, + WORKSPACE_EXPENSIFY_CARD_SELECT_FEED: { + route: 'settings/workspaces/:policyID/expensify-card/select-feed', + getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/expensify-card/select-feed`, backTo), + }, WORKSPACE_EXPENSIFY_CARD_SETTINGS_FREQUENCY: { route: 'settings/workspaces/:policyID/expensify-card/settings/frequency', getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/settings/frequency` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 093233e52bc2..d9d572fd8411 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -505,6 +505,7 @@ const SCREENS = { EXPENSIFY_CARD_LIMIT: 'Workspace_ExpensifyCard_Limit', EXPENSIFY_CARD_ISSUE_NEW: 'Workspace_ExpensifyCard_New', EXPENSIFY_CARD_NAME: 'Workspace_ExpensifyCard_Name', + EXPENSIFY_CARD_SELECT_FEED: 'Workspace_ExpensifyCard_Select_Feed', EXPENSIFY_CARD_LIMIT_TYPE: 'Workspace_ExpensifyCard_LimitType', EXPENSIFY_CARD_BANK_ACCOUNT: 'Workspace_ExpensifyCard_BankAccount', EXPENSIFY_CARD_SETTINGS: 'Workspace_ExpensifyCard_Settings', diff --git a/src/components/FeedSelector.tsx b/src/components/FeedSelector.tsx new file mode 100644 index 000000000000..0caa235739e0 --- /dev/null +++ b/src/components/FeedSelector.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import {View} from 'react-native'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import type IconAsset from '@src/types/utils/IconAsset'; +import CaretWrapper from './CaretWrapper'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; +import {PressableWithFeedback} from './Pressable'; +import Text from './Text'; + +type Props = { + /** Function to call when the feed is selected */ + onFeedSelect: () => void; + + /** Icon for the card */ + cardIcon: IconAsset; + + /** Whether to show assign card button */ + shouldChangeLayout?: boolean; + + /** Feed name */ + feedName?: string; + + /** Supporting text */ + supportingText?: string; + + /** Whether the RBR indicator should be shown */ + shouldShowRBR?: boolean; +}; + +function FeedSelector({onFeedSelect, cardIcon, shouldChangeLayout, feedName, supportingText, shouldShowRBR = false}: Props) { + const styles = useThemeStyles(); + const theme = useTheme(); + + return ( + + + + + + {feedName} + + {shouldShowRBR && ( + + )} + + {supportingText} + + + ); +} + +export default FeedSelector; diff --git a/src/hooks/useDefaultFundID.ts b/src/hooks/useDefaultFundID.ts new file mode 100644 index 000000000000..1515a473bfa9 --- /dev/null +++ b/src/hooks/useDefaultFundID.ts @@ -0,0 +1,37 @@ +import {useOnyx} from 'react-native-onyx'; +import {getFundIdFromSettingsKey} from '@libs/CardUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import useWorkspaceAccountID from './useWorkspaceAccountID'; + +/** + * Hook to get the default fundID for a given policyID. This is used to get the settings and cards for each of the feeds. + * It will always return lastSelectedExpensifyCardFeed if it exists or fallback to the workspaceAccountID or domainFundID. + */ +function useDefaultFundID(policyID: string | undefined) { + const workspaceAccountID = useWorkspaceAccountID(policyID); + const [lastSelectedExpensifyCardFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_EXPENSIFY_CARD_FEED}${policyID}`); + + const [domainFundID] = useOnyx(ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS, { + selector: (cardSettings) => { + const matchingKey = Object.entries(cardSettings ?? {}).find( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ([key, settings]) => settings?.preferredPolicy && settings.preferredPolicy === policyID && !key.includes(workspaceAccountID.toString()), + ); + + return getFundIdFromSettingsKey(matchingKey?.[0] ?? ''); + }, + }); + + if (lastSelectedExpensifyCardFeed) { + return lastSelectedExpensifyCardFeed; + } + + if (workspaceAccountID) { + return workspaceAccountID; + } + + return domainFundID ?? CONST.DEFAULT_NUMBER_ID; +} + +export default useDefaultFundID; diff --git a/src/hooks/useDomainFundID.ts b/src/hooks/useDomainFundID.ts deleted file mode 100644 index 29fe4fb6b3fa..000000000000 --- a/src/hooks/useDomainFundID.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {useOnyx} from 'react-native-onyx'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; - -function useDomainFundID(policyID: string | undefined) { - const [domainFundID] = useOnyx(ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS, { - selector: (cardSettings) => { - const matchingEntry = Object.entries(cardSettings ?? {}).find( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ([_, settings]) => settings?.preferredPolicy && settings.preferredPolicy === policyID, - ); - - if (!matchingEntry) { - return CONST.DEFAULT_NUMBER_ID; - } - - const key = matchingEntry[0]; - const prefix = ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS; - - if (!key.startsWith(prefix)) { - return CONST.DEFAULT_NUMBER_ID; - } - - const accountIDStr = key.substring(prefix.length); - - const accountID = Number(accountIDStr); - return Number.isNaN(accountID) ? CONST.DEFAULT_NUMBER_ID : accountID; - }, - }); - - return domainFundID; -} - -export default useDomainFundID; diff --git a/src/hooks/useExpensifyCardFeeds.ts b/src/hooks/useExpensifyCardFeeds.ts new file mode 100644 index 000000000000..bc3b113f344a --- /dev/null +++ b/src/hooks/useExpensifyCardFeeds.ts @@ -0,0 +1,19 @@ +import {useOnyx} from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; + +function useExpensifyCardFeeds(policyID: string | undefined) { + const [allExpensifyCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS, { + selector: (cardSettings) => { + const matchingEntries = Object.entries(cardSettings ?? {}).filter( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ([_, settings]) => settings?.preferredPolicy && settings.preferredPolicy === policyID, + ); + + return Object.fromEntries(matchingEntries); + }, + }); + + return allExpensifyCardFeeds; +} + +export default useExpensifyCardFeeds; diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index e5d3d9e9ddb6..faddf805f0f1 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -597,6 +597,17 @@ function isExpensifyCardFullySetUp(policy?: OnyxEntry, cardSettings?: On return !!(policy?.areExpensifyCardsEnabled && cardSettings?.paymentBankAccountID); } +function getFundIdFromSettingsKey(key: string) { + const prefix = ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS; + if (!key?.startsWith(prefix)) { + return CONST.DEFAULT_NUMBER_ID; + } + const fundIDStr = key.substring(prefix.length); + + const fundID = Number(fundIDStr); + return Number.isNaN(fundID) ? CONST.DEFAULT_NUMBER_ID : fundID; +} + export { isExpensifyCard, isCorporateCard, @@ -641,4 +652,5 @@ export { hasCardListObject, isExpensifyCardFullySetUp, filterInactiveCards, + getFundIdFromSettingsKey, }; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 38d332677858..8f6b9948e13e 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -564,6 +564,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/expensifyCard/WorkspaceCardSettingsPage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_ACCOUNT]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceSettlementAccountPage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_FREQUENCY]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage').default, + [SCREENS.WORKSPACE.EXPENSIFY_CARD_SELECT_FEED]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardSelectorPage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_NAME]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceEditCardNamePage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index 433e4dfa923b..c296848e03e5 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -237,6 +237,7 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_ACCOUNT]: { path: ROUTES.WORKSPACE_EXPENSIFY_CARD_SETTINGS_ACCOUNT.route, }, + [SCREENS.WORKSPACE.EXPENSIFY_CARD_SELECT_FEED]: { + path: ROUTES.WORKSPACE_EXPENSIFY_CARD_SELECT_FEED.route, + }, [SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS]: { path: ROUTES.WORKSPACE_COMPANY_CARDS_SETTINGS.route, }, @@ -1283,7 +1286,10 @@ const config: LinkingOptions['config'] = { [SCREENS.MONEY_REQUEST.STEP_WAYPOINT]: ROUTES.MONEY_REQUEST_STEP_WAYPOINT.route, [SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.route, [SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: ROUTES.MONEY_REQUEST_STEP_TAX_RATE.route, - [SCREENS.MONEY_REQUEST.STATE_SELECTOR]: {path: ROUTES.MONEY_REQUEST_STATE_SELECTOR.route, exact: true}, + [SCREENS.MONEY_REQUEST.STATE_SELECTOR]: { + path: ROUTES.MONEY_REQUEST_STATE_SELECTOR.route, + exact: true, + }, [SCREENS.MONEY_REQUEST.STEP_SPLIT_PAYER]: ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.route, [SCREENS.MONEY_REQUEST.STEP_ATTENDEES]: ROUTES.MONEY_REQUEST_ATTENDEE.route, [SCREENS.MONEY_REQUEST.STEP_UPGRADE]: ROUTES.MONEY_REQUEST_UPGRADE.route, diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 4cb585bf6fa0..a674306eb20d 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -334,11 +334,10 @@ function updateSettlementFrequency(workspaceAccountID: number, settlementFrequen API.write(WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_FREQUENCY, parameters, {optimisticData, successData, failureData}); } -function updateSettlementAccount(workspaceAccountID: number, policyID: string, settlementBankAccountID?: number, currentSettlementBankAccountID?: number) { +function updateSettlementAccount(domainName: string, workspaceAccountID: number, policyID: string, settlementBankAccountID?: number, currentSettlementBankAccountID?: number) { if (!settlementBankAccountID) { return; } - const domainName = PolicyUtils.getDomainNameForPolicy(policyID); const optimisticData: OnyxUpdate[] = [ { @@ -390,7 +389,12 @@ function getCardDefaultName(userName?: string) { } function setIssueNewCardStepAndData({data, isEditing, step, policyID}: IssueNewCardFlowData) { - Onyx.merge(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {data, isEditing, currentStep: step, errors: null}); + Onyx.merge(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, { + data, + isEditing, + currentStep: step, + errors: null, + }); } function clearIssueNewCardFlow(policyID: string | undefined) { @@ -918,6 +922,20 @@ function updateSelectedFeed(feed: CompanyCardFeed, policyID: string | undefined) ]); } +function updateSelectedExpensifyCardFeed(feed: number, policyID: string | undefined) { + if (!policyID) { + return; + } + + Onyx.update([ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_EXPENSIFY_CARD_FEED}${policyID}`, + value: feed, + }, + ]); +} + function queueExpensifyCardForBilling(feedCountry: string, domainAccountID: number) { const parameters = { feedCountry, @@ -948,6 +966,7 @@ export { toggleContinuousReconciliation, updateExpensifyCardLimitType, updateSelectedFeed, + updateSelectedExpensifyCardFeed, deactivateCard, getCardDefaultName, queueExpensifyCardForBilling, diff --git a/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx b/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx index acce8ea75bca..42da603378f6 100644 --- a/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx +++ b/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx @@ -5,13 +5,14 @@ import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; +import useDefaultFundID from '@hooks/useDefaultFundID'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWorkspaceAccountID from '@hooks/useWorkspaceAccountID'; import {getConnectionNameFromRouteParam} from '@libs/AccountingUtils'; import {getLastFourDigits} from '@libs/BankAccountUtils'; import {getEligibleBankAccountsForCard} from '@libs/CardUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import {getDomainNameForPolicy} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import {updateSettlementAccount} from '@userActions/Card'; @@ -29,17 +30,19 @@ function ReconciliationAccountSettingsPage({route}: ReconciliationAccountSetting const styles = useThemeStyles(); const {translate} = useLocalize(); - const workspaceAccountID = useWorkspaceAccountID(policyID); const connectionName = getConnectionNameFromRouteParam(connection); + const defaultFundID = useDefaultFundID(policyID); const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); - const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`); + const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`); const paymentBankAccountID = cardSettings?.paymentBankAccountID; const selectedBankAccount = useMemo(() => bankAccountList?.[paymentBankAccountID?.toString() ?? ''], [paymentBankAccountID, bankAccountList]); const bankAccountNumber = useMemo(() => selectedBankAccount?.accountData?.accountNumber ?? '', [selectedBankAccount]); const settlementAccountEnding = getLastFourDigits(bankAccountNumber); + const domainName = cardSettings?.domainName ?? getDomainNameForPolicy(policyID); + const sections = useMemo(() => { if (!bankAccountList || isEmptyObject(bankAccountList)) { return []; @@ -56,7 +59,7 @@ function ReconciliationAccountSettingsPage({route}: ReconciliationAccountSetting }, [bankAccountList, paymentBankAccountID]); const selectBankAccount = (newBankAccountID?: number) => { - updateSettlementAccount(workspaceAccountID, policyID, newBankAccountID, paymentBankAccountID); + updateSettlementAccount(domainName, defaultFundID, policyID, newBankAccountID, paymentBankAccountID); Navigation.goBack(ROUTES.WORKSPACE_ACCOUNTING_CARD_RECONCILIATION.getRoute(policyID, connection)); }; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx index 37786c6f578c..808b2ad67303 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx @@ -2,10 +2,9 @@ import React from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; -import CaretWrapper from '@components/CaretWrapper'; +import FeedSelector from '@components/FeedSelector'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; -import {PressableWithFeedback} from '@components/Pressable'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; @@ -26,7 +25,6 @@ import { isCustomFeed, } from '@libs/CardUtils'; import Navigation from '@navigation/Navigation'; -import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {CompanyCardFeed} from '@src/types/onyx'; @@ -67,33 +65,14 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed, shouldS return ( - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.getRoute(policyID))} - style={[styles.flexRow, styles.alignItemsCenter, styles.gap3, shouldChangeLayout && styles.mb3]} - accessibilityLabel={formattedFeedName ?? ''} - > - - - - - {formattedFeedName} - - {checkIfFeedConnectionIsBroken(flatAllCardsList(allFeedsCards, workspaceAccountID), selectedFeed) && ( - - )} - - {translate(isCommercialFeed ? 'workspace.companyCards.commercialFeed' : 'workspace.companyCards.directFeed')} - - - + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.getRoute(policyID))} + cardIcon={getCardFeedIcon(selectedFeed, illustrations)} + shouldChangeLayout={shouldChangeLayout} + feedName={formattedFeedName} + supportingText={translate(isCommercialFeed ? 'workspace.companyCards.commercialFeed' : 'workspace.companyCards.directFeed')} + shouldShowRBR={checkIfFeedConnectionIsBroken(flatAllCardsList(allFeedsCards, workspaceAccountID), selectedFeed)} + /> {!!shouldShowAssignCardButton && (