From 7c0422a0a3006f09cd98649e9c455b3afec12682 Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Fri, 3 Jul 2026 09:17:30 +0700 Subject: [PATCH 1/6] migrate receipt partner --- src/ROUTES.ts | 34 ++++++------------- src/SCREENS.ts | 4 +-- .../ModalStackNavigators/index.tsx | 5 +-- .../RELATIONS/WORKSPACE_TO_RHP.ts | 4 +-- src/libs/Navigation/linkingConfig/config.ts | 14 ++++---- src/libs/Navigation/types.ts | 8 ++--- ...micEditInviteReceiptPartnerPolicyPage.tsx} | 15 ++++---- ...DynamicInviteReceiptPartnerPolicyPage.tsx} | 19 ++++++----- .../WorkspaceReceiptPartnersPage.tsx | 11 ++++-- 9 files changed, 52 insertions(+), 62 deletions(-) rename src/pages/workspace/receiptPartners/{EditInviteReceiptPartnerPolicyPage.tsx => DynamicEditInviteReceiptPartnerPolicyPage.tsx} (95%) rename src/pages/workspace/receiptPartners/{InviteReceiptPartnerPolicyPage.tsx => DynamicInviteReceiptPartnerPolicyPage.tsx} (91%) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0cf673e5799c..14d3bf476d1f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -649,6 +649,16 @@ const DYNAMIC_ROUTES = { SCREENS.WORKSPACE.DYNAMIC_WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW, ], }, + WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT: { + path: ':integration/invite/edit', + entryScreens: [SCREENS.WORKSPACE.RECEIPT_PARTNERS], + getRoute: (integration: string) => `${integration}/invite/edit` as const, + }, + WORKSPACE_RECEIPT_PARTNERS_INVITE: { + path: ':integration/invite', + entryScreens: [SCREENS.WORKSPACE.RECEIPT_PARTNERS], + getRoute: (integration: string) => `${integration}/invite` as const, + }, EXPENSIFY_CARD_DETAILS: { path: 'expensify-card-details/:cardID/:policyID', entryScreens: [SCREENS.WORKSPACE.EXPENSIFY_CARD, SCREENS.REPORT, SCREENS.RIGHT_MODAL.SEARCH_REPORT, SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, SCREENS.DYNAMIC_PROFILE], @@ -3051,34 +3061,10 @@ const ROUTES = { return getUrlWithBackToParam(`workspaces/${policyID}/receipt-partners`, backTo); }, }, - WORKSPACE_RECEIPT_PARTNERS_INVITE: { - route: 'workspaces/:policyID/receipt-partners/:integration/invite', - getRoute: (policyID: string | undefined, integration: string, backTo?: string) => { - if (!policyID) { - Log.warn('Invalid policyID is used to build the WORKSPACE_RECEIPT_PARTNERS_INVITE route'); - } - - return getUrlWithBackToParam(`workspaces/${policyID}/receipt-partners/${integration}/invite`, backTo); - }, - }, WORKSPACE_RECEIPT_PARTNERS_CHANGE_BILLING_ACCOUNT: { route: 'workspaces/:policyID/receipt-partners/:integration/billing-account', getRoute: (policyID: string, integration: string) => `workspaces/${policyID}/receipt-partners/${integration}/billing-account` as const, }, - WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT: { - route: 'workspaces/:policyID/receipt-partners/:integration/invite/edit', - getRoute: (policyID: string | undefined, integration: string, backTo?: string) => { - if (!policyID) { - Log.warn('Invalid policyID is used to build the WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT route'); - } - - return getUrlWithBackToParam(`workspaces/${policyID}/receipt-partners/${integration}/invite/edit`, backTo); - }, - }, - WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT_ALL: 'workspaces/:policyID/receipt-partners/:integration/invite/edit/ReceiptPartnersAllTab', - WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT_LINKED: 'workspaces/:policyID/receipt-partners/:integration/invite/edit/ReceiptPartnersLinkedTab', - WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT_OUTSTANDING: 'workspaces/:policyID/receipt-partners/:integration/invite/edit/ReceiptPartnersOutstandingTab', - WORKSPACE_PER_DIEM_IMPORT: { route: 'workspaces/:policyID/per-diem/import', getRoute: (policyID: string) => `workspaces/${policyID}/per-diem/import` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index a2b2a00a8960..3ce5ab7adeb0 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -844,8 +844,8 @@ const SCREENS = { DOWNGRADE: 'Workspace_Downgrade', PAY_AND_DOWNGRADE: 'Workspace_Pay_And_Downgrade', RECEIPT_PARTNERS: 'Workspace_Receipt_Partners', - RECEIPT_PARTNERS_INVITE: 'Workspace_Receipt_Partners_Invite', - RECEIPT_PARTNERS_INVITE_EDIT: 'Workspace_Receipt_Partners_Invite_Edit', + DYNAMIC_RECEIPT_PARTNERS_INVITE: 'Dynamic_Workspace_Receipt_Partners_Invite', + DYNAMIC_RECEIPT_PARTNERS_INVITE_EDIT: 'Dynamic_Workspace_Receipt_Partners_Invite_Edit', RECEIPT_PARTNERS_CHANGE_BILLING_ACCOUNT: 'Workspace_Receipt_Partners_Change_Billing_Account', RULES: 'Policy_Rules', REPORTS_DEFAULT_TITLE: 'Reports_Default_Title', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 7a511ef19603..a167713f784d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -1120,8 +1120,9 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/perDiem/EditPerDiemSubratePage').default, [SCREENS.WORKSPACE.PER_DIEM_EDIT_AMOUNT]: () => require('../../../../pages/workspace/perDiem/EditPerDiemAmountPage').default, [SCREENS.WORKSPACE.PER_DIEM_EDIT_CURRENCY]: () => require('../../../../pages/workspace/perDiem/EditPerDiemCurrencyPage').default, - [SCREENS.WORKSPACE.RECEIPT_PARTNERS_INVITE]: () => require('../../../../pages/workspace/receiptPartners/InviteReceiptPartnerPolicyPage').default, - [SCREENS.WORKSPACE.RECEIPT_PARTNERS_INVITE_EDIT]: () => require('../../../../pages/workspace/receiptPartners/EditInviteReceiptPartnerPolicyPage').default, + [SCREENS.WORKSPACE.DYNAMIC_RECEIPT_PARTNERS_INVITE]: () => require('../../../../pages/workspace/receiptPartners/DynamicInviteReceiptPartnerPolicyPage').default, + [SCREENS.WORKSPACE.DYNAMIC_RECEIPT_PARTNERS_INVITE_EDIT]: () => + require('../../../../pages/workspace/receiptPartners/DynamicEditInviteReceiptPartnerPolicyPage').default, [SCREENS.WORKSPACE.RECEIPT_PARTNERS_CHANGE_BILLING_ACCOUNT]: () => require('../../../../pages/workspace/receiptPartners/ChangeReceiptBillingAccountPage').default, [SCREENS.WORKSPACE.TIME_TRACKING_DEFAULT_RATE]: () => require('../../../../pages/workspace/timeTracking/WorkspaceTimeTrackingDefaultRatePage').default, [SCREENS.DOMAIN.SAML_VERIFY]: () => require('../../../../pages/domain/VerifyDomainPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index 65ecc2e057ae..ff30e1c74a7f 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -402,8 +402,8 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.INVITE_MESSAGE_APPROVER]: { path: ROUTES.WORKSPACE_INVITE_MESSAGE_APPROVER.route, }, - [SCREENS.WORKSPACE.RECEIPT_PARTNERS_INVITE]: { - path: ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE.route, - }, + [SCREENS.WORKSPACE.DYNAMIC_RECEIPT_PARTNERS_INVITE]: DYNAMIC_ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE.path, [SCREENS.WORKSPACE.RECEIPT_PARTNERS_CHANGE_BILLING_ACCOUNT]: { path: ROUTES.WORKSPACE_RECEIPT_PARTNERS_CHANGE_BILLING_ACCOUNT.route, }, - [SCREENS.WORKSPACE.RECEIPT_PARTNERS_INVITE_EDIT]: { - path: ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT.route, + [SCREENS.WORKSPACE.DYNAMIC_RECEIPT_PARTNERS_INVITE_EDIT]: { + path: DYNAMIC_ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT.path, screens: { [CONST.TAB.RECEIPT_PARTNERS.ALL]: { - path: ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT_ALL, + path: CONST.TAB.RECEIPT_PARTNERS.ALL, exact: true, }, [CONST.TAB.RECEIPT_PARTNERS.LINKED]: { - path: ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT_LINKED, + path: CONST.TAB.RECEIPT_PARTNERS.LINKED, exact: true, }, [CONST.TAB.RECEIPT_PARTNERS.OUTSTANDING]: { - path: ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT_OUTSTANDING, + path: CONST.TAB.RECEIPT_PARTNERS.OUTSTANDING, exact: true, }, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 792699d60d8d..654c07d8835f 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2782,22 +2782,18 @@ type WorkspaceSplitNavigatorParamList = { [SCREENS.WORKSPACE.RECEIPT_PARTNERS]: { policyID: string; }; - [SCREENS.WORKSPACE.RECEIPT_PARTNERS_INVITE]: { + [SCREENS.WORKSPACE.DYNAMIC_RECEIPT_PARTNERS_INVITE]: { policyID: string; integration: string; - // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md - backTo?: Routes; }; [SCREENS.WORKSPACE.RECEIPT_PARTNERS_CHANGE_BILLING_ACCOUNT]: { policyID: string; integration: ValueOf; }; - [SCREENS.WORKSPACE.RECEIPT_PARTNERS_INVITE_EDIT]: { + [SCREENS.WORKSPACE.DYNAMIC_RECEIPT_PARTNERS_INVITE_EDIT]: { policyID: string; integration: string; - // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md - backTo?: Routes; }; [SCREENS.WORKSPACE.DYNAMIC_WORKSPACE_COMPANY_CARDS_ADD_NEW]: { policyID: string; diff --git a/src/pages/workspace/receiptPartners/EditInviteReceiptPartnerPolicyPage.tsx b/src/pages/workspace/receiptPartners/DynamicEditInviteReceiptPartnerPolicyPage.tsx similarity index 95% rename from src/pages/workspace/receiptPartners/EditInviteReceiptPartnerPolicyPage.tsx rename to src/pages/workspace/receiptPartners/DynamicEditInviteReceiptPartnerPolicyPage.tsx index f8573cfbfdc1..b9f803c5c1dd 100644 --- a/src/pages/workspace/receiptPartners/EditInviteReceiptPartnerPolicyPage.tsx +++ b/src/pages/workspace/receiptPartners/DynamicEditInviteReceiptPartnerPolicyPage.tsx @@ -10,6 +10,7 @@ import BareUserListItem from '@components/SelectionList/ListItem/BareUserListIte import type {ListItem} from '@components/SelectionList/types'; import TabSelector from '@components/TabSelector/TabSelector'; import useDebouncedState from '@hooks/useDebouncedState'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -32,21 +33,23 @@ import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; -type EditInviteReceiptPartnerPolicyPageProps = PlatformStackScreenProps; +type DynamicEditInviteReceiptPartnerPolicyPageProps = PlatformStackScreenProps; const TAB_NAMES = [CONST.TAB.RECEIPT_PARTNERS.ALL, CONST.TAB.RECEIPT_PARTNERS.LINKED, CONST.TAB.RECEIPT_PARTNERS.OUTSTANDING] as const; type ReceiptPartnersTab = TupleToUnion; type UberEmployeeStatus = ValueOf; -function EditInviteReceiptPartnerPolicyPage({route}: EditInviteReceiptPartnerPolicyPageProps) { +function DynamicEditInviteReceiptPartnerPolicyPage({route}: DynamicEditInviteReceiptPartnerPolicyPageProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const icons = useMemoizedLazyExpensifyIcons(['Checkmark', 'FallbackAvatar']); const illustrations = useMemoizedLazyIllustrations(['SewerDino']); const {translate, localeCompare} = useLocalize(); const {isOffline} = useNetwork(); + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT.path); const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); const policyID = route.params.policyID; const policy = usePolicy(policyID); @@ -270,12 +273,10 @@ function EditInviteReceiptPartnerPolicyPage({route}: EditInviteReceiptPartnerPol policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED} > - + { - Navigation.dismissModal(); - }} + onBackButtonPress={() => Navigation.goBack(backPath)} /> ; +type DynamicInviteReceiptPartnerPolicyPageProps = PlatformStackScreenProps; -function InviteReceiptPartnerPolicyPage({route}: InviteReceiptPartnerPolicyPageProps) { +function DynamicInviteReceiptPartnerPolicyPage({route}: DynamicInviteReceiptPartnerPolicyPageProps) { const styles = useThemeStyles(); const illustrations = useMemoizedLazyIllustrations(['ToddInCar']); const {translate, localeCompare} = useLocalize(); const {isOffline} = useNetwork(); + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE.path); const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const [selectedOptions, setSelectedOptions] = useState([]); const [isInvitationSent, setIsInvitationSent] = useState(false); @@ -177,10 +180,10 @@ function InviteReceiptPartnerPolicyPage({route}: InviteReceiptPartnerPolicyPageP if (isInvitationSent || shouldSkipToAllSet) { return ( - + Navigation.dismissModal()} + onBackButtonPress={() => Navigation.goBack(backPath)} /> Navigation.dismissModal()} + onButtonPress={() => Navigation.goBack(backPath)} descriptionStyle={styles.colorMuted} /> @@ -202,10 +205,10 @@ function InviteReceiptPartnerPolicyPage({route}: InviteReceiptPartnerPolicyPageP policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED} > - + Navigation.goBack()} + onBackButtonPress={() => Navigation.goBack(backPath)} /> { @@ -382,7 +383,11 @@ function WorkspaceReceiptPartnersPage({route}: WorkspaceReceiptPartnersPageProps shouldShowRightIcon icon={icons.Mail} style={[styles.sectionMenuItemTopDescription, styles.mbn3, !centralBillingAccountEmail && styles.mt6]} - onPress={() => Navigation.navigate(ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT.getRoute(policyID, CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER))} + onPress={() => + Navigation.navigate( + createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT.getRoute(CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER)), + ) + } /> )} From 5c265e2abe7210149dccad5d24e93eabd88d7b92 Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Fri, 3 Jul 2026 10:03:24 +0700 Subject: [PATCH 2/6] migrate expensify card details --- src/ROUTES.ts | 16 ++++++++-------- src/SCREENS.ts | 2 +- .../ModalStackNavigators/index.tsx | 5 +++-- .../RELATIONS/WORKSPACE_TO_RHP.ts | 2 +- src/libs/Navigation/linkingConfig/config.ts | 4 +--- src/libs/Navigation/types.ts | 4 +--- ...namicWorkspaceExpensifyCardDetailsPage.tsx} | 18 ++++++++++-------- .../WorkspaceExpensifyCardListPage.tsx | 4 ++-- .../members/WorkspaceMemberDetailsPage.tsx | 2 +- 9 files changed, 28 insertions(+), 29 deletions(-) rename src/pages/workspace/expensifyCard/{WorkspaceExpensifyCardDetailsPage.tsx => DynamicWorkspaceExpensifyCardDetailsPage.tsx} (96%) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 14d3bf476d1f..666c2f8a4d09 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -666,15 +666,20 @@ const DYNAMIC_ROUTES = { }, EXPENSIFY_CARD_LIMIT_TYPE: { path: 'edit/limit-type', - entryScreens: [SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS, SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_DETAILS], + entryScreens: [SCREENS.WORKSPACE.DYNAMIC_EXPENSIFY_CARD_DETAILS, SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_DETAILS], }, EXPENSIFY_CARD_LIMIT: { path: 'edit/limit', - entryScreens: [SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS, SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_DETAILS], + entryScreens: [SCREENS.WORKSPACE.DYNAMIC_EXPENSIFY_CARD_DETAILS, SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_DETAILS], }, EXPENSIFY_CARD_NAME: { path: 'edit/name', - entryScreens: [SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS, SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_DETAILS], + entryScreens: [SCREENS.WORKSPACE.DYNAMIC_EXPENSIFY_CARD_DETAILS, SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_DETAILS], + }, + WORKSPACE_EXPENSIFY_CARD_DETAILS: { + path: 'card-details/:cardID', + entryScreens: [SCREENS.WORKSPACE.EXPENSIFY_CARD, SCREENS.WORKSPACE.MEMBER_DETAILS], + getRoute: (cardID: string) => `card-details/${cardID}` as const, }, WORKSPACE_EXPENSIFY_CARD_SETTINGS_ACCOUNT: { path: 'account', @@ -2877,11 +2882,6 @@ const ROUTES = { return `workspaces/${policyID}/expensify-card` as const; }, }, - WORKSPACE_EXPENSIFY_CARD_DETAILS: { - route: 'workspaces/:policyID/expensify-card/:cardID', - - getRoute: (policyID: string, cardID: string, backTo?: string) => getUrlWithBackToParam(`workspaces/${policyID}/expensify-card/${cardID}`, backTo), - }, WORKSPACE_EXPENSIFY_CARD_BANK_ACCOUNT: { route: 'workspaces/:policyID/expensify-card/choose-bank-account', getRoute: (policyID: string | undefined) => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 3ce5ab7adeb0..8efe9017a283 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -708,7 +708,7 @@ const SCREENS = { COMPANY_CARD_EDIT_TRANSACTION_START_DATE: 'Workspace_CompanyCard_Edit_Transaction_Start_Date', DYNAMIC_COMPANY_CARD_EXPORT: 'Dynamic_Workspace_CompanyCard_Export', EXPENSIFY_CARD: 'Workspace_ExpensifyCard', - EXPENSIFY_CARD_DETAILS: 'Workspace_ExpensifyCard_Details', + DYNAMIC_EXPENSIFY_CARD_DETAILS: 'Dynamic_Workspace_ExpensifyCard_Details', EXPENSIFY_CARD_ADD_WORK_EMAIL: 'Workspace_ExpensifyCard_Add_Work_Email', DYNAMIC_WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: 'Dynamic_Workspace_ExpensifyCard_New', DYNAMIC_WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW_CONFIRM_MAGIC_CODE: 'Dynamic_Workspace_ExpensifyCard_IssueNew_ConfirmMagicCode', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index a167713f784d..1c9f2c282286 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -343,7 +343,8 @@ const TagsModalStackNavigator = createModalStackNavigator({ }); const ExpensifyCardModalStackNavigator = createModalStackNavigator({ - [SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_DETAILS]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage').default, + [SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_DETAILS]: () => + require('../../../../pages/workspace/expensifyCard/DynamicWorkspaceExpensifyCardDetailsPage').default, [SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_NAME]: () => require('../../../../pages/workspace/expensifyCard/DynamicExpensifyCardNamePage').default, [SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_LIMIT]: () => require('../../../../pages/workspace/expensifyCard/DynamicExpensifyCardLimitPage').default, [SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_LIMIT_TYPE]: () => require('../../../../pages/workspace/expensifyCard/DynamicExpensifyCardLimitTypePage').default, @@ -981,7 +982,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardVerifyWorkAccountPage').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.DYNAMIC_EXPENSIFY_CARD_DETAILS]: () => require('../../../../pages/workspace/expensifyCard/DynamicWorkspaceExpensifyCardDetailsPage').default, [SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage').default, [SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS_FEED_NAME]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardsSettingsFeedNamePage').default, [SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS_STATEMENT_CLOSE_DATE]: () => diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index ff30e1c74a7f..c1459fc16f64 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -342,7 +342,7 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS_STATEMENT_CLOSE_DATE]: { path: ROUTES.WORKSPACE_COMPANY_CARDS_SETTINGS_STATEMENT_CLOSE_DATE.route, }, - [SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS]: { - path: ROUTES.WORKSPACE_EXPENSIFY_CARD_DETAILS.route, - }, + [SCREENS.WORKSPACE.DYNAMIC_EXPENSIFY_CARD_DETAILS]: DYNAMIC_ROUTES.WORKSPACE_EXPENSIFY_CARD_DETAILS.path, [SCREENS.WORKSPACE.DYNAMIC_WORKSPACE_COMPANY_CARDS_ADD_NEW]: DYNAMIC_ROUTES.WORKSPACE_COMPANY_CARDS_ADD_NEW.path, [SCREENS.WORKSPACE.COMPANY_CARDS_IMPORT_SPREADSHEET]: { path: ROUTES.WORKSPACE_COMPANY_CARDS_IMPORT_SPREADSHEET.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 654c07d8835f..f6ecf12049a3 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1433,11 +1433,9 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS_STATEMENT_CLOSE_DATE]: { policyID: string; }; - [SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS]: { + [SCREENS.WORKSPACE.DYNAMIC_EXPENSIFY_CARD_DETAILS]: { policyID: string; cardID: string; - // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md - backTo?: Routes; }; [SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_DETAILS]: { policyID: string; diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx b/src/pages/workspace/expensifyCard/DynamicWorkspaceExpensifyCardDetailsPage.tsx similarity index 96% rename from src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx rename to src/pages/workspace/expensifyCard/DynamicWorkspaceExpensifyCardDetailsPage.tsx index 044e3ce8c753..506602c271aa 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx +++ b/src/pages/workspace/expensifyCard/DynamicWorkspaceExpensifyCardDetailsPage.tsx @@ -52,9 +52,9 @@ import {DYNAMIC_ROUTES} from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; -type WorkspaceExpensifyCardDetailsPageProps = PlatformStackScreenProps< +type DynamicWorkspaceExpensifyCardDetailsPageProps = PlatformStackScreenProps< SettingsNavigatorParamList, - typeof SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS | typeof SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_DETAILS + typeof SCREENS.WORKSPACE.DYNAMIC_EXPENSIFY_CARD_DETAILS | typeof SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_DETAILS >; type LimitHintTranslationKey = 'cardPage.smartLimit.title' | 'cardPage.monthlyLimit.title' | 'cardPage.fixedLimit.title'; @@ -72,11 +72,13 @@ function getLimitHintTranslationKey(limitType?: string): LimitHintTranslationKey } } -function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetailsPageProps) { +function DynamicWorkspaceExpensifyCardDetailsPage({route}: DynamicWorkspaceExpensifyCardDetailsPageProps) { const navigation = useNavigation>(); - const {policyID, cardID, backTo} = route.params; - const backPath = useDynamicBackPath(DYNAMIC_ROUTES.EXPENSIFY_CARD_DETAILS.path); + const {policyID, cardID} = route.params; + const quickSettingsBackPath = useDynamicBackPath(DYNAMIC_ROUTES.EXPENSIFY_CARD_DETAILS.path); + const workspaceBackPath = useDynamicBackPath(DYNAMIC_ROUTES.WORKSPACE_EXPENSIFY_CARD_DETAILS.path); const isQuickSettingsFlow = route.name === SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_DETAILS; + const backPath = isQuickSettingsFlow ? quickSettingsBackPath : workspaceBackPath; const {convertToDisplayString} = useCurrencyListActions(); const defaultFundID = useDefaultFundID(policyID); @@ -270,11 +272,11 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail > Navigation.goBack(isQuickSettingsFlow ? backPath : backTo)} + onBackButtonPress={() => Navigation.goBack(backPath)} /> {canManageCardFreeze && isCardFrozen(card) ? ( @@ -425,4 +427,4 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail ); } -export default WorkspaceExpensifyCardDetailsPage; +export default DynamicWorkspaceExpensifyCardDetailsPage; diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx index af70f969a116..4de1fa45f414 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx @@ -138,11 +138,11 @@ function WorkspaceExpensifyCardListPage({route, cardsList, fundID}: WorkspaceExp frozenDate: card.nameValuePairs?.frozen?.date, errors: card.errors, pendingAction: card.pendingAction, - action: () => Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_DETAILS.getRoute(policyID, card.cardID.toString())), + action: () => Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_EXPENSIFY_CARD_DETAILS.getRoute(card.cardID.toString()))), onClose: () => clearDeletePaymentMethodError(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${defaultFundID}_${CONST.EXPENSIFY_CARD.BANK}`, card.cardID), }; }), - [allCards, defaultFundID, personalDetails, policyID, settlementCurrency], + [allCards, defaultFundID, personalDetails, settlementCurrency], ); const bulkExportOptions: Array> = [ diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 763934ea1d61..13cab9c4bcec 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -244,7 +244,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const navigateToDetails = (card: MemberCard) => { if (card.bank === CONST.EXPENSIFY_CARD.BANK) { - Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_DETAILS.getRoute(policyID, card.cardID.toString(), Navigation.getActiveRoute())); + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_EXPENSIFY_CARD_DETAILS.getRoute(card.cardID.toString()))); return; } if (!card.fundID) { From ead7c0db480c515e5ee8bb90536bf89380216da1 Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Fri, 3 Jul 2026 10:51:58 +0700 Subject: [PATCH 3/6] update oldRoutes --- .../Navigation/linkingConfig/OldRoutes.ts | 9 +++++ tests/navigation/getMatchingNewRouteTest.ts | 34 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/libs/Navigation/linkingConfig/OldRoutes.ts b/src/libs/Navigation/linkingConfig/OldRoutes.ts index 772e999469ce..b567d7a6deeb 100644 --- a/src/libs/Navigation/linkingConfig/OldRoutes.ts +++ b/src/libs/Navigation/linkingConfig/OldRoutes.ts @@ -80,6 +80,15 @@ const oldRoutes: Record = { '/workspaces/*/company-cards/*/work-email': '/workspaces/$1/company-cards/$2/work-email', '/workspaces/*/company-cards/*/verify-work-email': '/workspaces/$1/company-cards/$2/verify-work-email', '/workspaces/*/company-cards/*/*': '/workspaces/$1/company-cards/company-card-details/$2/$3', + '/workspaces/*/expensify-card/card-details*': '/workspaces/$1/expensify-card/card-details$2', + '/workspaces/*/expensify-card/expensify-card-details*': '/workspaces/$1/expensify-card/expensify-card-details$2', + '/workspaces/*/expensify-card/settings*': '/workspaces/$1/expensify-card/settings$2', + '/workspaces/*/expensify-card/choose-bank-account*': '/workspaces/$1/expensify-card/choose-bank-account$2', + '/workspaces/*/expensify-card/select-feed*': '/workspaces/$1/expensify-card/select-feed$2', + '/workspaces/*/expensify-card/issue-new*': '/workspaces/$1/expensify-card/issue-new$2', + '/workspaces/*/expensify-card/*/work-email': '/workspaces/$1/expensify-card/$2/work-email', + '/workspaces/*/expensify-card/*/verify-work-email': '/workspaces/$1/expensify-card/$2/verify-work-email', + '/workspaces/*/expensify-card/*': '/workspaces/$1/expensify-card/card-details/$2', '/workspace/confirmation/currency': '/workspaces/workspace/confirmation/currency', '/workspace/confirmation': '/workspaces/workspace/confirmation', '/settings/wallet/enable-payments/plaid*': '/settings/wallet/enable-payments/add-bank-account/plaid$1', diff --git a/tests/navigation/getMatchingNewRouteTest.ts b/tests/navigation/getMatchingNewRouteTest.ts index 69cfde194f5d..88fb1689056a 100644 --- a/tests/navigation/getMatchingNewRouteTest.ts +++ b/tests/navigation/getMatchingNewRouteTest.ts @@ -313,4 +313,38 @@ describe('getBestMatchingPath', () => { getMatchingNewRoute('/workspaces/D56D50B841F69B0E/company-cards/company-card-details/assign-card/oauth.mockbank.com%2322298108/Mock%20Credit%20Card%20-%201234/assignee'), ).toBe('/workspaces/D56D50B841F69B0E/company-cards/assign-card/oauth.mockbank.com%2322298108/Mock%20Credit%20Card%20-%201234/assignee'); }); + + it('redirects legacy Expensify Card details paths to the new card-details dynamic route', () => { + expect(getMatchingNewRoute('/workspaces/p123/expensify-card/456')).toBe('/workspaces/p123/expensify-card/card-details/456'); + expect(getMatchingNewRoute('/workspaces/p123/expensify-card/456?backTo=/home')).toBe('/workspaces/p123/expensify-card/card-details/456?backTo=/home'); + }); + + it('redirects legacy Expensify Card details nested edit paths to the new card-details dynamic route', () => { + expect(getMatchingNewRoute('/workspaces/p123/expensify-card/456/edit/limit-type')).toBe('/workspaces/p123/expensify-card/card-details/456/edit/limit-type'); + expect(getMatchingNewRoute('/workspaces/p123/expensify-card/456/edit/limit')).toBe('/workspaces/p123/expensify-card/card-details/456/edit/limit'); + expect(getMatchingNewRoute('/workspaces/p123/expensify-card/456/edit/name')).toBe('/workspaces/p123/expensify-card/card-details/456/edit/name'); + }); + + it('does not rewrite Expensify Card keyword sibling routes that share the single-segment shape', () => { + expect(getMatchingNewRoute('/workspaces/p123/expensify-card/settings')).toBe('/workspaces/p123/expensify-card/settings'); + expect(getMatchingNewRoute('/workspaces/p123/expensify-card/choose-bank-account')).toBe('/workspaces/p123/expensify-card/choose-bank-account'); + expect(getMatchingNewRoute('/workspaces/p123/expensify-card/select-feed')).toBe('/workspaces/p123/expensify-card/select-feed'); + expect(getMatchingNewRoute('/workspaces/p123/expensify-card/issue-new')).toBe('/workspaces/p123/expensify-card/issue-new'); + }); + + it('does not rewrite Expensify Card sibling subtrees', () => { + expect(getMatchingNewRoute('/workspaces/p123/expensify-card/settings/frequency')).toBe('/workspaces/p123/expensify-card/settings/frequency'); + expect(getMatchingNewRoute('/workspaces/p123/expensify-card/settings/account')).toBe('/workspaces/p123/expensify-card/settings/account'); + expect(getMatchingNewRoute('/workspaces/p123/expensify-card/issue-new/rules/max-amount')).toBe('/workspaces/p123/expensify-card/issue-new/rules/max-amount'); + }); + + it('does not rewrite Expensify Card numeric fundID work-email paths', () => { + expect(getMatchingNewRoute('/workspaces/p123/expensify-card/789/work-email')).toBe('/workspaces/p123/expensify-card/789/work-email'); + expect(getMatchingNewRoute('/workspaces/p123/expensify-card/789/verify-work-email')).toBe('/workspaces/p123/expensify-card/789/verify-work-email'); + }); + + it('does not rewrite the already-migrated Expensify Card details paths', () => { + expect(getMatchingNewRoute('/workspaces/p123/expensify-card/card-details/456')).toBe('/workspaces/p123/expensify-card/card-details/456'); + expect(getMatchingNewRoute('/workspaces/p123/expensify-card/card-details/456/edit/limit')).toBe('/workspaces/p123/expensify-card/card-details/456/edit/limit'); + }); }); From 5c2789e7f7f2fca8cc56640601226d0f73c922a1 Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:35:30 +0700 Subject: [PATCH 4/6] update fmt --- ...namicWorkspaceExpensifyCardDetailsPage.tsx | 1010 +++++++---------- .../WorkspaceExpensifyCardListPage.tsx | 808 ++++++------- ...amicEditInviteReceiptPartnerPolicyPage.tsx | 825 ++++++-------- .../DynamicInviteReceiptPartnerPolicyPage.tsx | 513 ++++----- .../WorkspaceReceiptPartnersPage.tsx | 909 +++++++-------- 5 files changed, 1726 insertions(+), 2339 deletions(-) diff --git a/src/pages/workspace/expensifyCard/DynamicWorkspaceExpensifyCardDetailsPage.tsx b/src/pages/workspace/expensifyCard/DynamicWorkspaceExpensifyCardDetailsPage.tsx index 4c46d23adafd..79cbd3fe8a35 100644 --- a/src/pages/workspace/expensifyCard/DynamicWorkspaceExpensifyCardDetailsPage.tsx +++ b/src/pages/workspace/expensifyCard/DynamicWorkspaceExpensifyCardDetailsPage.tsx @@ -1,593 +1,451 @@ -import cardScarf from "@assets/images/card-scarf.svg"; - -import Badge from "@components/Badge"; -import DecisionModal from "@components/DecisionModal"; -import FrozenCardHeader from "@components/FrozenCardHeader"; -import HeaderWithBackButton from "@components/HeaderWithBackButton"; -import ImageSVG from "@components/ImageSVG"; -import MenuItem from "@components/MenuItem"; -import MenuItemWithTopDescription from "@components/MenuItemWithTopDescription"; -import { ModalActions } from "@components/Modal/Global/ModalContext"; -import OfflineWithFeedback from "@components/OfflineWithFeedback"; -import ScreenWrapper from "@components/ScreenWrapper"; -import ScrollView from "@components/ScrollView"; -import Text from "@components/Text"; - -import useCardFeeds from "@hooks/useCardFeeds"; -import useConfirmModal from "@hooks/useConfirmModal"; -import useCurrencyForExpensifyCard from "@hooks/useCurrencyForExpensifyCard"; -import { useCurrencyListActions } from "@hooks/useCurrencyList"; -import useCurrentUserPersonalDetails from "@hooks/useCurrentUserPersonalDetails"; -import useDefaultFundID from "@hooks/useDefaultFundID"; -import useDynamicBackPath from "@hooks/useDynamicBackPath"; -import useExpensifyCardFeeds from "@hooks/useExpensifyCardFeeds"; -import { - useMemoizedLazyExpensifyIcons, - useMemoizedLazyIllustrations, -} from "@hooks/useLazyAsset"; -import useLocalize from "@hooks/useLocalize"; -import useNetwork from "@hooks/useNetwork"; -import useOnyx from "@hooks/useOnyx"; -import useResponsiveLayout from "@hooks/useResponsiveLayout"; -import useStyleUtils from "@hooks/useStyleUtils"; -import useThemeStyles from "@hooks/useThemeStyles"; - -import { openPolicyExpensifyCardsPage } from "@libs/actions/Policy/Policy"; -import navigateToCardTransactions from "@libs/CardNavigationUtils"; -import { - getAllCardsForWorkspace, - getCardFeedTextColor, - getCardHintText, - getTranslationKeyForLimitType, - isCardFrozen, - maskCard, -} from "@libs/CardUtils"; -import createDynamicRoute from "@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute"; -import type { PlatformStackScreenProps } from "@libs/Navigation/PlatformStackNavigation/types"; -import type { SettingsNavigatorParamList } from "@libs/Navigation/types"; -import { temporaryGetDisplayNameOrDefault } from "@libs/PersonalDetailsUtils"; -import { arePolicyRulesEnabled, canMemberWrite } from "@libs/PolicyUtils"; -import { - getSpendRuleByCardID, - getSpendRuleSummaryText, -} from "@libs/SpendRulesUtils"; - -import Navigation from "@navigation/Navigation"; - -import NotFoundPage from "@pages/ErrorPage/NotFoundPage"; -import CardDetailsActionButtons, { - CardDetailsActionButton, -} from "@pages/settings/Wallet/CardDetailsActionButtons"; -import AccessOrNotFoundWrapper from "@pages/workspace/AccessOrNotFoundWrapper"; - -import variables from "@styles/variables"; - -import { - deactivateCard as deactivateCardAction, - freezeCard as freezeCardAction, - openCardDetailsPage, - unfreezeCard as unfreezeCardAction, -} from "@userActions/Card"; - -import CONST from "@src/CONST"; -import ONYXKEYS from "@src/ONYXKEYS"; -import { DYNAMIC_ROUTES } from "@src/ROUTES"; -import SCREENS from "@src/SCREENS"; -import isLoadingOnyxValue from "@src/types/utils/isLoadingOnyxValue"; -import type { NavigationProp } from "@react-navigation/native"; - -import { useNavigation } from "@react-navigation/native"; -import { cardByIdSelector } from "@selectors/Card"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { View } from "react-native"; +import cardScarf from '@assets/images/card-scarf.svg'; + +import Badge from '@components/Badge'; +import DecisionModal from '@components/DecisionModal'; +import FrozenCardHeader from '@components/FrozenCardHeader'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ImageSVG from '@components/ImageSVG'; +import MenuItem from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; + +import useCardFeeds from '@hooks/useCardFeeds'; +import useConfirmModal from '@hooks/useConfirmModal'; +import useCurrencyForExpensifyCard from '@hooks/useCurrencyForExpensifyCard'; +import {useCurrencyListActions} from '@hooks/useCurrencyList'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDefaultFundID from '@hooks/useDefaultFundID'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; +import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; +import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import {openPolicyExpensifyCardsPage} from '@libs/actions/Policy/Policy'; +import navigateToCardTransactions from '@libs/CardNavigationUtils'; +import {getAllCardsForWorkspace, getCardFeedTextColor, getCardHintText, getTranslationKeyForLimitType, isCardFrozen, maskCard} from '@libs/CardUtils'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {temporaryGetDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; +import {arePolicyRulesEnabled, canMemberWrite} from '@libs/PolicyUtils'; +import {getSpendRuleByCardID, getSpendRuleSummaryText} from '@libs/SpendRulesUtils'; + +import Navigation from '@navigation/Navigation'; + +import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; +import CardDetailsActionButtons, {CardDetailsActionButton} from '@pages/settings/Wallet/CardDetailsActionButtons'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; + +import variables from '@styles/variables'; + +import {deactivateCard as deactivateCardAction, freezeCard as freezeCardAction, openCardDetailsPage, unfreezeCard as unfreezeCardAction} from '@userActions/Card'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; + +import type {NavigationProp} from '@react-navigation/native'; + +import {useNavigation} from '@react-navigation/native'; +import {cardByIdSelector} from '@selectors/Card'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import {View} from 'react-native'; type DynamicWorkspaceExpensifyCardDetailsPageProps = PlatformStackScreenProps< - SettingsNavigatorParamList, - | typeof SCREENS.WORKSPACE.DYNAMIC_EXPENSIFY_CARD_DETAILS - | typeof SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_DETAILS + SettingsNavigatorParamList, + typeof SCREENS.WORKSPACE.DYNAMIC_EXPENSIFY_CARD_DETAILS | typeof SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_DETAILS >; -type LimitHintTranslationKey = - | "cardPage.smartLimit.title" - | "cardPage.monthlyLimit.title" - | "cardPage.fixedLimit.title"; - -function getLimitHintTranslationKey( - limitType?: string, -): LimitHintTranslationKey | undefined { - switch (limitType) { - case CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART: - return "cardPage.smartLimit.title"; - case CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY: - return "cardPage.monthlyLimit.title"; - case CONST.EXPENSIFY_CARD.LIMIT_TYPES.FIXED: - return "cardPage.fixedLimit.title"; - default: - return undefined; - } -} - -function DynamicWorkspaceExpensifyCardDetailsPage({ - route, -}: DynamicWorkspaceExpensifyCardDetailsPageProps) { - const navigation = - useNavigation>(); - const { policyID, cardID } = route.params; - const quickSettingsBackPath = useDynamicBackPath( - DYNAMIC_ROUTES.EXPENSIFY_CARD_DETAILS.path, - ); - const workspaceBackPath = useDynamicBackPath( - DYNAMIC_ROUTES.WORKSPACE_EXPENSIFY_CARD_DETAILS.path, - ); - const isQuickSettingsFlow = - route.name === SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_DETAILS; - const backPath = isQuickSettingsFlow - ? quickSettingsBackPath - : workspaceBackPath; - const { convertToDisplayString } = useCurrencyListActions(); - const defaultFundID = useDefaultFundID(policyID); - - const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); - const { translate } = useLocalize(); - const { login: currentUserLogin = "" } = useCurrentUserPersonalDetails(); - const expensifyIcons = useMemoizedLazyExpensifyIcons([ - "FreezeCard", - "MoneySearch", - "Trashcan", - "CreditCardLock", - ]); - const illustrations = useMemoizedLazyIllustrations(["ExpensifyCardImage"]); - // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use the correct modal type for the decision modal - // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const { isSmallScreenWidth } = useResponsiveLayout(); - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const { showConfirmModal } = useConfirmModal(); - - const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - const [session] = useOnyx(ONYXKEYS.SESSION); - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); - const [policyCategories] = useOnyx( - `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - ); - const [cardFromCardList] = useOnyx(ONYXKEYS.CARD_LIST, { - selector: cardByIdSelector(cardID), - }); - const [cardFeeds] = useCardFeeds(policyID); - const expensifyCardSettings = useExpensifyCardFeeds(policyID); - const [fundCardSettings] = useOnyx( - `${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`, - ); - const [allFeedsCards, allFeedsCardsResult] = useOnyx( - ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, - ); - const workspaceCards = getAllCardsForWorkspace( - defaultFundID, - allFeedsCards, - cardFeeds, - expensifyCardSettings, - /* includeDeactivated */ true, - ); - - const workspaceCard = workspaceCards?.[cardID]; - const card = workspaceCard ?? cardFromCardList; - const currency = useCurrencyForExpensifyCard({ - policyID, - fundID: defaultFundID, - }); - const cardholder = - personalDetails?.[card?.accountID ?? CONST.DEFAULT_NUMBER_ID]; - const isVirtual = !!card?.nameValuePairs?.isVirtual; - const formattedAvailableSpendAmount = convertToDisplayString( - card?.availableSpend, - currency, - ); - const formattedLimit = convertToDisplayString( - card?.nameValuePairs?.unapprovedExpenseLimit, - currency, - ); - const displayName = temporaryGetDisplayNameOrDefault({ - passedPersonalDetails: cardholder, - translate, - }); - const translationForLimitType = getTranslationKeyForLimitType( - card?.nameValuePairs?.limitType, - ); - const remainingLimitHintTranslationKey = getLimitHintTranslationKey( - card?.nameValuePairs?.limitType, - ); - const remainingLimitHint = remainingLimitHintTranslationKey - ? translate(remainingLimitHintTranslationKey, formattedAvailableSpendAmount) - : undefined; - const canWriteExpensifyCard = canMemberWrite( - policy, - currentUserLogin, - CONST.POLICY.POLICY_FEATURE.EXPENSIFY_CARD, - ); - const canEditSpendRules = - canWriteExpensifyCard && arePolicyRulesEnabled(policy, policyCategories); - const isDeactivated = - card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_DEACTIVATED; - const isCardOpen = card?.state === CONST.EXPENSIFY_CARD.STATE.OPEN; - - const fetchCardDetails = useCallback(() => { - openCardDetailsPage(Number(cardID)); - }, [cardID]); - - const { isOffline } = useNetwork({ onReconnect: fetchCardDetails }); - useEffect(() => fetchCardDetails(), [fetchCardDetails]); - - useEffect(() => { - if (!canWriteExpensifyCard) { - return; - } - if (!defaultFundID || defaultFundID === CONST.DEFAULT_NUMBER_ID) { - return; - } - if (fundCardSettings?.isLoading) { - return; - } - if (fundCardSettings?.hasOnceLoaded) { - return; +type LimitHintTranslationKey = 'cardPage.smartLimit.title' | 'cardPage.monthlyLimit.title' | 'cardPage.fixedLimit.title'; + +function getLimitHintTranslationKey(limitType?: string): LimitHintTranslationKey | undefined { + switch (limitType) { + case CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART: + return 'cardPage.smartLimit.title'; + case CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY: + return 'cardPage.monthlyLimit.title'; + case CONST.EXPENSIFY_CARD.LIMIT_TYPES.FIXED: + return 'cardPage.fixedLimit.title'; + default: + return undefined; } +} - openPolicyExpensifyCardsPage(policyID, defaultFundID); - }, [ - canWriteExpensifyCard, - defaultFundID, - fundCardSettings?.hasOnceLoaded, - fundCardSettings?.isLoading, - policyID, - ]); - - const deactivateCard = () => { - showConfirmModal({ - title: translate("workspace.card.deactivateCardModal.deactivateCard"), - shouldSetModalVisibility: false, - prompt: translate( - "workspace.card.deactivateCardModal.deactivateConfirmation", - ), - confirmText: translate("workspace.card.deactivateCardModal.deactivate"), - cancelText: translate("common.cancel"), - danger: true, - }).then(({ action }) => { - if (action !== ModalActions.CONFIRM) { - return; - } - - Navigation.goBack(undefined, { - afterTransition: () => deactivateCardAction(defaultFundID, card), - }); +function DynamicWorkspaceExpensifyCardDetailsPage({route}: DynamicWorkspaceExpensifyCardDetailsPageProps) { + const navigation = useNavigation>(); + const {policyID, cardID} = route.params; + const quickSettingsBackPath = useDynamicBackPath(DYNAMIC_ROUTES.EXPENSIFY_CARD_DETAILS.path); + const workspaceBackPath = useDynamicBackPath(DYNAMIC_ROUTES.WORKSPACE_EXPENSIFY_CARD_DETAILS.path); + const isQuickSettingsFlow = route.name === SCREENS.EXPENSIFY_CARD.DYNAMIC_EXPENSIFY_CARD_DETAILS; + const backPath = isQuickSettingsFlow ? quickSettingsBackPath : workspaceBackPath; + const {convertToDisplayString} = useCurrencyListActions(); + const defaultFundID = useDefaultFundID(policyID); + + const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); + const {translate} = useLocalize(); + const {login: currentUserLogin = ''} = useCurrentUserPersonalDetails(); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['FreezeCard', 'MoneySearch', 'Trashcan', 'CreditCardLock']); + const illustrations = useMemoizedLazyIllustrations(['ExpensifyCardImage']); + // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use the correct modal type for the decision modal + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {isSmallScreenWidth} = useResponsiveLayout(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {showConfirmModal} = useConfirmModal(); + + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); + const [session] = useOnyx(ONYXKEYS.SESSION); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + const [cardFromCardList] = useOnyx(ONYXKEYS.CARD_LIST, { + selector: cardByIdSelector(cardID), }); - }; - - const handleFreezePress = () => { - showConfirmModal({ - title: `${translate("cardPage.freezeCard")}?`, - shouldSetModalVisibility: false, - prompt: translate("cardPage.freezeDescription"), - confirmText: translate("cardPage.freezeCard"), - cancelText: translate("common.cancel"), - danger: true, - }).then((result) => { - if (result.action !== ModalActions.CONFIRM || !card) { - return; - } - freezeCardAction( - Number(card?.fundID ?? defaultFundID ?? CONST.DEFAULT_NUMBER_ID), - card, - session?.accountID ?? CONST.DEFAULT_NUMBER_ID, - ); + const [cardFeeds] = useCardFeeds(policyID); + const expensifyCardSettings = useExpensifyCardFeeds(policyID); + const [fundCardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`); + const [allFeedsCards, allFeedsCardsResult] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST); + const workspaceCards = getAllCardsForWorkspace(defaultFundID, allFeedsCards, cardFeeds, expensifyCardSettings, /* includeDeactivated */ true); + + const workspaceCard = workspaceCards?.[cardID]; + const card = workspaceCard ?? cardFromCardList; + const currency = useCurrencyForExpensifyCard({ + policyID, + fundID: defaultFundID, }); - }; - - const handleUnfreezePress = () => { - showConfirmModal({ - title: `${translate("cardPage.unfreezeCard")}?`, - shouldSetModalVisibility: false, - prompt: translate("cardPage.unfreezeDescription"), - confirmText: translate("cardPage.unfreezeCard"), - cancelText: translate("common.cancel"), - }).then((result) => { - if (result.action !== ModalActions.CONFIRM || !card) { - return; - } - unfreezeCardAction( - Number(card?.fundID ?? defaultFundID ?? CONST.DEFAULT_NUMBER_ID), - card, - session?.accountID ?? CONST.DEFAULT_NUMBER_ID, - ); + const cardholder = personalDetails?.[card?.accountID ?? CONST.DEFAULT_NUMBER_ID]; + const isVirtual = !!card?.nameValuePairs?.isVirtual; + const formattedAvailableSpendAmount = convertToDisplayString(card?.availableSpend, currency); + const formattedLimit = convertToDisplayString(card?.nameValuePairs?.unapprovedExpenseLimit, currency); + const displayName = temporaryGetDisplayNameOrDefault({ + passedPersonalDetails: cardholder, + translate, }); - }; + const translationForLimitType = getTranslationKeyForLimitType(card?.nameValuePairs?.limitType); + const remainingLimitHintTranslationKey = getLimitHintTranslationKey(card?.nameValuePairs?.limitType); + const remainingLimitHint = remainingLimitHintTranslationKey ? translate(remainingLimitHintTranslationKey, formattedAvailableSpendAmount) : undefined; + const canWriteExpensifyCard = canMemberWrite(policy, currentUserLogin, CONST.POLICY.POLICY_FEATURE.EXPENSIFY_CARD); + const canEditSpendRules = canWriteExpensifyCard && arePolicyRulesEnabled(policy, policyCategories); + const isDeactivated = card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_DEACTIVATED; + const isCardOpen = card?.state === CONST.EXPENSIFY_CARD.STATE.OPEN; + + const fetchCardDetails = useCallback(() => { + openCardDetailsPage(Number(cardID)); + }, [cardID]); + + const {isOffline} = useNetwork({onReconnect: fetchCardDetails}); + useEffect(() => fetchCardDetails(), [fetchCardDetails]); + + useEffect(() => { + if (!canWriteExpensifyCard) { + return; + } + if (!defaultFundID || defaultFundID === CONST.DEFAULT_NUMBER_ID) { + return; + } + if (fundCardSettings?.isLoading) { + return; + } + if (fundCardSettings?.hasOnceLoaded) { + return; + } + + openPolicyExpensifyCardsPage(policyID, defaultFundID); + }, [canWriteExpensifyCard, defaultFundID, fundCardSettings?.hasOnceLoaded, fundCardSettings?.isLoading, policyID]); + + const deactivateCard = () => { + showConfirmModal({ + title: translate('workspace.card.deactivateCardModal.deactivateCard'), + shouldSetModalVisibility: false, + prompt: translate('workspace.card.deactivateCardModal.deactivateConfirmation'), + confirmText: translate('workspace.card.deactivateCardModal.deactivate'), + cancelText: translate('common.cancel'), + danger: true, + }).then(({action}) => { + if (action !== ModalActions.CONFIRM) { + return; + } - const spendRule = getSpendRuleByCardID(expensifyCardSettings, cardID); - const spendRulesSummary = spendRule - ? getSpendRuleSummaryText( - spendRule.formValues, - currency, - translate, - convertToDisplayString, - ) - : []; - - const navigateToSpendRules = () => { - if (!spendRule) { - navigation.navigate(SCREENS.WORKSPACE.RULES_SPEND_NEW, { policyID }); - return; + Navigation.goBack(undefined, { + afterTransition: () => deactivateCardAction(defaultFundID, card), + }); + }); + }; + + const handleFreezePress = () => { + showConfirmModal({ + title: `${translate('cardPage.freezeCard')}?`, + shouldSetModalVisibility: false, + prompt: translate('cardPage.freezeDescription'), + confirmText: translate('cardPage.freezeCard'), + cancelText: translate('common.cancel'), + danger: true, + }).then((result) => { + if (result.action !== ModalActions.CONFIRM || !card) { + return; + } + freezeCardAction(Number(card?.fundID ?? defaultFundID ?? CONST.DEFAULT_NUMBER_ID), card, session?.accountID ?? CONST.DEFAULT_NUMBER_ID); + }); + }; + + const handleUnfreezePress = () => { + showConfirmModal({ + title: `${translate('cardPage.unfreezeCard')}?`, + shouldSetModalVisibility: false, + prompt: translate('cardPage.unfreezeDescription'), + confirmText: translate('cardPage.unfreezeCard'), + cancelText: translate('common.cancel'), + }).then((result) => { + if (result.action !== ModalActions.CONFIRM || !card) { + return; + } + unfreezeCardAction(Number(card?.fundID ?? defaultFundID ?? CONST.DEFAULT_NUMBER_ID), card, session?.accountID ?? CONST.DEFAULT_NUMBER_ID); + }); + }; + + const spendRule = getSpendRuleByCardID(expensifyCardSettings, cardID); + const spendRulesSummary = spendRule ? getSpendRuleSummaryText(spendRule.formValues, currency, translate, convertToDisplayString) : []; + + const navigateToSpendRules = () => { + if (!spendRule) { + navigation.navigate(SCREENS.WORKSPACE.RULES_SPEND_NEW, {policyID}); + return; + } + + navigation.navigate(SCREENS.WORKSPACE.RULES_SPEND_EDIT, { + policyID, + ruleID: spendRule.ruleID, + }); + }; + + const navigateToTransactions = () => navigateToCardTransactions(cardID); + + const spendRulesTitleComponent = useMemo( + () => ( + + {spendRulesSummary.map((summary) => ( + + {summary} + + ))} + + ), + [spendRulesSummary], + ); + + const canManageCardFreeze = canWriteExpensifyCard && !!card; + const scarfOverlayStyle = useMemo( + () => ({ + top: 0, + left: (variables.cardPreviewWidth - variables.cardScarfOverlayWidth) / 2, + zIndex: variables.cardScarfOverlayZIndex, + width: variables.cardScarfOverlayWidth, + height: variables.cardScarfOverlayHeight, + }), + [], + ); + const workspaceCardImage = ( + <> + + + {displayName} + + + + ); + if (!card && !isLoadingOnyxValue(allFeedsCardsResult)) { + return ; } - navigation.navigate(SCREENS.WORKSPACE.RULES_SPEND_EDIT, { - policyID, - ruleID: spendRule.ruleID, - }); - }; - - const navigateToTransactions = () => navigateToCardTransactions(cardID); - - const spendRulesTitleComponent = useMemo( - () => ( - - {spendRulesSummary.map((summary) => ( - - {summary} - - ))} - - ), - [spendRulesSummary], - ); - - const canManageCardFreeze = canWriteExpensifyCard && !!card; - const scarfOverlayStyle = useMemo( - () => ({ - top: 0, - left: (variables.cardPreviewWidth - variables.cardScarfOverlayWidth) / 2, - zIndex: variables.cardScarfOverlayZIndex, - width: variables.cardScarfOverlayWidth, - height: variables.cardScarfOverlayHeight, - }), - [], - ); - const workspaceCardImage = ( - <> - - - {displayName} - - - - ); - if (!card && !isLoadingOnyxValue(allFeedsCardsResult)) { - return ; - } - - return ( - - - Navigation.goBack(backPath)} - /> - - {canManageCardFreeze && isCardFrozen(card) ? ( - - {workspaceCardImage} - - - - - } - canUnfreezeCard={canManageCardFreeze} - onAskToUnfreezePress={() => {}} + return ( + + - - - ) : ( - - {workspaceCardImage} - - )} - {(!isCardFrozen(card) || !canManageCardFreeze) && ( - - {canManageCardFreeze && isCardOpen && !isCardFrozen(card) && ( - Navigation.goBack(backPath)} /> - )} - - - )} - - - - Navigation.navigate( - createDynamicRoute(DYNAMIC_ROUTES.EXPENSIFY_CARD_NAME.path), - ) - } - interactive={canWriteExpensifyCard} - /> - - - {spendRulesSummary.length > 0 && ( - - )} - - - - - - Navigation.navigate( - createDynamicRoute( - DYNAMIC_ROUTES.EXPENSIFY_CARD_LIMIT_TYPE.path, - ), - ) - } - interactive={canWriteExpensifyCard} - hintText={getCardHintText( - card?.nameValuePairs?.validFrom, - card?.nameValuePairs?.validThru, - cardholder?.timezone?.selected, - translate, - )} - /> - - - - Navigation.navigate( - createDynamicRoute(DYNAMIC_ROUTES.EXPENSIFY_CARD_LIMIT.path), - ) - } - interactive={canWriteExpensifyCard} - /> - - - - {canEditSpendRules && ( - - )} - {canWriteExpensifyCard && !isDeactivated && ( - - isOffline ? setIsOfflineModalVisible(true) : deactivateCard() - } - /> - )} - - setIsOfflineModalVisible(false)} - secondOptionText={translate("common.buttonConfirm")} - isVisible={isOfflineModalVisible} - onClose={() => setIsOfflineModalVisible(false)} - /> - - - - ); + + {canManageCardFreeze && isCardFrozen(card) ? ( + + {workspaceCardImage} + + + + + } + canUnfreezeCard={canManageCardFreeze} + onAskToUnfreezePress={() => {}} + > + + + ) : ( + {workspaceCardImage} + )} + {(!isCardFrozen(card) || !canManageCardFreeze) && ( + + {canManageCardFreeze && isCardOpen && !isCardFrozen(card) && ( + + )} + + + )} + + + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.EXPENSIFY_CARD_NAME.path))} + interactive={canWriteExpensifyCard} + /> + + + {spendRulesSummary.length > 0 && ( + + )} + + + + + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.EXPENSIFY_CARD_LIMIT_TYPE.path))} + interactive={canWriteExpensifyCard} + hintText={getCardHintText(card?.nameValuePairs?.validFrom, card?.nameValuePairs?.validThru, cardholder?.timezone?.selected, translate)} + /> + + + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.EXPENSIFY_CARD_LIMIT.path))} + interactive={canWriteExpensifyCard} + /> + + + + {canEditSpendRules && ( + + )} + {canWriteExpensifyCard && !isDeactivated && ( + (isOffline ? setIsOfflineModalVisible(true) : deactivateCard())} + /> + )} + + setIsOfflineModalVisible(false)} + secondOptionText={translate('common.buttonConfirm')} + isVisible={isOfflineModalVisible} + onClose={() => setIsOfflineModalVisible(false)} + /> + + + + ); } export default DynamicWorkspaceExpensifyCardDetailsPage; diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx index f1cdae721a13..0600d9f80eda 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx @@ -1,524 +1,360 @@ -import Button from "@components/Button"; -import ButtonWithDropdownMenu from "@components/ButtonWithDropdownMenu"; -import type { DropdownOption } from "@components/ButtonWithDropdownMenu/types"; -import CardFeedIcon from "@components/CardFeedIcon"; -import { - useDelegateNoAccessActions, - useDelegateNoAccessState, -} from "@components/DelegateNoAccessModalProvider"; -import FeedSelector from "@components/FeedSelector"; -import HeaderWithBackButton from "@components/HeaderWithBackButton"; -import { - useLockedAccountActions, - useLockedAccountState, -} from "@components/LockedAccountModalProvider"; -import ScreenWrapper from "@components/ScreenWrapper"; -import type { WorkspaceExpensifyCardTableRowData } from "@components/Tables/WorkspaceExpensifyCardsTable"; -import WorkspaceExpensifyCardsTable from "@components/Tables/WorkspaceExpensifyCardsTable"; -import Text from "@components/Text"; +import Button from '@components/Button'; +import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; +import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import CardFeedIcon from '@components/CardFeedIcon'; +import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; +import FeedSelector from '@components/FeedSelector'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import {useLockedAccountActions, useLockedAccountState} from '@components/LockedAccountModalProvider'; +import ScreenWrapper from '@components/ScreenWrapper'; +import type {WorkspaceExpensifyCardTableRowData} from '@components/Tables/WorkspaceExpensifyCardsTable'; +import WorkspaceExpensifyCardsTable from '@components/Tables/WorkspaceExpensifyCardsTable'; +import Text from '@components/Text'; -import useAndroidBackButtonHandler from "@hooks/useAndroidBackButtonHandler"; -import useCleanupSelectedOptions from "@hooks/useCleanupSelectedOptions"; -import useCurrencyForExpensifyCard from "@hooks/useCurrencyForExpensifyCard"; -import useDefaultFundID from "@hooks/useDefaultFundID"; -import useEmptyViewHeaderHeight from "@hooks/useEmptyViewHeaderHeight"; -import useExpensifyCardFeedsForFeedSelector from "@hooks/useExpensifyCardFeedsForFeedSelector"; -import { - useMemoizedLazyExpensifyIcons, - useMemoizedLazyIllustrations, -} from "@hooks/useLazyAsset"; -import useLocalize from "@hooks/useLocalize"; -import useMobileSelectionMode from "@hooks/useMobileSelectionMode"; -import useOnyx from "@hooks/useOnyx"; -import usePolicy from "@hooks/usePolicy"; -import usePolicyFeatureWriteAccess from "@hooks/usePolicyFeatureWriteAccess"; -import useResponsiveLayout from "@hooks/useResponsiveLayout"; -import useShouldDisplayButtonsInSeparateLine from "@hooks/useShouldDisplayButtonsInSeparateLine"; -import useThemeStyles from "@hooks/useThemeStyles"; -import useWindowDimensions from "@hooks/useWindowDimensions"; +import useAndroidBackButtonHandler from '@hooks/useAndroidBackButtonHandler'; +import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions'; +import useCurrencyForExpensifyCard from '@hooks/useCurrencyForExpensifyCard'; +import useDefaultFundID from '@hooks/useDefaultFundID'; +import useEmptyViewHeaderHeight from '@hooks/useEmptyViewHeaderHeight'; +import useExpensifyCardFeedsForFeedSelector from '@hooks/useExpensifyCardFeedsForFeedSelector'; +import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; +import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; +import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useShouldDisplayButtonsInSeparateLine from '@hooks/useShouldDisplayButtonsInSeparateLine'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; -import { - clearIssueNewCardFormData, - exportExpensifyCardListToCSV, - setIssueNewCardStepAndData, -} from "@libs/actions/Card"; -import { turnOffMobileSelectionMode } from "@libs/actions/MobileSelectionMode"; -import { clearDeletePaymentMethodError } from "@libs/actions/PaymentMethods"; -import { - getCardsByCardholderName, - getCardSettings, - isCurrencySupportedForECards, -} from "@libs/CardUtils"; -import { getExpensifyCardFeedDescription } from "@libs/ExpensifyCardFeedSelectorUtils"; -import createDynamicRoute from "@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute"; -import type { PlatformStackRouteProp } from "@libs/Navigation/PlatformStackNavigation/types"; -import { temporaryGetDisplayNameOrDefault } from "@libs/PersonalDetailsUtils"; -import { getMemberAccountIDsForWorkspace } from "@libs/PolicyUtils"; +import {clearIssueNewCardFormData, exportExpensifyCardListToCSV, setIssueNewCardStepAndData} from '@libs/actions/Card'; +import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; +import {clearDeletePaymentMethodError} from '@libs/actions/PaymentMethods'; +import {getCardsByCardholderName, getCardSettings, isCurrencySupportedForECards} from '@libs/CardUtils'; +import {getExpensifyCardFeedDescription} from '@libs/ExpensifyCardFeedSelectorUtils'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; +import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; +import {temporaryGetDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; +import {getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils'; -import Navigation from "@navigation/Navigation"; -import type { WorkspaceSplitNavigatorParamList } from "@navigation/types"; +import Navigation from '@navigation/Navigation'; +import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; -import variables from "@styles/variables"; +import variables from '@styles/variables'; -import CONST from "@src/CONST"; -import ONYXKEYS from "@src/ONYXKEYS"; -import ROUTES, { DYNAMIC_ROUTES } from "@src/ROUTES"; -import type SCREENS from "@src/SCREENS"; -import type { WorkspaceCardsList } from "@src/types/onyx"; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {WorkspaceCardsList} from '@src/types/onyx'; -import type { OnyxEntry } from "react-native-onyx"; +import type {OnyxEntry} from 'react-native-onyx'; -import React, { useCallback, useMemo, useState } from "react"; -import { View } from "react-native"; +import React, {useCallback, useMemo, useState} from 'react'; +import {View} from 'react-native'; -import EmptyCardView from "./EmptyCardView"; +import EmptyCardView from './EmptyCardView'; type WorkspaceExpensifyCardListPageProps = { - /** Route from navigation */ - route: PlatformStackRouteProp< - WorkspaceSplitNavigatorParamList, - typeof SCREENS.WORKSPACE.EXPENSIFY_CARD - >; + /** Route from navigation */ + route: PlatformStackRouteProp; - /** List of Expensify cards */ - cardsList: OnyxEntry; + /** List of Expensify cards */ + cardsList: OnyxEntry; - /** Fund ID */ - fundID: number; + /** Fund ID */ + fundID: number; }; -function WorkspaceExpensifyCardListPage({ - route, - cardsList, - fundID, -}: WorkspaceExpensifyCardListPageProps) { - const icons = useMemoizedLazyExpensifyIcons(["Export", "Gear", "Plus"]); - const { shouldUseNarrowLayout, isMediumScreenWidth, isInLandscapeMode } = - useResponsiveLayout(); - const { translate } = useLocalize(); - const styles = useThemeStyles(); - const illustrations = useMemoizedLazyIllustrations([ - "HandCard", - "ExpensifyCardImage", - ]); - const isMobileSelectionModeEnabled = useMobileSelectionMode(); - const policyID = route.params.policyID; - const policy = usePolicy(policyID); - const defaultFundID = useDefaultFundID(policyID); - const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - const [cardOnWaitlist] = useOnyx( - `${ONYXKEYS.COLLECTION.NVP_EXPENSIFY_ON_CARD_WAITLIST}${policyID}`, - ); - const [cardSettings] = useOnyx( - `${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${fundID}`, - ); - const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); - const [domains] = useOnyx(ONYXKEYS.COLLECTION.DOMAIN); - const [cardList] = useOnyx(ONYXKEYS.CARD_LIST); - const settings = getCardSettings(cardSettings); - const { allFeeds: allAdminExpensifyCardFeeds } = - useExpensifyCardFeedsForFeedSelector(policyID); - const shouldShowSelector = allAdminExpensifyCardFeeds.length >= 1; - const { isDelegateAccessRestricted } = useDelegateNoAccessState(); - const { showDelegateNoAccessModal } = useDelegateNoAccessActions(); - const { isAccountLocked } = useLockedAccountState(); - const { showLockedAccountModal } = useLockedAccountActions(); - const isBankAccountVerified = !cardOnWaitlist; - const shouldChangeLayout = isMediumScreenWidth || shouldUseNarrowLayout; - const { windowHeight } = useWindowDimensions(); - const shouldDisplayButtonsInSeparateLine = - useShouldDisplayButtonsInSeparateLine(); - const { canWrite: canWriteExpensifyCard, showReadOnlyModal } = - usePolicyFeatureWriteAccess( - policy, - CONST.POLICY.POLICY_FEATURE.EXPENSIFY_CARD, - ); - const headerHeight = useEmptyViewHeaderHeight( - shouldDisplayButtonsInSeparateLine, - isBankAccountVerified, - ); - const [footerHeight, setFooterHeight] = useState(0); - const cardFeedIcon = ( - - ); - - const settlementCurrency = useCurrencyForExpensifyCard({ policyID, fundID }); - const shouldShowEuUkDisclaimer = - isCurrencySupportedForECards(settlementCurrency); - const allCards = useMemo(() => { - const policyMembersAccountIDs = Object.values( - getMemberAccountIDsForWorkspace(policy?.employeeList), +function WorkspaceExpensifyCardListPage({route, cardsList, fundID}: WorkspaceExpensifyCardListPageProps) { + const icons = useMemoizedLazyExpensifyIcons(['Export', 'Gear', 'Plus']); + const {shouldUseNarrowLayout, isMediumScreenWidth, isInLandscapeMode} = useResponsiveLayout(); + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const illustrations = useMemoizedLazyIllustrations(['HandCard', 'ExpensifyCardImage']); + const isMobileSelectionModeEnabled = useMobileSelectionMode(); + const policyID = route.params.policyID; + const policy = usePolicy(policyID); + const defaultFundID = useDefaultFundID(policyID); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); + const [cardOnWaitlist] = useOnyx(`${ONYXKEYS.COLLECTION.NVP_EXPENSIFY_ON_CARD_WAITLIST}${policyID}`); + const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${fundID}`); + const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); + const [domains] = useOnyx(ONYXKEYS.COLLECTION.DOMAIN); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST); + const settings = getCardSettings(cardSettings); + const {allFeeds: allAdminExpensifyCardFeeds} = useExpensifyCardFeedsForFeedSelector(policyID); + const shouldShowSelector = allAdminExpensifyCardFeeds.length >= 1; + const {isDelegateAccessRestricted} = useDelegateNoAccessState(); + const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); + const {isAccountLocked} = useLockedAccountState(); + const {showLockedAccountModal} = useLockedAccountActions(); + const isBankAccountVerified = !cardOnWaitlist; + const shouldChangeLayout = isMediumScreenWidth || shouldUseNarrowLayout; + const {windowHeight} = useWindowDimensions(); + const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine(); + const {canWrite: canWriteExpensifyCard, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.EXPENSIFY_CARD); + const headerHeight = useEmptyViewHeaderHeight(shouldDisplayButtonsInSeparateLine, isBankAccountVerified); + const [footerHeight, setFooterHeight] = useState(0); + const cardFeedIcon = ( + ); - return getCardsByCardholderName(cardsList, policyMembersAccountIDs); - }, [cardsList, policy?.employeeList]); - const isCardListEmpty = allCards.length === 0; - const [selectedCardKeys, setSelectedCardKeys] = useState([]); - const selectableCardKeySet = useMemo( - () => new Set(allCards.map((card) => String(card.cardID))), - [allCards], - ); - const validatedSelectedCardKeys = useMemo( - () => selectedCardKeys.filter((key) => selectableCardKeySet.has(key)), - [selectedCardKeys, selectableCardKeySet], - ); - const selectedCardIDs = useMemo( - () => validatedSelectedCardKeys.map((key) => Number(key)), - [validatedSelectedCardKeys], - ); + const settlementCurrency = useCurrencyForExpensifyCard({policyID, fundID}); + const shouldShowEuUkDisclaimer = isCurrencySupportedForECards(settlementCurrency); + const allCards = useMemo(() => { + const policyMembersAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList)); + return getCardsByCardholderName(cardsList, policyMembersAccountIDs); + }, [cardsList, policy?.employeeList]); - const clearTableSelection = useCallback(() => { - setSelectedCardKeys((prevSelectedCardKeys) => - prevSelectedCardKeys.length > 0 ? [] : prevSelectedCardKeys, - ); - }, []); - - useCleanupSelectedOptions(clearTableSelection); + const isCardListEmpty = allCards.length === 0; + const [selectedCardKeys, setSelectedCardKeys] = useState([]); + const selectableCardKeySet = useMemo(() => new Set(allCards.map((card) => String(card.cardID))), [allCards]); + const validatedSelectedCardKeys = useMemo(() => selectedCardKeys.filter((key) => selectableCardKeySet.has(key)), [selectedCardKeys, selectableCardKeySet]); + const selectedCardIDs = useMemo(() => validatedSelectedCardKeys.map((key) => Number(key)), [validatedSelectedCardKeys]); - const cardRows = useMemo( - () => - allCards.map((card) => { - const frozenByDisplayName = card.nameValuePairs?.frozen?.byAccountID - ? temporaryGetDisplayNameOrDefault({ - passedPersonalDetails: - personalDetails?.[card.nameValuePairs.frozen.byAccountID], - defaultValue: "", - shouldFallbackToHidden: false, - translate, - }) || undefined - : undefined; + const clearTableSelection = useCallback(() => { + setSelectedCardKeys((prevSelectedCardKeys) => (prevSelectedCardKeys.length > 0 ? [] : prevSelectedCardKeys)); + }, []); - return { - keyForList: String(card.cardID), - cardID: card.cardID, - card, - lastFourPAN: card.lastFourPAN ?? "", - name: card.nameValuePairs?.cardTitle ?? "", - cardholder: - personalDetails?.[card.accountID ?? CONST.DEFAULT_NUMBER_ID], - limit: card.nameValuePairs?.unapprovedExpenseLimit ?? 0, - currency: settlementCurrency, - isVirtual: !!card.nameValuePairs?.isVirtual, - limitType: card.nameValuePairs?.limitType, - frozenByDisplayName, - frozenByAccountID: card.nameValuePairs?.frozen?.byAccountID, - frozenDate: card.nameValuePairs?.frozen?.date, - errors: card.errors, - pendingAction: card.pendingAction, - action: () => - Navigation.navigate( - createDynamicRoute( - DYNAMIC_ROUTES.WORKSPACE_EXPENSIFY_CARD_DETAILS.getRoute( - card.cardID.toString(), - ), - ), - ), - onClose: () => - clearDeletePaymentMethodError( - `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${defaultFundID}_${CONST.EXPENSIFY_CARD.BANK}`, - card.cardID, - ), - }; - }), - [allCards, defaultFundID, personalDetails, settlementCurrency, translate], - ); + useCleanupSelectedOptions(clearTableSelection); - const bulkExportOptions: Array< - DropdownOption - > = [ - { - icon: icons.Export, - text: translate("workspace.expensifyCard.exportAsCSV"), - value: CONST.EXPENSIFY_CARD.BULK_ACTIONS.EXPORT_CSV, - onSelected: () => { - const selectedCards = cardRows - .filter((row) => validatedSelectedCardKeys.includes(row.keyForList)) - .map((row) => row.card); - exportExpensifyCardListToCSV({ - policyID, - cards: selectedCards, - personalDetailsList: personalDetails, - settlementCurrency, - translate, - }); - }, - }, - ]; + const cardRows = useMemo( + () => + allCards.map((card) => { + const frozenByDisplayName = card.nameValuePairs?.frozen?.byAccountID + ? temporaryGetDisplayNameOrDefault({ + passedPersonalDetails: personalDetails?.[card.nameValuePairs.frozen.byAccountID], + defaultValue: '', + shouldFallbackToHidden: false, + translate, + }) || undefined + : undefined; - const handleIssueCardPress = () => { - if (!canWriteExpensifyCard) { - showReadOnlyModal(); - return; - } - clearIssueNewCardFormData(); - if (isAccountLocked) { - showLockedAccountModal(); - return; - } - if (isDelegateAccessRestricted) { - showDelegateNoAccessModal(); - return; - } - setIssueNewCardStepAndData({ policyID, isChangeAssigneeDisabled: false }); - Navigation.navigate( - createDynamicRoute( - DYNAMIC_ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW.path, - ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID), - ), + return { + keyForList: String(card.cardID), + cardID: card.cardID, + card, + lastFourPAN: card.lastFourPAN ?? '', + name: card.nameValuePairs?.cardTitle ?? '', + cardholder: personalDetails?.[card.accountID ?? CONST.DEFAULT_NUMBER_ID], + limit: card.nameValuePairs?.unapprovedExpenseLimit ?? 0, + currency: settlementCurrency, + isVirtual: !!card.nameValuePairs?.isVirtual, + limitType: card.nameValuePairs?.limitType, + frozenByDisplayName, + frozenByAccountID: card.nameValuePairs?.frozen?.byAccountID, + frozenDate: card.nameValuePairs?.frozen?.date, + errors: card.errors, + pendingAction: card.pendingAction, + action: () => Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_EXPENSIFY_CARD_DETAILS.getRoute(card.cardID.toString()))), + onClose: () => clearDeletePaymentMethodError(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${defaultFundID}_${CONST.EXPENSIFY_CARD.BANK}`, card.cardID), + }; + }), + [allCards, defaultFundID, personalDetails, settlementCurrency, translate], ); - }; - const secondaryActions = canWriteExpensifyCard - ? [ + + const bulkExportOptions: Array> = [ { - icon: icons.Gear, - text: translate("common.settings"), - onSelected: () => - Navigation.navigate( - ROUTES.WORKSPACE_EXPENSIFY_CARD_SETTINGS.getRoute(policyID), - ), - value: CONST.POLICY.SECONDARY_ACTIONS.SETTINGS, + icon: icons.Export, + text: translate('workspace.expensifyCard.exportAsCSV'), + value: CONST.EXPENSIFY_CARD.BULK_ACTIONS.EXPORT_CSV, + onSelected: () => { + const selectedCards = cardRows.filter((row) => validatedSelectedCardKeys.includes(row.keyForList)).map((row) => row.card); + exportExpensifyCardListToCSV({ + policyID, + cards: selectedCards, + personalDetailsList: personalDetails, + settlementCurrency, + translate, + }); + }, }, - ] - : []; - const getHeaderButtons = () => { - const headerButtonsRowStyle = [ - styles.flexRow, - styles.gap2, - !shouldShowSelector && shouldDisplayButtonsInSeparateLine && styles.mb3, - shouldShowSelector && shouldDisplayButtonsInSeparateLine && styles.mt3, ]; - const shouldShowBulkSelectionDropdown = shouldUseNarrowLayout - ? isMobileSelectionModeEnabled - : selectedCardIDs.length > 0; + const handleIssueCardPress = () => { + if (!canWriteExpensifyCard) { + showReadOnlyModal(); + return; + } + clearIssueNewCardFormData(); + if (isAccountLocked) { + showLockedAccountModal(); + return; + } + if (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + setIssueNewCardStepAndData({policyID, isChangeAssigneeDisabled: false}); + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW.path, ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID))); + }; + const secondaryActions = canWriteExpensifyCard + ? [ + { + icon: icons.Gear, + text: translate('common.settings'), + onSelected: () => Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_SETTINGS.getRoute(policyID)), + value: CONST.POLICY.SECONDARY_ACTIONS.SETTINGS, + }, + ] + : []; + const getHeaderButtons = () => { + const headerButtonsRowStyle = [ + styles.flexRow, + styles.gap2, + !shouldShowSelector && shouldDisplayButtonsInSeparateLine && styles.mb3, + shouldShowSelector && shouldDisplayButtonsInSeparateLine && styles.mt3, + ]; - if (shouldShowBulkSelectionDropdown) { - return ( - - - success - onPress={() => {}} - customText={translate("workspace.common.selected", { - count: selectedCardIDs.length, - })} - options={bulkExportOptions} - isSplitButton={false} - shouldAlwaysShowDropdownMenu - isDisabled={!selectedCardIDs.length} - sentryLabel={ - CONST.SENTRY_LABEL.WORKSPACE_EXPENSIFY_CARD.BULK_ACTIONS_DROPDOWN - } - wrapperStyle={[ - !isInLandscapeMode && styles.flexGrow1, - shouldDisplayButtonsInSeparateLine && styles.flexShrink1, - ]} - /> - - ); - } + const shouldShowBulkSelectionDropdown = shouldUseNarrowLayout ? isMobileSelectionModeEnabled : selectedCardIDs.length > 0; - return ( - - {!isCardListEmpty && ( -