diff --git a/src/CONST.ts b/src/CONST.ts index d74474978c2b..337058796a23 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5057,6 +5057,12 @@ const CONST = { }, }, + WORKSPACE_CARDS_LIST_LABEL_TYPE: { + CURRENT_BALANCE: 'currentBalance', + REMAINING_LIMIT: 'remainingLimit', + CASH_BACK: 'cashBack', + }, + EXCLUDE_FROM_LAST_VISITED_PATH: [SCREENS.NOT_FOUND, SCREENS.SAML_SIGN_IN, SCREENS.VALIDATE_LOGIN] as string[], } as const; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 8ec415442041..5088c1d3158f 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -427,6 +427,15 @@ const ONYXKEYS = { // Shared NVPs /** Collection of objects where each object represents the owner of the workspace that is past due billing AND the user is a member of. */ SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END: 'sharedNVP_private_billingGracePeriodEnd_', + + /** Expensify cards settings */ + SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS: 'sharedNVP_private_expensifyCardSettings_', + + /** + * Stores the card list for a given fundID and feed in the format: card__ + * So for example: card_12345_Expensify Card + */ + WORKSPACE_CARDS_LIST: 'card_', }, /** List of Form ids */ @@ -650,6 +659,8 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress; [ONYXKEYS.COLLECTION.SNAPSHOT]: OnyxTypes.SearchResults; [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: OnyxTypes.BillingGraceEndPeriod; + [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS]: OnyxTypes.ExpensifyCardSettings; + [ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST]: OnyxTypes.WorkspaceCardsList; }; type OnyxValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 1798b79bde0f..fc6cf82a0b33 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -783,6 +783,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/reportFields', getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields` as const, }, + WORKSPACE_EXPENSIFY_CARD: { + route: 'settings/workspaces/:policyID/expensify-card', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card` as const, + }, // TODO: uncomment after development is done // WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: { // route: 'settings/workspaces/:policyID/expensify-card/issues-new', @@ -794,10 +798,6 @@ const ROUTES = { route: 'settings/workspaces/:policyID/distance-rates', getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates` as const, }, - WORKSPACE_EXPENSIFY_CARD: { - route: 'settings/workspaces/:policyID/expensify-card', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card` as const, - }, WORKSPACE_CREATE_DISTANCE_RATE: { route: 'settings/workspaces/:policyID/distance-rates/new', getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates/new` as const, diff --git a/src/languages/en.ts b/src/languages/en.ts index 9a862171ae49..4a82830d6ccf 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1976,6 +1976,7 @@ export default { workspace: { common: { card: 'Cards', + expensifyCard: 'Expensify Card', workflows: 'Workflows', workspace: 'Workspace', edit: 'Edit workspace', @@ -2016,7 +2017,6 @@ export default { moreFeatures: 'More features', requested: 'Requested', distanceRates: 'Distance rates', - expensifyCard: 'Expensify Card', welcomeNote: ({workspaceName}: WelcomeNoteParams) => `You have been invited to ${workspaceName || 'a workspace'}! Download the Expensify mobile app at use.expensify.com/download to start tracking your expenses.`, subscription: 'Subscription', @@ -2308,6 +2308,20 @@ export default { control: 'Control', collect: 'Collect', }, + expensifyCard: { + issueCard: 'Issue card', + name: 'Name', + lastFour: 'Last 4', + limit: 'Limit', + currentBalance: 'Current balance', + currentBalanceDescription: 'Current balance is the sum of all posted Expensify Card transactions that have occurred since the last settlement date.', + remainingLimit: 'Remaining limit', + requestLimitIncrease: 'Request limit increase', + remainingLimitDescription: + 'We consider a number of factors when calculating your remaining limit: your tenure as a customer, the business-related information you provided during signup, and the available cash in your business bank account. Your remaining limit can fluctuate on a daily basis.', + cashBack: 'Cash back', + cashBackDescription: 'Cash back balance is based on settled monthly Expensify Card spend across your workspace.', + }, categories: { deleteCategories: 'Delete categories', deleteCategoriesPrompt: 'Are you sure you want to delete these categories?', diff --git a/src/languages/es.ts b/src/languages/es.ts index 911912ca59aa..d17693c869bb 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2000,6 +2000,7 @@ export default { workspace: { common: { card: 'Tarjetas', + expensifyCard: 'Tarjeta Expensify', workflows: 'Flujos de trabajo', workspace: 'Espacio de trabajo', edit: 'Editar espacio de trabajo', @@ -2015,7 +2016,6 @@ export default { bills: 'Pagar facturas', invoices: 'Enviar facturas', travel: 'Viajes', - expensifyCard: 'Tarjeta Expensify', members: 'Miembros', accounting: 'Contabilidad', plan: 'Plan', @@ -2341,6 +2341,21 @@ export default { control: 'Control', collect: 'Recolectar', }, + expensifyCard: { + issueCard: 'Emitir tarjeta', + name: 'Nombre', + lastFour: '4 últimos', + limit: 'Limite', + currentBalance: 'Saldo actual', + currentBalanceDescription: + 'El saldo actual es la suma de todas las transacciones contabilizadas con la Tarjeta Expensify que se han producido desde la última fecha de liquidación.', + remainingLimit: 'Límite restante', + requestLimitIncrease: 'Solicitar aumento de límite', + remainingLimitDescription: + 'A la hora de calcular tu límite restante, tenemos en cuenta una serie de factores: su antigüedad como cliente, la información relacionada con tu negocio que nos facilitaste al darte de alta y el efectivo disponible en tu cuenta bancaria comercial. Tu límite restante puede fluctuar a diario.', + cashBack: 'Reembolso', + cashBackDescription: 'El saldo de devolución se basa en el gasto mensual realizado con la tarjeta Expensify en tu espacio de trabajo.', + }, categories: { deleteCategories: 'Eliminar categorías', deleteCategoriesPrompt: '¿Estás seguro de que quieres eliminar estas categorías?', diff --git a/src/libs/API/parameters/OpenPolicyExpensifyCardsPageParams.ts b/src/libs/API/parameters/OpenPolicyExpensifyCardsPageParams.ts new file mode 100644 index 000000000000..c3c89857ab3b --- /dev/null +++ b/src/libs/API/parameters/OpenPolicyExpensifyCardsPageParams.ts @@ -0,0 +1,6 @@ +type OpenPolicyExpensifyCardsPageParams = { + policyID: string; + authToken: string | null | undefined; +}; + +export default OpenPolicyExpensifyCardsPageParams; diff --git a/src/libs/API/parameters/RequestExpensifyCardLimitIncreaseParams.ts b/src/libs/API/parameters/RequestExpensifyCardLimitIncreaseParams.ts new file mode 100644 index 000000000000..6e118f2a1c06 --- /dev/null +++ b/src/libs/API/parameters/RequestExpensifyCardLimitIncreaseParams.ts @@ -0,0 +1,6 @@ +type RequestExpensifyCardLimitIncreaseParams = { + authToken: string | null | undefined; + settlementBankAccountID: string; +}; + +export default RequestExpensifyCardLimitIncreaseParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index f032edf96e36..841c497c97bc 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -234,4 +234,6 @@ export type {default as UpdateSubscriptionAddNewUsersAutomaticallyParams} from ' export type {default as GenerateSpotnanaTokenParams} from './GenerateSpotnanaTokenParams'; export type {default as UpdateSubscriptionSizeParams} from './UpdateSubscriptionSizeParams'; export type {default as UpdateNetSuiteSubsidiaryParams} from './UpdateNetSuiteSubsidiaryParams'; +export type {default as OpenPolicyExpensifyCardsPageParams} from './OpenPolicyExpensifyCardsPageParams'; +export type {default as RequestExpensifyCardLimitIncreaseParams} from './RequestExpensifyCardLimitIncreaseParams'; export type {default as UpdateNetSuiteGenericTypeParams} from './UpdateNetSuiteGenericTypeParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 1d6456f3df47..a628092b12db 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -245,6 +245,7 @@ const WRITE_COMMANDS = { UPDATE_NETSUITE_TAX_POSTING_ACCOUNT: 'UpdateNetSuiteTaxPostingAccount', UPDATE_NETSUITE_ALLOW_FOREIGN_CURRENCY: 'UpdateNetSuiteAllowForeignCurrency', UPDATE_NETSUITE_EXPORT_TO_NEXT_OPEN_PERIOD: 'UpdateNetSuiteExportToNextOpenPeriod', + REQUEST_EXPENSIFY_CARD_LIMIT_INCREASE: 'RequestExpensifyCardLimitIncrease', CONNECT_POLICY_TO_SAGE_INTACCT: 'ConnectPolicyToSageIntacct', } as const; @@ -449,6 +450,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_UNIT]: Parameters.SetPolicyDistanceRatesUnitParams; [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams; [WRITE_COMMANDS.ENABLE_DISTANCE_REQUEST_TAX]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams; + [WRITE_COMMANDS.REQUEST_EXPENSIFY_CARD_LIMIT_INCREASE]: Parameters.RequestExpensifyCardLimitIncreaseParams; [WRITE_COMMANDS.UPDATE_POLICY_CONNECTION_CONFIG]: Parameters.UpdatePolicyConnectionConfigParams; [WRITE_COMMANDS.UPDATE_MANY_POLICY_CONNECTION_CONFIGS]: Parameters.UpdateManyPolicyConnectionConfigurationsParams; @@ -532,6 +534,7 @@ const READ_COMMANDS = { OPEN_POLICY_CATEGORIES_PAGE: 'OpenPolicyCategoriesPage', OPEN_POLICY_TAGS_PAGE: 'OpenPolicyTagsPage', OPEN_POLICY_TAXES_PAGE: 'OpenPolicyTaxesPage', + OPEN_POLICY_EXPENSIFY_CARDS_PAGE: 'OpenPolicyExpensifyCardsPage', OPEN_WORKSPACE_INVITE_PAGE: 'OpenWorkspaceInvitePage', OPEN_DRAFT_WORKSPACE_REQUEST: 'OpenDraftWorkspaceRequest', OPEN_POLICY_WORKFLOWS_PAGE: 'OpenPolicyWorkflowsPage', @@ -586,6 +589,7 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE]: Parameters.OpenPolicyDistanceRatesPageParams; [READ_COMMANDS.OPEN_POLICY_MORE_FEATURES_PAGE]: Parameters.OpenPolicyMoreFeaturesPageParams; [READ_COMMANDS.OPEN_POLICY_ACCOUNTING_PAGE]: Parameters.OpenPolicyAccountingPageParams; + [READ_COMMANDS.OPEN_POLICY_EXPENSIFY_CARDS_PAGE]: Parameters.OpenPolicyExpensifyCardsPageParams; [READ_COMMANDS.SEARCH]: Parameters.SearchParams; [READ_COMMANDS.OPEN_SUBSCRIPTION_PAGE]: null; }; diff --git a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx index 748d92b49a1c..16e8404f5fe9 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx @@ -19,6 +19,7 @@ type Screens = Partial React.Co const CENTRAL_PANE_WORKSPACE_SCREENS = { [SCREENS.WORKSPACE.PROFILE]: () => require('../../../../pages/workspace/WorkspaceProfilePage').default, [SCREENS.WORKSPACE.CARD]: () => require('../../../../pages/workspace/card/WorkspaceCardPage').default, + [SCREENS.WORKSPACE.EXPENSIFY_CARD]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardPage').default, [SCREENS.WORKSPACE.WORKFLOWS]: () => require('../../../../pages/workspace/workflows/WorkspaceWorkflowsPage').default, [SCREENS.WORKSPACE.REIMBURSE]: () => require('../../../../pages/workspace/reimburse/WorkspaceReimbursePage').default, [SCREENS.WORKSPACE.BILLS]: () => require('../../../../pages/workspace/bills/WorkspaceBillsPage').default, @@ -31,7 +32,6 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = { [SCREENS.WORKSPACE.TAGS]: () => require('../../../../pages/workspace/tags/WorkspaceTagsPage').default, [SCREENS.WORKSPACE.TAXES]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxesPage').default, [SCREENS.WORKSPACE.REPORT_FIELDS]: () => require('../../../../pages/workspace/reportFields/WorkspaceReportFieldsPage').default, - [SCREENS.WORKSPACE.EXPENSIFY_CARD]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardPage').default, [SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default, } satisfies Screens; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 08274ab4c143..840803f77d18 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -827,6 +827,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.CARD]: { path: ROUTES.WORKSPACE_CARD.route, }, + [SCREENS.WORKSPACE.EXPENSIFY_CARD]: { + path: ROUTES.WORKSPACE_EXPENSIFY_CARD.route, + }, [SCREENS.WORKSPACE.WORKFLOWS]: { path: ROUTES.WORKSPACE_WORKFLOWS.route, }, @@ -863,9 +866,6 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.REPORT_FIELDS]: { path: ROUTES.WORKSPACE_REPORT_FIELDS.route, }, - [SCREENS.WORKSPACE.EXPENSIFY_CARD]: { - path: ROUTES.WORKSPACE_EXPENSIFY_CARD.route, - }, [SCREENS.WORKSPACE.DISTANCE_RATES]: { path: ROUTES.WORKSPACE_DISTANCE_RATES.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 26c14a50ec0a..9179c3671be2 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -853,6 +853,9 @@ type FullScreenNavigatorParamList = { [SCREENS.WORKSPACE.CARD]: { policyID: string; }; + [SCREENS.WORKSPACE.EXPENSIFY_CARD]: { + policyID: string; + }; [SCREENS.WORKSPACE.WORKFLOWS]: { policyID: string; }; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 1bb53fbfa002..cd1acb564e22 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -18,12 +18,14 @@ import type { EnablePolicyWorkflowsParams, LeavePolicyParams, OpenDraftWorkspaceRequestParams, + OpenPolicyExpensifyCardsPageParams, OpenPolicyMoreFeaturesPageParams, OpenPolicyTaxesPageParams, OpenPolicyWorkflowsPageParams, OpenWorkspaceInvitePageParams, OpenWorkspaceParams, OpenWorkspaceReimburseViewParams, + RequestExpensifyCardLimitIncreaseParams, SetWorkspaceApprovalModeParams, SetWorkspaceAutoReportingFrequencyParams, SetWorkspaceAutoReportingMonthlyOffsetParams, @@ -1927,6 +1929,17 @@ function openPolicyTaxesPage(policyID: string) { API.read(READ_COMMANDS.OPEN_POLICY_TAXES_PAGE, params); } +function openPolicyExpensifyCardsPage(policyID: string) { + const authToken = NetworkStore.getAuthToken(); + + const params: OpenPolicyExpensifyCardsPageParams = { + policyID, + authToken, + }; + + API.read(READ_COMMANDS.OPEN_POLICY_EXPENSIFY_CARDS_PAGE, params); +} + function openWorkspaceInvitePage(policyID: string, clientMemberEmails: string[]) { if (!policyID || !clientMemberEmails) { Log.warn('openWorkspaceInvitePage invalid params', {policyID, clientMemberEmails}); @@ -1947,6 +1960,17 @@ function openDraftWorkspaceRequest(policyID: string) { API.read(READ_COMMANDS.OPEN_DRAFT_WORKSPACE_REQUEST, params); } +function requestExpensifyCardLimitIncrease(settlementBankAccountID: string) { + const authToken = NetworkStore.getAuthToken(); + + const params: RequestExpensifyCardLimitIncreaseParams = { + authToken, + settlementBankAccountID, + }; + + API.write(WRITE_COMMANDS.REQUEST_EXPENSIFY_CARD_LIMIT_INCREASE, params); +} + function setWorkspaceInviteMessageDraft(policyID: string, message: string | null) { Onyx.set(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT}${policyID}`, message); } @@ -3043,6 +3067,8 @@ export { buildPolicyData, enableExpensifyCard, createPolicyExpenseChats, + openPolicyExpensifyCardsPage, + requestExpensifyCardLimitIncrease, getPoliciesConnectedToSageIntacct, }; diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 3810ea30b830..032b3698ff00 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -89,10 +89,9 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro const policyID = policy?.id ?? ''; // @ts-expect-error a new props will be added during feed api implementation const workspaceAccountID = policy?.workspaceAccountID ?? ''; - // @ts-expect-error onyx key will be available after this PR https://github.com/Expensify/App/pull/44469 - const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.EXPENSIFY_CARDS_LIST}${workspaceAccountID}_Expensify Card`); + const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`); // Uncomment this line for testing disabled toggle feature - for c+ - // const [cardsList = mockedCardsList] = useOnyx(`${ONYXKEYS.COLLECTION.EXPENSIFY_CARDS_LIST}${workspaceAccountID}_Expensify Card`); + // const [cardsList = mockedCardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`); const [isOrganizeWarningModalOpen, setIsOrganizeWarningModalOpen] = useState(false); const [isIntegrateWarningModalOpen, setIsIntegrateWarningModalOpen] = useState(false); diff --git a/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx b/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx new file mode 100644 index 000000000000..0ec5b2fc18fa --- /dev/null +++ b/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import WorkspaceCardsListLabel from './WorkspaceCardsListLabel'; + +// TODO: remove when Onyx data is available +const mockedSettings = { + currentBalance: 5000, + remainingLimit: 3000, + cashBack: 2000, +}; + +function WorkspaceCardListHeader() { + const {shouldUseNarrowLayout, isMediumScreenWidth, isSmallScreenWidth} = useResponsiveLayout(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const isLessThanMediumScreen = isMediumScreenWidth || isSmallScreenWidth; + + // TODO: uncomment the code line below to use cardSettings data from Onyx when it's supported + // const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS}${policyID}`); + const cardSettings = mockedSettings; + + return ( + + + + + + + + + + + + + {translate('workspace.expensifyCard.name')} + + + + + {translate('workspace.expensifyCard.lastFour')} + + + + + {translate('workspace.expensifyCard.limit')} + + + + + ); +} + +WorkspaceCardListHeader.displayName = 'WorkspaceCardListHeader'; + +export default WorkspaceCardListHeader; diff --git a/src/pages/workspace/expensifyCard/WorkspaceCardListRow.tsx b/src/pages/workspace/expensifyCard/WorkspaceCardListRow.tsx new file mode 100644 index 000000000000..92d814604e57 --- /dev/null +++ b/src/pages/workspace/expensifyCard/WorkspaceCardListRow.tsx @@ -0,0 +1,82 @@ +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; +import Avatar from '@components/Avatar'; +import Badge from '@components/Badge'; +import Text from '@components/Text'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import {getDefaultAvatarURL} from '@libs/UserUtils'; +import CONST from '@src/CONST'; +import type {PersonalDetails} from '@src/types/onyx'; + +type WorkspacesListRowProps = { + /** Additional styles applied to the row */ + style: StyleProp; + + /** The last four digits of the card */ + lastFourPAN: string; + + /** Card name */ + name: string; + + /** Cardholder personal details */ + cardholder: PersonalDetails; + + /** Card limit */ + limit: number; + + /** Policy currency */ + currency: string; +}; + +function WorkspaceCardListRow({style, limit, cardholder, lastFourPAN, name, currency}: WorkspacesListRowProps) { + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const styles = useThemeStyles(); + + const cardholderName = useMemo(() => PersonalDetailsUtils.getDisplayNameOrDefault(cardholder), [cardholder]); + + return ( + + + + + + {cardholderName} + + + {name} + + + + + + {lastFourPAN} + + + + + + + ); +} + +WorkspaceCardListRow.displayName = 'WorkspaceCardListRow'; + +export default WorkspaceCardListRow; diff --git a/src/pages/workspace/expensifyCard/WorkspaceCardsListLabel.tsx b/src/pages/workspace/expensifyCard/WorkspaceCardsListLabel.tsx new file mode 100644 index 000000000000..59a6b168d0bc --- /dev/null +++ b/src/pages/workspace/expensifyCard/WorkspaceCardsListLabel.tsx @@ -0,0 +1,133 @@ +import type {RouteProp} from '@react-navigation/native'; +import {useRoute} from '@react-navigation/native'; +import React, {useEffect, useMemo, useRef, useState} from 'react'; +import {View} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import Button from '@components/Button'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Popover from '@components/Popover'; +import {PressableWithFeedback} from '@components/Pressable'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import getClickedTargetLocation from '@libs/getClickedTargetLocation'; +import type {FullScreenNavigatorParamList} from '@navigation/types'; +import variables from '@styles/variables'; +import * as Report from '@userActions/Report'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; + +type WorkspaceCardsListLabelProps = { + /** Label type */ + type: ValueOf; + + /** Label value */ + value: number; + + /** Additional style props */ + style?: StyleProp; +}; + +function WorkspaceCardsListLabel({type, value, style}: WorkspaceCardsListLabelProps) { + const route = useRoute>(); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`); + const styles = useThemeStyles(); + const {windowWidth} = useWindowDimensions(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const theme = useTheme(); + const {translate} = useLocalize(); + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const [isVisible, setVisible] = useState(false); + const [anchorPosition, setAnchorPosition] = useState({top: 0, left: 0}); + const anchorRef = useRef(null); + + const policyCurrency = useMemo(() => policy?.outputCurrency ?? CONST.CURRENCY.USD, [policy]); + // TODO: instead of the first bankAccount on the list get settlementBankAccountID from the private_expensifyCardSettings NVP and check if that is connected via Plaid. + const isConnectedWithPlaid = useMemo(() => !!Object.values(bankAccountList ?? {})[0]?.accountData?.additionalData?.plaidAccountID, [bankAccountList]); + + useEffect(() => { + if (!anchorRef.current || !isVisible) { + return; + } + + const position = getClickedTargetLocation(anchorRef.current); + const BOTTOM_MARGIN_OFFSET = 3; + + setAnchorPosition({ + top: position.top + position.height + BOTTOM_MARGIN_OFFSET, + left: position.left, + }); + }, [isVisible, windowWidth]); + + const requestLimitIncrease = () => { + // TODO: uncomment when RequestExpensifyCardLimitIncrease API call is supported + // Policy.requestExpensifyCardLimitIncrease(settlementBankAccountID); + setVisible(false); + Report.navigateToConciergeChat(); + }; + + return ( + + + {translate(`workspace.expensifyCard.${type}`)} + setVisible(true)} + > + + + + + {CurrencyUtils.convertToDisplayString(value, policyCurrency)} + + setVisible(false)} + isVisible={isVisible} + outerStyle={!shouldUseNarrowLayout ? styles.pr5 : undefined} + innerContainerStyle={!shouldUseNarrowLayout ? {maxWidth: variables.modalContentMaxWidth} : undefined} + anchorRef={anchorRef} + anchorPosition={anchorPosition} + > + + + {translate(`workspace.expensifyCard.${type}`)} + + {translate(`workspace.expensifyCard.${type}Description`)} + + {!isConnectedWithPlaid && type === CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.REMAINING_LIMIT && ( + +