diff --git a/src/CONST.ts b/src/CONST.ts index 171dc7ff2c8a..97ea1dd2b27d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -825,6 +825,7 @@ const CONST = { CARD_MISSING_ADDRESS: 'CARDMISSINGADDRESS', CARD_ISSUED: 'CARDISSUED', CARD_ISSUED_VIRTUAL: 'CARDISSUEDVIRTUAL', + CARD_ASSIGNED: 'CARDASSIGNED', CHANGE_FIELD: 'CHANGEFIELD', // OldDot Action CHANGE_POLICY: 'CHANGEPOLICY', // OldDot Action CHANGE_TYPE: 'CHANGETYPE', // OldDot Action @@ -2509,6 +2510,7 @@ const CONST = { MASTER_CARD: 'cdf', VISA: 'vcf', AMEX: 'gl1025', + STRIPE: 'stripe', }, STEP_NAMES: ['1', '2', '3', '4'], STEP: { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 0b69fe9be80b..d083a46d7760 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -441,6 +441,9 @@ const ONYXKEYS = { /** Stores recently used currencies */ RECENTLY_USED_CURRENCIES: 'nvp_recentlyUsedCurrencies', + /** Company cards custom names */ + NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES: 'nvp_expensify_ccCustomNames', + /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -849,7 +852,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST]: OnyxTypes.WorkspaceCardsList; [ONYXKEYS.COLLECTION.EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.PolicyConnectionName; [ONYXKEYS.COLLECTION.EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION]: boolean; - [ONYXKEYS.COLLECTION.LAST_SELECTED_FEED]: string; + [ONYXKEYS.COLLECTION.LAST_SELECTED_FEED]: OnyxTypes.CompanyCardFeed; }; type OnyxValuesMapping = { @@ -1003,6 +1006,7 @@ type OnyxValuesMapping = { [ONYXKEYS.LAST_ROUTE]: string; [ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean; [ONYXKEYS.SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP]: boolean; + [ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 98ea64bc65b4..33dd4da77532 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1072,7 +1072,7 @@ const ROUTES = { }, WORKSPACE_COMPANY_CARDS_ASSIGN_CARD: { route: 'settings/workspaces/:policyID/company-cards/:feed/assign-card', - getRoute: (policyID: string, feed: string) => `settings/workspaces/${policyID}/company-cards/${feed}/assign-card` as const, + getRoute: (policyID: string, feed: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/company-cards/${feed}/assign-card`, backTo), }, WORKSPACE_COMPANY_CARD_DETAILS: { route: 'settings/workspaces/:policyID/company-cards/:bank/:cardID', diff --git a/src/languages/en.ts b/src/languages/en.ts index d73015693e7a..ccc482941d51 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3046,9 +3046,10 @@ const translations = { addNewCard: { other: 'Other', cardProviders: { - amex: 'American Express Corporate Cards', - mastercard: 'Mastercard Commercial Cards', - visa: 'Visa Commercial Cards', + gl1025: 'American Express Corporate Cards', + cdf: 'Mastercard Commercial Cards', + vcf: 'Visa Commercial Cards', + stripe: 'Stripe Cards', }, yourCardProvider: `Who's your card provider?`, whoIsYourBankAccount: 'Who’s your bank?', @@ -3062,25 +3063,25 @@ const translations = { enableFeed: { title: ({provider}: GoBackMessageParams) => `Enable your ${provider} feed`, heading: 'We have a direct integration with your card issuer and can import your transaction data into Expensify quickly and accurately.\n\nTo get started, simply:', - visa: `1. Visit [this help article](${CONST.COMPANY_CARDS_HELP}) for detailed instructionson how to set up your Visa Commercial Cards.\n\n2. [Contact your bank](${CONST.COMPANY_CARDS_HELP}) to verify they support a custom feed for your program, and ask them toenable it.\n\n3. *Once the feed is enabled and you have its details, continue to the next screen.*`, - amex: `1. Visit [this help article](${CONST.COMPANY_CARDS_HELP}) to find out if American Express can enable a custom feed for your program.\n\n2. Once the feed is enabled, Amex will send you a production letter.\n\n3. *Once you have the feed information, continue to the next screen.*`, - mastercard: `1. Visit [this help article](${CONST.COMPANY_CARDS_HELP}) for detailed instructions on how to set up your Mastercard Commercial Cards.\n\n 2. [Contact your bank](${CONST.COMPANY_CARDS_HELP}) to verify they support a custom feed for your program, and ask them to enable it.\n\n3. *Once the feed is enabled and you have its details, continue to the next screen.*`, + vcf: `1. Visit [this help article](${CONST.COMPANY_CARDS_HELP}) for detailed instructionson how to set up your Visa Commercial Cards.\n\n2. [Contact your bank](${CONST.COMPANY_CARDS_HELP}) to verify they support a custom feed for your program, and ask them toenable it.\n\n3. *Once the feed is enabled and you have its details, continue to the next screen.*`, + gl1025: `1. Visit [this help article](${CONST.COMPANY_CARDS_HELP}) to find out if American Express can enable a custom feed for your program.\n\n2. Once the feed is enabled, Amex will send you a production letter.\n\n3. *Once you have the feed information, continue to the next screen.*`, + cdf: `1. Visit [this help article](${CONST.COMPANY_CARDS_HELP}) for detailed instructions on how to set up your Mastercard Commercial Cards.\n\n 2. [Contact your bank](${CONST.COMPANY_CARDS_HELP}) to verify they support a custom feed for your program, and ask them to enable it.\n\n3. *Once the feed is enabled and you have its details, continue to the next screen.*`, stripe: `1. Visit Stripe’s Dashboard, and go to [Settings](${CONST.COMPANY_CARDS_STRIPE_HELP}).\n\n2. Under Product Integrations, click Enable next to Expensify.\n\n3. Once the feed is enabled, click Submit below and we’ll work on adding it.`, }, whatBankIssuesCard: 'What bank issues these cards?', enterNameOfBank: 'Enter name of bank', feedDetails: { - visa: { + vcf: { title: 'What are the Visa feed details?', processorLabel: 'Processor ID', bankLabel: 'Financial institution (bank) ID', companyLabel: 'Company ID', }, - amex: { + gl1025: { title: `What's the Amex delivery file name?`, fileNameLabel: 'Delivery file name', }, - mastercard: { + cdf: { title: `What's the Mastercard distribution ID?`, distributionLabel: 'Distribution ID', }, @@ -3116,7 +3117,8 @@ const translations = { brokenConnectionErrorFirstPart: `Card feed connection is broken. Please `, brokenConnectionErrorLink: 'log into your bank ', brokenConnectionErrorSecondPart: 'so we can establish the connection again.', - assignedYouCard: ({assigner}: AssignedYouCardParams) => `${assigner} assigned you a company card! Imported transactions will appear in this chat.`, + assignedYouCard: ({link}: AssignedYouCardParams) => `assigned you a ${link}! Imported transactions will appear in this chat.`, + companyCard: 'company card', chooseCardFeed: 'Choose card feed', }, expensifyCard: { diff --git a/src/languages/es.ts b/src/languages/es.ts index a9d35a6f8228..ca2e730d71c5 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3083,9 +3083,10 @@ const translations = { addNewCard: { other: 'Otros', cardProviders: { - amex: 'Tarjetas de empresa American Express', - mastercard: 'Tarjetas comerciales Mastercard', - visa: 'Tarjetas comerciales Visa', + gl1025: 'Tarjetas de empresa American Express', + cdf: 'Tarjetas comerciales Mastercard', + vcf: 'Tarjetas comerciales Visa', + stripe: 'Tarjetas comerciales Stripe', }, yourCardProvider: `¿Quién es su proveedor de tarjetas?`, whoIsYourBankAccount: '¿Cuál es tu banco?', @@ -3100,25 +3101,25 @@ const translations = { title: ({provider}: GoBackMessageParams) => `Habilita tu feed ${provider}`, heading: 'Tenemos una integración directa con el emisor de su tarjeta y podemos importar los datos de sus transacciones a Expensify de forma rápida y precisa.\n\nPara empezar, simplemente:', - visa: `1. Visite [este artículo de ayuda](${CONST.COMPANY_CARDS_HELP}) para obtener instrucciones detalladas sobre cómo configurar sus tarjetas comerciales Visa.\n\n2. [Póngase en contacto con su banco](${CONST.COMPANY_CARDS_HELP}) para comprobar que admiten un feed personalizado para su programa, y pídales que lo activen.\n\n3. *Una vez que el feed esté habilitado y tengas sus datos, pasa a la siguiente pantalla.*`, - amex: `1. Visite [este artículo de ayuda](${CONST.COMPANY_CARDS_HELP}) para saber si American Express puede habilitar un feed personalizado para su programa.\n\n2. Una vez activada la alimentación, Amex le enviará una carta de producción.\n\n3. *Una vez que tenga la información de alimentación, continúe con la siguiente pantalla.*`, - mastercard: `1. Visite [este artículo de ayuda](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS}) para obtener instrucciones detalladas sobre cómo configurar sus tarjetas comerciales Mastercard.\n\n 2. [Póngase en contacto con su banco](${CONST.COMPANY_CARDS_HELP}) para verificar que admiten un feed personalizado para su programa, y pídales que lo habiliten.\n\n3. *Una vez que el feed esté habilitado y tengas sus datos, pasa a la siguiente pantalla.*`, + vcf: `1. Visite [este artículo de ayuda](${CONST.COMPANY_CARDS_HELP}) para obtener instrucciones detalladas sobre cómo configurar sus tarjetas comerciales Visa.\n\n2. [Póngase en contacto con su banco](${CONST.COMPANY_CARDS_HELP}) para comprobar que admiten un feed personalizado para su programa, y pídales que lo activen.\n\n3. *Una vez que el feed esté habilitado y tengas sus datos, pasa a la siguiente pantalla.*`, + gl1025: `1. Visite [este artículo de ayuda](${CONST.COMPANY_CARDS_HELP}) para saber si American Express puede habilitar un feed personalizado para su programa.\n\n2. Una vez activada la alimentación, Amex le enviará una carta de producción.\n\n3. *Una vez que tenga la información de alimentación, continúe con la siguiente pantalla.*`, + cdf: `1. Visite [este artículo de ayuda](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS}) para obtener instrucciones detalladas sobre cómo configurar sus tarjetas comerciales Mastercard.\n\n 2. [Póngase en contacto con su banco](${CONST.COMPANY_CARDS_HELP}) para verificar que admiten un feed personalizado para su programa, y pídales que lo habiliten.\n\n3. *Una vez que el feed esté habilitado y tengas sus datos, pasa a la siguiente pantalla.*`, stripe: `1. Visita el Panel de Stripe y ve a [Configuraciones](${CONST.COMPANY_CARDS_STRIPE_HELP}).\n\n2. En Integraciones de Productos, haz clic en Habilitar junto a Expensify.\n\n3. Una vez que la fuente esté habilitada, haz clic en Enviar abajo y comenzaremos a añadirla.`, }, whatBankIssuesCard: '¿Qué banco emite estas tarjetas?', enterNameOfBank: 'Introduzca el nombre del banco', feedDetails: { - visa: { + vcf: { title: '¿Cuáles son los datos de alimentación de Visa?', processorLabel: 'ID del procesador', bankLabel: 'Identificación de la institución financiera (banco)', companyLabel: 'Empresa ID', }, - amex: { + gl1025: { title: `¿Cuál es el nombre del archivo de entrega de Amex?`, fileNameLabel: 'Nombre del archivo de entrega', }, - mastercard: { + cdf: { title: `¿Cuál es el identificador de distribución de Mastercard?`, distributionLabel: 'ID de distribución', }, @@ -3154,7 +3155,8 @@ const translations = { brokenConnectionErrorFirstPart: `La conexión de la fuente de tarjetas está rota. Por favor, `, brokenConnectionErrorLink: 'inicia sesión en tu banco ', brokenConnectionErrorSecondPart: 'para que podamos restablecer la conexión.', - assignedYouCard: ({assigner}: AssignedYouCardParams) => `¡${assigner} te ha asignado una tarjeta de empresa! Las transacciones importadas aparecerán en este chat.`, + assignedYouCard: ({link}: AssignedYouCardParams) => `te ha asignado una ${link}! Las transacciones importadas aparecerán en este chat.`, + companyCard: 'tarjeta de empresa', chooseCardFeed: 'Elige feed de tarjetas', }, expensifyCard: { diff --git a/src/languages/params.ts b/src/languages/params.ts index 12b119777016..02dafa76a46d 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -477,7 +477,7 @@ type SpreadCategoriesParams = { }; type AssignedYouCardParams = { - assigner: string; + link: string; }; type FeatureNameParams = { diff --git a/src/libs/API/parameters/AssignCompanyCardParams.ts b/src/libs/API/parameters/AssignCompanyCardParams.ts new file mode 100644 index 000000000000..c4dcd7c628a0 --- /dev/null +++ b/src/libs/API/parameters/AssignCompanyCardParams.ts @@ -0,0 +1,10 @@ +type AssignCompanyCardParams = { + policyID: string; + bankName: string; + encryptedCardNumber: string; + email: string; + startDate: string; + reportActionID: string; +}; + +export default AssignCompanyCardParams; diff --git a/src/libs/API/parameters/OpenPolicyCompanyCardsFeedParams.ts b/src/libs/API/parameters/OpenPolicyCompanyCardsFeedParams.ts new file mode 100644 index 000000000000..e3a8653886b9 --- /dev/null +++ b/src/libs/API/parameters/OpenPolicyCompanyCardsFeedParams.ts @@ -0,0 +1,6 @@ +type OpenPolicyCompanyCardsFeedParams = { + policyID: string; + feed: string; +}; + +export default OpenPolicyCompanyCardsFeedParams; diff --git a/src/libs/API/parameters/RequestFeedSetupParams.ts b/src/libs/API/parameters/RequestFeedSetupParams.ts new file mode 100644 index 000000000000..98e22c611efd --- /dev/null +++ b/src/libs/API/parameters/RequestFeedSetupParams.ts @@ -0,0 +1,8 @@ +type RequestFeedSetupParams = { + authToken: string; + policyID: string; + feedDetails: string; + feedType: string; +}; + +export default RequestFeedSetupParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index ddf10a138725..1d0b995c2c05 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -327,10 +327,13 @@ export type {default as UpdateCardSettlementAccountParams} from './UpdateCardSet export type {default as SetCompanyCardFeedName} from './SetCompanyCardFeedName'; export type {default as DeleteCompanyCardFeed} from './DeleteCompanyCardFeed'; export type {default as SetCompanyCardTransactionLiability} from './SetCompanyCardTransactionLiability'; +export type {default as OpenPolicyCompanyCardsFeedParams} from './OpenPolicyCompanyCardsFeedParams'; +export type {default as AssignCompanyCardParams} from './AssignCompanyCardParams'; export type {default as UnassignCompanyCard} from './UnassignCompanyCard'; export type {default as UpdateCompanyCard} from './UpdateCompanyCard'; export type {default as UpdateCompanyCardNameParams} from './UpdateCompanyCardNameParams'; export type {default as SetCompanyCardExportAccountParams} from './SetCompanyCardExportAccountParams'; +export type {default as RequestFeedSetupParams} from './RequestFeedSetupParams'; export type {default as SetMissingPersonalDetailsAndShipExpensifyCardParams} from './SetMissingPersonalDetailsAndShipExpensifyCardParams'; export type {default as SetInvoicingTransferBankAccountParams} from './SetInvoicingTransferBankAccountParams'; export type {default as ConnectPolicyToQuickBooksDesktopParams} from './ConnectPolicyToQuickBooksDesktopParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 14387b0e170c..1c517657b70a 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -413,9 +413,11 @@ const WRITE_COMMANDS = { UPDATE_XERO_SYNC_INVOICE_COLLECTIONS_ACCOUNT_ID: 'UpdateXeroSyncInvoiceCollectionsAccountID', UPDATE_XERO_SYNC_SYNC_REIMBURSED_REPORTS: 'UpdateXeroSyncSyncReimbursedReports', UPDATE_XERO_SYNC_REIMBURSEMENT_ACCOUNT_ID: 'UpdateXeroSyncReimbursementAccountID', + REQUEST_FEED_SETUP: 'RequestFeedSetup', SET_COMPANY_CARD_FEED_NAME: 'SetFeedName', DELETE_COMPANY_CARD_FEED: 'RemoveFeed', SET_COMPANY_CARD_TRANSACTION_LIABILITY: 'SetFeedTransactionLiability', + ASSIGN_COMPANY_CARD: 'AssignCard', UNASSIGN_COMPANY_CARD: 'UnassignCard', UPDATE_COMPANY_CARD: 'SyncCard', UPDATE_COMPANY_CARD_NAME: 'SetCardName', @@ -480,8 +482,10 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_STATUS]: Parameters.UpdateStatusParams; [WRITE_COMMANDS.CLEAR_STATUS]: null; [WRITE_COMMANDS.UPDATE_PERSONAL_DETAILS_FOR_WALLET]: Parameters.UpdatePersonalDetailsForWalletParams; + [WRITE_COMMANDS.REQUEST_FEED_SETUP]: Parameters.RequestFeedSetupParams; [WRITE_COMMANDS.SET_COMPANY_CARD_FEED_NAME]: Parameters.SetCompanyCardFeedName; [WRITE_COMMANDS.DELETE_COMPANY_CARD_FEED]: Parameters.DeleteCompanyCardFeed; + [WRITE_COMMANDS.ASSIGN_COMPANY_CARD]: Parameters.AssignCompanyCardParams; [WRITE_COMMANDS.UNASSIGN_COMPANY_CARD]: Parameters.UnassignCompanyCard; [WRITE_COMMANDS.UPDATE_COMPANY_CARD]: Parameters.UpdateCompanyCard; [WRITE_COMMANDS.UPDATE_COMPANY_CARD_NAME]: Parameters.UpdateCompanyCardNameParams; @@ -896,6 +900,7 @@ const READ_COMMANDS = { OPEN_POLICY_TAXES_PAGE: 'OpenPolicyTaxesPage', OPEN_POLICY_REPORT_FIELDS_PAGE: 'OpenPolicyReportFieldsPage', OPEN_POLICY_EXPENSIFY_CARDS_PAGE: 'OpenPolicyExpensifyCardsPage', + OPEN_POLICY_COMPANY_CARDS_FEED: 'OpenPolicyCompanyCardsFeed', OPEN_POLICY_COMPANY_CARDS_PAGE: 'OpenPolicyCompanyCardsPage', OPEN_POLICY_EDIT_CARD_LIMIT_TYPE_PAGE: 'OpenPolicyEditCardLimitTypePage', OPEN_WORKSPACE_INVITE_PAGE: 'OpenWorkspaceInvitePage', @@ -961,6 +966,7 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_POLICY_ACCOUNTING_PAGE]: Parameters.OpenPolicyAccountingPageParams; [READ_COMMANDS.OPEN_POLICY_EXPENSIFY_CARDS_PAGE]: Parameters.OpenPolicyExpensifyCardsPageParams; [READ_COMMANDS.OPEN_POLICY_COMPANY_CARDS_PAGE]: Parameters.OpenPolicyExpensifyCardsPageParams; + [READ_COMMANDS.OPEN_POLICY_COMPANY_CARDS_FEED]: Parameters.OpenPolicyCompanyCardsFeedParams; [READ_COMMANDS.OPEN_POLICY_EDIT_CARD_LIMIT_TYPE_PAGE]: Parameters.OpenPolicyEditCardLimitTypePageParams; [READ_COMMANDS.OPEN_POLICY_PROFILE_PAGE]: Parameters.OpenPolicyProfilePageParams; [READ_COMMANDS.OPEN_POLICY_INITIAL_PAGE]: Parameters.OpenPolicyInitialPageParams; diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 0ee037c3c354..7c81c5c224c6 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -2,12 +2,13 @@ import groupBy from 'lodash/groupBy'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import ExpensifyCardImage from '@assets/images/expensify-card.svg'; import * as Illustrations from '@src/components/Icon/Illustrations'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type {OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {BankAccountList, Card, CardList, PersonalDetailsList, WorkspaceCardsList} from '@src/types/onyx'; +import type {BankAccountList, Card, CardFeeds, CardList, CompanyCardFeed, PersonalDetailsList, WorkspaceCardsList} from '@src/types/onyx'; import type Policy from '@src/types/onyx/Policy'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -172,7 +173,8 @@ function getEligibleBankAccountsForCard(bankAccountsList: OnyxEntry, personalDetails: OnyxEntry): Card[] { - return Object.values(cardsList ?? {}).sort((cardA: Card, cardB: Card) => { + const {cardList, ...cards} = cardsList ?? {}; + return Object.values(cards).sort((cardA: Card, cardB: Card) => { const userA = personalDetails?.[cardA.accountID ?? '-1'] ?? {}; const userB = personalDetails?.[cardB.accountID ?? '-1'] ?? {}; @@ -183,6 +185,14 @@ function sortCardsByCardholderName(cardsList: OnyxEntry, per }); } +function getCompanyCardNumber(cardList: Record, lastFourPAN?: string): string { + if (!lastFourPAN) { + return ''; + } + + return Object.keys(cardList).find((card) => card.endsWith(lastFourPAN)) ?? ''; +} + function getCardFeedIcon(cardFeed: string): IconAsset { if (cardFeed.startsWith(CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD)) { return Illustrations.MasterCardCompanyCards; @@ -195,6 +205,17 @@ function getCardFeedIcon(cardFeed: string): IconAsset { return Illustrations.AmexCompanyCards; } +function getCardFeedName(feedType: CompanyCardFeed): string { + const feedNamesMapping = { + [CONST.COMPANY_CARD.FEED_BANK_NAME.VISA]: 'Visa', + [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD]: 'Mastercard', + [CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX]: 'American Express', + [CONST.COMPANY_CARD.FEED_BANK_NAME.STRIPE]: 'Stripe', + }; + + return feedNamesMapping[feedType]; +} + function getCardDetailsImage(cardFeed: string): IconAsset { if (cardFeed.startsWith(CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD)) { return Illustrations.MasterCardCompanyCardDetail; @@ -204,17 +225,20 @@ function getCardDetailsImage(cardFeed: string): IconAsset { return Illustrations.VisaCompanyCardDetail; } + if (cardFeed.startsWith(CONST.EXPENSIFY_CARD.BANK)) { + return ExpensifyCardImage; + } + return Illustrations.AmexCardCompanyCardDetail; } function getMemberCards(policy: OnyxEntry, allCardsList: OnyxCollection, accountID?: number) { const workspaceId = policy?.workspaceAccountID ? policy.workspaceAccountID.toString() : ''; const cards: WorkspaceCardsList = {}; - const mockedCardsList = allCardsList ?? {}; - Object.keys(mockedCardsList) + Object.keys(allCardsList ?? {}) .filter((key) => key !== `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceId}_${CONST.EXPENSIFY_CARD.BANK}` && key.includes(workspaceId)) .forEach((key) => { - const feedCards = mockedCardsList?.[key]; + const feedCards = allCardsList?.[key]; if (feedCards && Object.keys(feedCards).length > 0) { Object.keys(feedCards).forEach((feedCardKey) => { if (feedCards?.[feedCardKey].accountID !== accountID) { @@ -275,6 +299,11 @@ const getCorrectStepForSelectedBank = (selectedBank: ValueOf, cardFeeds: OnyxEntry): CompanyCardFeed { + const defaultFeed = Object.keys(cardFeeds?.settings?.companyCards ?? {}).at(0) as CompanyCardFeed; + return lastSelectedFeed ?? defaultFeed; +} + export { isExpensifyCard, isCorporateCard, @@ -290,9 +319,12 @@ export { getTranslationKeyForLimitType, getEligibleBankAccountsForCard, sortCardsByCardholderName, + getCompanyCardNumber, getCardFeedIcon, + getCardFeedName, getCardDetailsImage, getMemberCards, getBankCardDetailsImage, + getSelectedFeed, getCorrectStepForSelectedBank, }; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index ca71ecb3d95d..8493cb04ceed 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -20,6 +20,7 @@ import type NAVIGATORS from '@src/NAVIGATORS'; import type {HybridAppRoute, Route as Routes} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type EXIT_SURVEY_REASON_FORM_INPUT_IDS from '@src/types/form/ExitSurveyReasonForm'; +import type {CompanyCardFeed} from '@src/types/onyx'; import type {ConnectionName, SageIntacctMappingName} from '@src/types/onyx/Policy'; type NavigationRef = NavigationContainerRefWithCurrent; @@ -817,6 +818,11 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS]: { policyID: string; }; + [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: { + policyID: string; + feed: CompanyCardFeed; + backTo?: Routes; + }; [SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS_FEED_NAME]: { policyID: string; }; @@ -1351,10 +1357,6 @@ type FullScreenNavigatorParamList = { [SCREENS.WORKSPACE.COMPANY_CARDS_ADD_NEW]: { policyID: string; }; - [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: { - policyID: string; - feed: string; - }; [SCREENS.WORKSPACE.WORKFLOWS]: { policyID: string; }; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 31b44ac8f916..de0b5fab49d9 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1723,7 +1723,13 @@ function getRemovedFromApprovalChainMessage(reportAction: OnyxEntry) { - return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED, CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED_VIRTUAL, CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS); + return isActionOfType( + reportAction, + CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED, + CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED_VIRTUAL, + CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS, + CONST.REPORT.ACTIONS.TYPE.CARD_ASSIGNED, + ); } function getCardIssuedMessage(reportAction: OnyxEntry, shouldRenderHTML = false, policyID = '-1') { @@ -1742,9 +1748,12 @@ function getCardIssuedMessage(reportAction: OnyxEntry, shouldRende const isPolicyAdmin = PolicyUtils.isPolicyAdmin(PolicyUtils.getPolicy(policyID)); const assignee = shouldRenderHTML ? `` : assigneeDetails?.firstName ?? assigneeDetails?.login ?? ''; const navigateRoute = isPolicyAdmin ? ROUTES.EXPENSIFY_CARD_DETAILS.getRoute(policyID, String(cardID)) : ROUTES.SETTINGS_DOMAINCARD_DETAIL.getRoute(String(cardID)); - const link = shouldRenderHTML + const expensifyCardLink = shouldRenderHTML ? `${Localize.translateLocal('cardPage.expensifyCard')}` : Localize.translateLocal('cardPage.expensifyCard'); + const companyCardLink = shouldRenderHTML + ? `${Localize.translateLocal('workspace.companyCards.companyCard')}` + : Localize.translateLocal('workspace.companyCards.companyCard'); const missingDetails = !privatePersonalDetails?.legalFirstName || @@ -1761,7 +1770,9 @@ function getCardIssuedMessage(reportAction: OnyxEntry, shouldRende case CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED: return Localize.translateLocal('workspace.expensifyCard.issuedCard', {assignee}); case CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED_VIRTUAL: - return Localize.translateLocal('workspace.expensifyCard.issuedCardVirtual', {assignee, link}); + return Localize.translateLocal('workspace.expensifyCard.issuedCardVirtual', {assignee, link: expensifyCardLink}); + case CONST.REPORT.ACTIONS.TYPE.CARD_ASSIGNED: + return Localize.translateLocal('workspace.companyCards.assignedYouCard', {link: companyCardLink}); case CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS: return Localize.translateLocal(`workspace.expensifyCard.${shouldShowAddMissingDetailsButton ? 'issuedCardNoShippingDetails' : 'addedShippingDetails'}`, {assignee}); default: diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index bf687c973abb..f07bf0ab5b3e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -268,6 +268,11 @@ type OptimisticClosedReportAction = Pick< 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'originalMessage' | 'pendingAction' | 'person' | 'reportActionID' | 'shouldShow' >; +type OptimisticCardAssignedReportAction = Pick< + ReportAction, + 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'originalMessage' | 'pendingAction' | 'person' | 'reportActionID' | 'shouldShow' +>; + type OptimisticDismissedViolationReportAction = Pick< ReportAction, 'actionName' | 'actorAccountID' | 'avatar' | 'created' | 'message' | 'originalMessage' | 'person' | 'reportActionID' | 'shouldShow' | 'pendingAction' @@ -5629,6 +5634,27 @@ function buildOptimisticEditedTaskFieldReportAction({title, description}: Task): }; } +function buildOptimisticCardAssignedReportAction(assigneeAccountID: number): OptimisticCardAssignedReportAction { + return { + actionName: CONST.REPORT.ACTIONS.TYPE.CARD_ASSIGNED, + actorAccountID: currentUserAccountID, + avatar: getCurrentUserAvatar(), + created: DateUtils.getDBTime(), + originalMessage: {assigneeAccountID, cardID: -1}, + message: [{type: CONST.REPORT.MESSAGE.TYPE.COMMENT, text: '', html: ''}], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + person: [ + { + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + style: 'strong', + text: getCurrentUserDisplayNameOrEmail(), + }, + ], + reportActionID: NumberUtils.rand64(), + shouldShow: true, + }; +} + function buildOptimisticChangedTaskAssigneeReportAction(assigneeAccountID: number): OptimisticEditedTaskReportAction { const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail); @@ -8297,6 +8323,7 @@ export { buildOptimisticUnHoldReportAction, buildOptimisticAnnounceChat, buildOptimisticWorkspaceChats, + buildOptimisticCardAssignedReportAction, buildParticipantsFromAccountIDs, buildTransactionThread, canAccessReport, diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index c0de9510c6ba..2a0ab6defa12 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -20,7 +20,7 @@ import * as NetworkStore from '@libs/Network/NetworkStore'; import * as PolicyUtils from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Card} from '@src/types/onyx'; +import type {Card, CompanyCardFeed} from '@src/types/onyx'; import type {CardLimitType, ExpensifyCardDetails, IssueNewCardData, IssueNewCardStep} from '@src/types/onyx/Card'; import type {ConnectionName} from '@src/types/onyx/Policy'; @@ -717,7 +717,7 @@ function toggleContinuousReconciliation(workspaceAccountID: number, shouldUseCon }); } -function updateSelectedFeed(feed: string, policyID: string) { +function updateSelectedFeed(feed: CompanyCardFeed, policyID: string) { Onyx.update([ { onyxMethod: Onyx.METHOD.MERGE, diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 8baef3006a5b..c517439aeda2 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -9,6 +9,7 @@ import * as API from '@libs/API'; import type { AddBillingCardAndRequestWorkspaceOwnerChangeParams, AddPaymentCardParams, + AssignCompanyCardParams, CreateWorkspaceFromIOUPaymentParams, CreateWorkspaceParams, DeleteWorkspaceAvatarParams, @@ -26,6 +27,7 @@ import type { EnablePolicyWorkflowsParams, LeavePolicyParams, OpenDraftWorkspaceRequestParams, + OpenPolicyCompanyCardsFeedParams, OpenPolicyEditCardLimitTypePageParams, OpenPolicyExpensifyCardsPageParams, OpenPolicyInitialPageParams, @@ -36,6 +38,7 @@ import type { OpenWorkspaceInvitePageParams, OpenWorkspaceParams, RequestExpensifyCardLimitIncreaseParams, + RequestFeedSetupParams, SetCompanyCardExportAccountParams, SetPolicyAutomaticApprovalLimitParams, SetPolicyAutomaticApprovalRateParams, @@ -79,6 +82,7 @@ import * as PersistedRequests from '@userActions/PersistedRequests'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type { + CompanyCardFeed, InvitedEmailsToAccountIDs, PersonalDetailsList, Policy, @@ -90,6 +94,7 @@ import type { TaxRatesWithDefault, Transaction, } from '@src/types/onyx'; +import type {AssignCardData} from '@src/types/onyx/AssignCard'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {Attributes, CompanyAddress, CustomUnit, NetSuiteCustomList, NetSuiteCustomSegment, Rate, TaxRate} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; @@ -1999,6 +2004,15 @@ function openPolicyTaxesPage(policyID: string) { API.read(READ_COMMANDS.OPEN_POLICY_TAXES_PAGE, params); } +function openPolicyCompanyCardsFeed(policyID: string, feed: CompanyCardFeed) { + const parameters: OpenPolicyCompanyCardsFeedParams = { + policyID, + feed, + }; + + API.read(READ_COMMANDS.OPEN_POLICY_COMPANY_CARDS_FEED, parameters); +} + function openPolicyCompanyCardsPage(policyID: string, workspaceAccountID: number) { const authToken = NetworkStore.getAuthToken(); @@ -4421,6 +4435,23 @@ function enablePolicyAutoReimbursementLimit(policyID: string, enabled: boolean) }); } +function addNewCompanyCardsFeed(policyID: string, feedType: string, feedDetails: string) { + const authToken = NetworkStore.getAuthToken(); + + if (!authToken) { + return; + } + + const parameters: RequestFeedSetupParams = { + policyID, + authToken, + feedType, + feedDetails, + }; + + API.write(WRITE_COMMANDS.REQUEST_FEED_SETUP, parameters); +} + function setWorkspaceCompanyCardFeedName(policyID: string, workspaceAccountID: number, bankName: string, userDefinedName: string) { const authToken = NetworkStore.getAuthToken(); const onyxData: OnyxData = { @@ -4429,8 +4460,10 @@ function setWorkspaceCompanyCardFeedName(policyID: string, workspaceAccountID: n onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, value: { - companyCardNicknames: { - [bankName]: userDefinedName, + settings: { + companyCardNicknames: { + [bankName]: userDefinedName, + }, }, }, }, @@ -4447,7 +4480,7 @@ function setWorkspaceCompanyCardFeedName(policyID: string, workspaceAccountID: n API.write(WRITE_COMMANDS.SET_COMPANY_CARD_FEED_NAME, parameters, onyxData); } -function setWorkspaceCompanyCardTransactionLiability(workspaceAccountID: number, bankName: string, liabilityType: string) { +function setWorkspaceCompanyCardTransactionLiability(workspaceAccountID: number, policyID: string, bankName: string, liabilityType: string) { const authToken = NetworkStore.getAuthToken(); const onyxData: OnyxData = { optimisticData: [ @@ -4455,8 +4488,10 @@ function setWorkspaceCompanyCardTransactionLiability(workspaceAccountID: number, onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, value: { - companyCards: { - [bankName]: {liabilityType}, + settings: { + companyCards: { + [bankName]: {liabilityType}, + }, }, }, }, @@ -4465,6 +4500,7 @@ function setWorkspaceCompanyCardTransactionLiability(workspaceAccountID: number, const parameters = { authToken, + policyID, bankName, liabilityType, }; @@ -4481,11 +4517,13 @@ function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: nu onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, value: { - companyCards: { - [bankName]: null, - }, - companyCardNicknames: { - [bankName]: null, + settings: { + companyCards: { + [bankName]: null, + }, + companyCardNicknames: { + [bankName]: null, + }, }, }, }, @@ -4501,6 +4539,59 @@ function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: nu API.write(WRITE_COMMANDS.DELETE_COMPANY_CARD_FEED, parameters, onyxData); } +function assignWorkspaceCompanyCard(policyID: string, data?: Partial) { + if (!data) { + return; + } + const {bankName = '', email = '', encryptedCardNumber = '', startDate = ''} = data; + const assigneeDetails = PersonalDetailsUtils.getPersonalDetailByEmail(email); + const optimisticCardAssignedReportAction = ReportUtils.buildOptimisticCardAssignedReportAction(assigneeDetails?.accountID ?? -1); + + const parameters: AssignCompanyCardParams = { + policyID, + bankName, + encryptedCardNumber, + email, + startDate, + reportActionID: optimisticCardAssignedReportAction.reportActionID, + }; + const policy = getPolicy(policyID); + const policyExpenseChat = ReportUtils.getPolicyExpenseChat(policy?.ownerAccountID ?? -1, policyID); + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${policyExpenseChat?.reportID}`, + value: { + [optimisticCardAssignedReportAction.reportActionID]: optimisticCardAssignedReportAction, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${policyExpenseChat?.reportID}`, + value: {[optimisticCardAssignedReportAction.reportActionID]: {pendingAction: null}}, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${policyExpenseChat?.reportID}`, + value: { + [optimisticCardAssignedReportAction.reportActionID]: { + pendingAction: null, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + }, + ], + }; + + API.write(WRITE_COMMANDS.ASSIGN_COMPANY_CARD, parameters, onyxData); +} + function unassignWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, bankName: string) { const authToken = NetworkStore.getAuthToken(); @@ -4534,10 +4625,25 @@ function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, [cardID]: { isLoadingLastUpdated: true, pendingFields: { - lastUpdated: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + lastScrape: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + errorFields: { + lastScrape: null, + }, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { + isLoadingLastUpdated: true, + pendingFields: { + lastScrape: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, }, errorFields: { - lastUpdated: null, + lastScrape: null, }, }, }, @@ -4546,9 +4652,11 @@ function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, value: { - companyCards: { - [bankName]: { - errors: null, + settings: { + companyCards: { + [bankName]: { + errors: null, + }, }, }, }, @@ -4563,7 +4671,19 @@ function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, [cardID]: { isLoadingLastUpdated: false, pendingFields: { - lastUpdated: null, + lastScrape: null, + }, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { + isLoadingLastUpdated: false, + pendingFields: { + lastScrape: null, }, }, }, @@ -4578,10 +4698,25 @@ function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, [cardID]: { isLoadingLastUpdated: false, pendingFields: { - lastUpdated: null, + lastScrape: null, + }, + errorFields: { + lastScrape: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { + isLoadingLastUpdated: false, + pendingFields: { + lastScrape: null, }, errorFields: { - lastUpdated: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + lastScrape: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), }, }, }, @@ -4590,9 +4725,11 @@ function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, value: { - companyCards: { - [bankName]: { - errors: {error: CONST.COMPANY_CARDS.CONNECTION_ERROR}, + settings: { + companyCards: { + [bankName]: { + errors: {error: CONST.COMPANY_CARDS.CONNECTION_ERROR}, + }, }, }, }, @@ -4607,7 +4744,7 @@ function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, API.write(WRITE_COMMANDS.UPDATE_COMPANY_CARD, parameters, {optimisticData, finallyData, failureData}); } -function updateCompanyCardName(workspaceAccountID: number, cardID: string, newCardTitle: string, bankName: string) { +function updateCompanyCardName(workspaceAccountID: number, cardID: string, newCardTitle: string, bankName: string, oldCardTitle?: string) { const authToken = NetworkStore.getAuthToken(); const optimisticData: OnyxUpdate[] = [ @@ -4628,6 +4765,11 @@ function updateCompanyCardName(workspaceAccountID: number, cardID: string, newCa }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES, + value: {[cardID]: newCardTitle}, + }, ]; const finallyData: OnyxUpdate[] = [ @@ -4662,6 +4804,11 @@ function updateCompanyCardName(workspaceAccountID: number, cardID: string, newCa }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES, + value: {[cardID]: oldCardTitle}, + }, ]; const parameters: UpdateCompanyCardNameParams = { @@ -4863,6 +5010,9 @@ export { deleteWorkspaceCompanyCardFeed, setWorkspaceCompanyCardTransactionLiability, openPolicyCompanyCardsPage, + openPolicyCompanyCardsFeed, + addNewCompanyCardsFeed, + assignWorkspaceCompanyCard, unassignWorkspaceCompanyCard, updateWorkspaceCompanyCard, updateCompanyCardName, diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index cb9f95e5bb5e..c6f271a76de2 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -704,7 +704,13 @@ function ReportActionItem({ } else if (ReportActionsUtils.isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.REMOVED_FROM_APPROVAL_CHAIN)) { children = ; } else if ( - ReportActionsUtils.isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED, CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED_VIRTUAL, CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS) + ReportActionsUtils.isActionOfType( + action, + CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED, + CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED_VIRTUAL, + CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS, + CONST.REPORT.ACTIONS.TYPE.CARD_ASSIGNED, + ) ) { children = ( Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)))), routeName: SCREENS.WORKSPACE.COMPANY_CARDS, - brickRoadIndicator: PolicyUtils.hasPolicyFeedsError(cardFeeds?.companyCards ?? {}) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + brickRoadIndicator: PolicyUtils.hasPolicyFeedsError(cardFeeds?.settings?.companyCards ?? {}) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }); } diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 54d8401a99f8..0bc44a1d2298 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -126,7 +126,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro subtitleTranslationKey: 'workspace.moreFeatures.companyCards.subtitle', isActive: policy?.areCompanyCardsEnabled ?? false, pendingAction: policy?.pendingFields?.areCompanyCardsEnabled, - disabled: !isEmptyObject(cardFeeds?.companyCards), + disabled: !isEmptyObject(cardFeeds?.settings?.companyCards), action: (isEnabled: boolean) => { if (!policyID) { return; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx index df4e9635e851..726001e80146 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx @@ -38,6 +38,7 @@ type WorkspaceCompanyCardDetailsPageProps = StackScreenProps Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_NAME.getRoute(policyID, cardID, bank))} @@ -143,26 +144,26 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag /> } description={translate('workspace.moreFeatures.companyCards.lastUpdated')} - title={card?.isLoadingLastUpdated ? translate('workspace.moreFeatures.companyCards.updating') : card?.lastUpdated} + title={card?.isLoadingLastUpdated ? translate('workspace.moreFeatures.companyCards.updating') : card?.lastScrape} interactive={false} /> Policy.clearCompanyCardErrorField(workspaceAccountID, cardID, bank, 'lastUpdated', true)} + errors={ErrorUtils.getLatestErrorField(card ?? {}, 'lastScrape')} + onClose={() => Policy.clearCompanyCardErrorField(workspaceAccountID, cardID, bank, 'lastScrape', true)} > diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardEditCardNamePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardEditCardNamePage.tsx index c55d575838a8..56e8d88d8d54 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardEditCardNamePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardEditCardNamePage.tsx @@ -27,16 +27,15 @@ type WorkspaceCompanyCardEditCardNamePageProps = StackScreenProps) => { - Policy.updateCompanyCardName(workspaceAccountID, cardID, values[INPUT_IDS.NAME], bank); + Policy.updateCompanyCardName(workspaceAccountID, cardID, values[INPUT_IDS.NAME], bank, defaultValue); Navigation.goBack(); }; @@ -72,7 +71,7 @@ function WorkspaceCompanyCardEditCardNamePage({route}: WorkspaceCompanyCardEditC hint={translate('workspace.moreFeatures.companyCards.giveItNameInstruction')} aria-label={translate('workspace.moreFeatures.companyCards.cardName')} role={CONST.ROLE.PRESENTATION} - defaultValue={card?.nameValuePairs?.cardTitle} + defaultValue={defaultValue} maxLength={CONST.EXPENSIFY_CARD.CARD_TITLE_INPUT_LIMIT} ref={inputCallbackRef} /> diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx index 767f08068f95..2589f05e2769 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx @@ -13,64 +13,45 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CardUtils from '@libs/CardUtils'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import * as PolicyUtils from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import variables from '@styles/variables'; import * as Card from '@userActions/Card'; +import * as CompanyCards from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {CardFeeds} from '@src/types/onyx'; +import type {CompanyCardFeed} from '@src/types/onyx'; type CardFeedListItem = ListItem & { /** Card feed value */ - value: string; -}; - -const mockedData: CardFeeds = { - companyCards: { - cdfbmo: { - pending: false, - asrEnabled: true, - forceReimbursable: 'force_no', - liabilityType: 'corporate', - errors: {error: CONST.COMPANY_CARDS.CONNECTION_ERROR}, - preferredPolicy: '', - reportTitleFormat: '{report:card}{report:bank}{report:submit:from}{report:total}{report:enddate:MMMM}', - statementPeriodEndDay: 'LAST_DAY_OF_MONTH', - }, - }, - companyCardNicknames: { - cdfbmo: 'BMO MasterCard', - }, + value: CompanyCardFeed; }; type WorkspaceCompanyCardFeedSelectorPageProps = StackScreenProps; function WorkspaceCompanyCardFeedSelectorPage({route}: WorkspaceCompanyCardFeedSelectorPageProps) { const {policyID} = route.params; + const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyID); const {translate} = useLocalize(); const styles = useThemeStyles(); - // TODO: use data form onyx instead of mocked one when API is implemented - // const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); + const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`); + const selectedFeed = CardUtils.getSelectedFeed(lastSelectedFeed, cardFeeds); - const cardFeeds = mockedData; - const defaultFeed = Object.keys(cardFeeds?.companyCards ?? {}).at(0); - const selectedFeed = lastSelectedFeed ?? defaultFeed ?? ''; - - const feeds: CardFeedListItem[] = Object.entries(cardFeeds?.companyCardNicknames ?? {}).map(([key, value]) => ({ - value: key, - text: value, - keyForList: key, - isSelected: key === selectedFeed, - brickRoadIndicator: CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR, - canShowSeveralIndicators: !!cardFeeds?.companyCards?.[selectedFeed]?.errors, + const feeds: CardFeedListItem[] = Object.keys(cardFeeds?.settings?.companyCards ?? {}).map((feed) => ({ + value: feed as CompanyCardFeed, + text: cardFeeds?.settings?.companyCardNicknames?.[feed] ?? translate(`workspace.companyCards.addNewCard.cardProviders.${feed as CompanyCardFeed}`), + keyForList: feed, + isSelected: feed === selectedFeed, + brickRoadIndicator: cardFeeds?.settings?.companyCards?.[feed]?.errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + canShowSeveralIndicators: !!cardFeeds?.settings?.companyCards?.[feed]?.errors, leftElement: ( { - // TODO: navigate to Add Feed flow when it's implemented + CompanyCards.clearAddNewCardFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ADD_NEW.getRoute(policyID)); }} /> } diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsList.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsList.tsx index c9c01592304a..fd2c73ddb402 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsList.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsList.tsx @@ -1,12 +1,13 @@ import React, {useCallback, useMemo} from 'react'; import type {ListRenderItemInfo} from 'react-native'; -import {FlatList, View} from 'react-native'; +import {ActivityIndicator, FlatList, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {PressableWithFeedback} from '@components/Pressable'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CardUtils from '@libs/CardUtils'; import Navigation from '@navigation/Navigation'; @@ -27,8 +28,10 @@ type WorkspaceCompanyCardsListProps = { function WorkspaceCompanyCardsList({cardsList, policyID}: WorkspaceCompanyCardsListProps) { const styles = useThemeStyles(); + const theme = useTheme(); const {translate} = useLocalize(); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); + const [customCardNames] = useOnyx(ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES); const sortedCards = useMemo(() => CardUtils.sortCardsByCardholderName(cardsList, personalDetails), [cardsList, personalDetails]); @@ -55,14 +58,14 @@ function WorkspaceCompanyCardsList({cardsList, policyID}: WorkspaceCompanyCardsL > ); }, - [cardsList, personalDetails, policyID, styles], + [cardsList, customCardNames, personalDetails, policyID, styles], ); const renderListHeader = useCallback( @@ -85,6 +88,16 @@ function WorkspaceCompanyCardsList({cardsList, policyID}: WorkspaceCompanyCardsL [styles, translate], ); + if (!cardsList) { + return ( + + ); + } + if (sortedCards.length === 0) { return ; } diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx index 320eb71247cb..74e6593d6986 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx @@ -18,13 +18,14 @@ import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {CompanyCardFeed} from '@src/types/onyx'; type WorkspaceCompanyCardsListHeaderButtonsProps = { /** Current policy id */ policyID: string; /** Currently selected feed */ - selectedFeed: string; + selectedFeed: CompanyCardFeed; }; function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed}: WorkspaceCompanyCardsListHeaderButtonsProps) { @@ -35,10 +36,11 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed}: Worksp const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyID); const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); const shouldChangeLayout = isMediumScreenWidth || shouldUseNarrowLayout; + const feedName = cardFeeds?.settings?.companyCardNicknames?.[selectedFeed] ?? translate(`workspace.companyCards.addNewCard.cardProviders.${selectedFeed}`); return ( @@ -46,7 +48,7 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed}: Worksp Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.getRoute(policyID))} style={[styles.flexRow, styles.alignItemsCenter, styles.gap3, shouldChangeLayout && styles.mb3]} - accessibilityLabel={cardFeeds?.companyCardNicknames?.[selectedFeed] ?? ''} + accessibilityLabel={feedName} > - {cardFeeds?.companyCardNicknames?.[selectedFeed]} + {feedName} - {PolicyUtils.hasPolicyFeedsError(cardFeeds?.companyCards ?? {}, selectedFeed) && ( + {PolicyUtils.hasPolicyFeedsError(cardFeeds?.settings?.companyCards ?? {}, selectedFeed) && (