Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,7 @@ const CONST = {
BETAS: {
ALL: 'all',
ASAP_SUBMIT: 'asapSubmit',
CSV_CARD_IMPORT: 'csvCardImport',
DEFAULT_ROOMS: 'defaultRooms',
PREVENT_SPOTNANA_TRAVEL: 'preventSpotnanaTravel',
REPORT_FIELDS_FEATURE: 'reportFieldsFeature',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2188,6 +2188,9 @@ const translations: TranslationDeepObject<typeof en> = {
unshareBankAccountWarning: ({admin}: {admin?: string | null}) => `${admin} verliert den Zugriff auf dieses Geschäftskonto. Laufende Zahlungen werden weiterhin ausgeführt.`,
reachOutForHelp: 'Dieses Konto wird mit der Expensify Card verwendet. <concierge-link>Wenden Sie sich an den Concierge</concierge-link>, wenn Sie die Freigabe aufheben möchten.',
unshareErrorModalTitle: 'Bankkonto kann nicht freigegeben werden',
deleteCard: 'Karte löschen',
deleteCardConfirmation:
'Alle nicht eingereichten Kartentransaktionen, einschließlich der auf offenen Berichten, werden entfernt. Möchten Sie diese Karte wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.',
},
cardPage: {
expensifyCard: 'Expensify Card',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2177,6 +2177,9 @@ const translations = {
setDefaultSuccess: 'Default payment method set!',
deleteAccount: 'Delete account',
deleteConfirmation: 'Are you sure you want to delete this account?',
deleteCard: 'Delete card',
deleteCardConfirmation:
'All unsubmitted card transactions, including those on open reports, will be removed. Are you sure you want to delete this card? You cannot undo this action.',
error: {
notOwnerOfBankAccount: 'An error occurred while setting this bank account as your default payment method',
invalidBankAccount: 'This bank account is temporarily suspended',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1906,6 +1906,9 @@ const translations: TranslationDeepObject<typeof en> = {
setDefaultSuccess: 'Método de pago configurado',
deleteAccount: 'Eliminar cuenta',
deleteConfirmation: '¿Estás seguro de que quieres eliminar esta cuenta?',
deleteCard: 'Eliminar tarjeta',
deleteCardConfirmation:
'Todas las transacciones no enviadas, incluidas las de informes abiertos, serán eliminadas. ¿Estás seguro de que quieres eliminar esta tarjeta? Esta acción no se puede deshacer.',
error: {
notOwnerOfBankAccount: 'Se ha producido un error al establecer esta cuenta bancaria como método de pago predeterminado',
invalidBankAccount: 'Esta cuenta bancaria está temporalmente suspendida',
Expand Down
5 changes: 4 additions & 1 deletion src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2194,6 +2194,9 @@ const translations: TranslationDeepObject<typeof en> = {
unshareBankAccountWarning: ({admin}: {admin?: string | null}) => `${admin} perdra l’accès à ce compte bancaire professionnel. Les paiements en cours seront honorés.`,
reachOutForHelp: 'Ce compte est utilisé avec la carte Expensify. <concierge-link>Contactez le service de conciergerie</concierge-link> si vous souhaitez le retirer du partage.',
unshareErrorModalTitle: 'Impossible de retirer le partage du compte bancaire',
deleteCard: 'Supprimer la carte',
deleteCardConfirmation:
'Toutes les transactions de carte non soumises, y compris celles figurant dans les rapports ouverts, seront supprimées. Êtes-vous sûr de vouloir supprimer cette carte ? Cette action est irréversible.',
},
cardPage: {
expensifyCard: 'Carte Expensify',
Expand Down Expand Up @@ -7042,7 +7045,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
[CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'ID de retrait',
[CONST.SEARCH.GROUP_BY.CATEGORY]: 'Catégorie',
[CONST.SEARCH.GROUP_BY.MERCHANT]: 'Commerçant',
[CONST.SEARCH.GROUP_BY.TAG]: 'Étiquette',
[CONST.SEARCH.GROUP_BY.TAG]: 'Tag',
[CONST.SEARCH.GROUP_BY.MONTH]: 'Mois',
[CONST.SEARCH.GROUP_BY.WEEK]: 'Semaine',
[CONST.SEARCH.GROUP_BY.YEAR]: 'Année',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2185,6 +2185,9 @@ const translations: TranslationDeepObject<typeof en> = {
unshareBankAccountWarning: ({admin}: {admin?: string | null}) => `${admin} perderà l'accesso a questo conto bancario aziendale. Completeremo comunque tutti i pagamenti in corso.`,
reachOutForHelp: 'È in uso con la carta Expensify. <concierge-link>Contatta il Concierge</concierge-link> se devi revocare la condivisione.',
unshareErrorModalTitle: 'Impossibile revocare la condivisione del conto bancario',
deleteCard: 'Elimina carta',
deleteCardConfirmation:
'Tutte le transazioni con carta non inviate, comprese quelle nei report aperti, verranno rimosse. Sei sicuro di voler eliminare questa carta? Non puoi annullare questa azione.',
},
cardPage: {
expensifyCard: 'Carta Expensify',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2179,6 +2179,8 @@ const translations: TranslationDeepObject<typeof en> = {
unshareBankAccountWarning: ({admin}: {admin?: string | null}) => `${admin} はこのビジネス銀行口座にアクセスできなくなります。処理中のお支払いは引き続き完了します。`,
reachOutForHelp: 'この口座は Expensify カードで使用されています。共有を解除する必要がある場合は、<concierge-link>コンシェルジュまでお問い合わせください</concierge-link>。',
unshareErrorModalTitle: '銀行口座の共有を解除できません',
deleteCard: 'カードを削除',
deleteCardConfirmation: '未提出のカード取引(未精算レポート上の取引も含む)はすべて削除されます。このカードを本当に削除してもよろしいですか?この操作は元に戻せません。',
},
cardPage: {
expensifyCard: 'Expensify Card',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2184,6 +2184,9 @@ const translations: TranslationDeepObject<typeof en> = {
`${admin} verliest de toegang tot deze zakelijke bankrekening. We zullen alle betalingen die in behandeling zijn nog steeds voltooien.`,
reachOutForHelp: 'Deze wordt gebruikt met de Expensify Card. <concierge-link>Neem contact op met Concierge</concierge-link> als u de deling ongedaan wilt maken.',
unshareErrorModalTitle: 'Deling bankrekening kan niet ongedaan worden gemaakt',
deleteCard: 'Kaart verwijderen',
deleteCardConfirmation:
'Alle niet-ingediende kaarttransacties, inclusief die in openstaande rapporten, worden verwijderd. Weet je zeker dat je deze kaart wilt verwijderen? Je kunt deze actie niet ongedaan maken.',
},
cardPage: {
expensifyCard: 'Expensify Card',
Expand Down
5 changes: 4 additions & 1 deletion src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2181,6 +2181,9 @@ const translations: TranslationDeepObject<typeof en> = {
unshareBankAccountWarning: ({admin}: {admin?: string | null}) => `${admin} utraci dostęp do tego firmowego konta bankowego. Nadal będziemy realizować wszystkie płatności w toku.`,
reachOutForHelp: 'Jest ono używane z kartą Expensify. <concierge-link>Skontaktuj się z Concierge</concierge-link>, jeśli chcesz je anulować.',
unshareErrorModalTitle: 'Nie można anulować udostępniania konta bankowego',
deleteCard: 'Usuń kartę',
deleteCardConfirmation:
'Wszystkie niewysłane transakcje kartą, w tym znajdujące się w otwartych raportach, zostaną usunięte. Czy na pewno chcesz usunąć tę kartę? Nie można cofnąć tej czynności.',
},
cardPage: {
expensifyCard: 'Karta Expensify',
Expand Down Expand Up @@ -6987,7 +6990,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i
groupBy: {
[CONST.SEARCH.GROUP_BY.FROM]: 'Od',
[CONST.SEARCH.GROUP_BY.CARD]: 'Karta',
[CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'ID wypłaty',
[CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'Identyfikator wypłaty',
[CONST.SEARCH.GROUP_BY.CATEGORY]: 'Kategoria',
[CONST.SEARCH.GROUP_BY.MERCHANT]: 'Sprzedawca',
[CONST.SEARCH.GROUP_BY.TAG]: 'Tag',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2179,6 +2179,9 @@ const translations: TranslationDeepObject<typeof en> = {
unshareBankAccountWarning: ({admin}: {admin?: string | null}) => `${admin} perderá o acesso a esta conta bancária comercial. Ainda concluiremos quaisquer pagamentos em andamento.`,
reachOutForHelp: 'Está sendo usada com o Cartão Expensify. <concierge-link>Entre em contato com o Concierge</concierge-link> se precisar descompartilhar.',
unshareErrorModalTitle: 'Não foi possível descompartilhar a conta bancária',
deleteCard: 'Excluir cartão',
deleteCardConfirmation:
'Todas as transações de cartão não enviadas, incluindo aquelas em relatórios em aberto, serão removidas. Tem certeza de que deseja excluir este cartão? Esta ação não pode ser desfeita.',
},
cardPage: {
expensifyCard: 'Cartão Expensify',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2152,6 +2152,8 @@ const translations: TranslationDeepObject<typeof en> = {
unshareBankAccountWarning: ({admin}: {admin?: string | null}) => `${admin} 将失去对此企业银行账户的访问权限。我们仍将完成所有正在进行的付款。`,
reachOutForHelp: '此账户正在与 Expensify 卡一起使用。如果您需要取消共享,请联系礼宾部。',
unshareErrorModalTitle: '无法取消共享银行账户',
deleteCard: '删除卡片',
deleteCardConfirmation: '所有未提交的银行卡交易(包括开放报表中的交易)都将被移除。确定要删除此银行卡吗?此操作无法撤销。',
},
cardPage: {
expensifyCard: 'Expensify Card',
Expand Down
5 changes: 5 additions & 0 deletions src/libs/API/parameters/DeletePersonalCardParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type DeletePersonalCardParams = {
cardID: number;
};

export default DeletePersonalCardParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type {default as SyncPolicyToQuickbooksDesktopParams} from './SyncPolicyT
export type {default as DeleteContactMethodParams} from './DeleteContactMethodParams';
export type {default as DeletePaymentBankAccountParams} from './DeletePaymentBankAccountParams';
export type {default as DeletePaymentCardParams} from './DeletePaymentCardParams';
export type {default as DeletePersonalCardParams} from './DeletePersonalCardParams';
export type {default as TogglePolicyUberAutoInvitePageParams} from './TogglePolicyUberAutoInvitePageParams';
export type {default as DismissReferralBannerParams} from './DismissReferralBannerParams';
export type {default as ExpandURLPreviewParams} from './ExpandURLPreviewParams';
Expand Down
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const WRITE_COMMANDS = {
UPDATE_EXPENSIFY_CARD_TITLE: 'UpdateExpensifyCardTitle',
UPDATE_EXPENSIFY_CARD_LIMIT_TYPE: 'UpdateExpensifyCardLimitType',
CARD_DEACTIVATE: 'Card_Deactivate',
DELETE_PERSONAL_CARD: 'DeleteCard',
CHRONOS_REMOVE_OOO_EVENT: 'Chronos_RemoveOOOEvent',
MAKE_DEFAULT_PAYMENT_METHOD: 'MakeDefaultPaymentMethod',
TOGGLE_WORKSPACE_UBER_AUTO_INVITE: 'ToggleWorkspaceUberAutoInvite',
Expand Down Expand Up @@ -572,6 +573,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_EXPENSIFY_CARD_TITLE]: Parameters.UpdateExpensifyCardTitleParams;
[WRITE_COMMANDS.UPDATE_EXPENSIFY_CARD_LIMIT_TYPE]: Parameters.UpdateExpensifyCardLimitTypeParams;
[WRITE_COMMANDS.CARD_DEACTIVATE]: Parameters.CardDeactivateParams;
[WRITE_COMMANDS.DELETE_PERSONAL_CARD]: Parameters.DeletePersonalCardParams;
[WRITE_COMMANDS.MAKE_DEFAULT_PAYMENT_METHOD]: Parameters.MakeDefaultPaymentMethodParams;
[WRITE_COMMANDS.TOGGLE_WORKSPACE_UBER_AUTO_INVITE]: Parameters.TogglePolicyUberAutoInvitePageParams;
[WRITE_COMMANDS.SET_WORKSPACE_UBER_CENTRAL_BILL]: Parameters.ChangePolicyUberBillingAccountPageParams;
Expand Down
22 changes: 22 additions & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2002,6 +2002,27 @@ function isOpenReport(report: OnyxEntry<Report>): boolean {
return report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS_NUM.OPEN;
}

/**
* Checks if a report is in an open/unsubmitted state where its transactions can be deleted.
* Returns true for:
* - Undefined or empty reportID
* - UNREPORTED_REPORT_ID (transactions not yet on a report)
* - Reports that don't exist in the collection
* - Reports with stateNum === OPEN
* This matches the backend logic in Card::remove which deletes transactions on open reports.
*/
function isReportOpenOrUnsubmitted(reportID: string | undefined, reports: OnyxCollection<Report>): boolean {
if (!reportID || reportID === CONST.REPORT.UNREPORTED_REPORT_ID) {
return true;
}
const report = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
if (!report) {
return true;
}
// Backend deletes transactions on reports with state = STATE_OPEN (0)
return report.stateNum === CONST.REPORT.STATE_NUM.OPEN;
}

/**
* Determines if a report requires manual submission based on policy settings and report state
*/
Expand Down Expand Up @@ -13418,6 +13439,7 @@ export {
isTrackExpenseReportNew,
shouldHideSingleReportField,
getReportForHeader,
isReportOpenOrUnsubmitted,
};

export type {
Expand Down
75 changes: 73 additions & 2 deletions src/libs/actions/Card.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import Onyx from 'react-native-onyx';
import type {OnyxUpdate} from 'react-native-onyx';
import type {OnyxCollection, OnyxUpdate} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import * as API from '@libs/API';
import type {
ActivatePhysicalExpensifyCardParams,
CardDeactivateParams,
DeletePersonalCardParams,
OpenCardDetailsPageParams,
ReportVirtualExpensifyCardFraudParams,
RequestReplacementExpensifyCardParams,
Expand All @@ -22,9 +23,10 @@ import type {
import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import Log from '@libs/Log';
import {isReportOpenOrUnsubmitted} from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Card, CompanyCardFeedWithDomainID} from '@src/types/onyx';
import type {Card, CompanyCardFeedWithDomainID, Report, Transaction} from '@src/types/onyx';
import type {CardLimitType, ExpensifyCardDetails, IssueNewCardData, IssueNewCardStep} from '@src/types/onyx/Card';
import type {ConnectionName} from '@src/types/onyx/Policy';

Expand Down Expand Up @@ -1022,6 +1024,74 @@ function deactivateCard(workspaceAccountID: number, card?: Card) {
API.write(WRITE_COMMANDS.CARD_DEACTIVATE, parameters, {optimisticData, failureData});
}

type DeletePersonalCardData = {
cardID: number;
card?: Card;
allTransactions: OnyxCollection<Transaction>;
allReports: OnyxCollection<Report>;
};

/**
* Deletes a personal card (CSV-imported card) and its associated transactions.
* The backend will handle deleting transactions on unsubmitted/open reports.
*/
function deletePersonalCard({cardID, card, allTransactions, allReports}: DeletePersonalCardData) {
// Find all transactions associated with this card that are on open/unsubmitted reports
// This matches the backend logic which only deletes transactions on open reports
const transactionsToDelete: Transaction[] = [];
for (const transaction of Object.values(allTransactions ?? {})) {
if (transaction?.cardID === cardID && isReportOpenOrUnsubmitted(transaction.reportID, allReports)) {
transactionsToDelete.push(transaction);
}
}

// Optimistically remove the card immediately for instant UI feedback
const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.CARD_LIST | typeof ONYXKEYS.COLLECTION.TRANSACTION>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.CARD_LIST,
value: {
[cardID]: null,
},
},
];

const failureData: Array<OnyxUpdate<typeof ONYXKEYS.CARD_LIST | typeof ONYXKEYS.COLLECTION.TRANSACTION>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.CARD_LIST,
value: {
[cardID]: {
...card,
pendingAction: null,
errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
},
},
},
];

// Optimistically delete transactions and prepare failure data to restore them
for (const transaction of transactionsToDelete) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
value: null,
});

failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
value: transaction,
});
}

const parameters: DeletePersonalCardParams = {
cardID,
};

API.write(WRITE_COMMANDS.DELETE_PERSONAL_CARD, parameters, {optimisticData, failureData});
}

function startIssueNewCardFlow(policyID: string | undefined) {
const parameters: StartIssueNewCardFlowParams = {
policyID,
Expand Down Expand Up @@ -1389,6 +1459,7 @@ export {
updateSelectedFeed,
updateSelectedExpensifyCardFeed,
deactivateCard,
deletePersonalCard,
getCardDefaultName,
queueExpensifyCardForBilling,
clearIssueNewCardFormData,
Expand Down
33 changes: 17 additions & 16 deletions src/pages/settings/Wallet/PaymentMethodList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,22 +253,23 @@ function PaymentMethodList({
} else {
cardDescription = getDescriptionForPolicyDomainCard(card.domainName, policiesForAssignedCards);
}
// Personal cards navigate to personal card details page
// Personal cards navigate to personal card details page (except CSV cards which need 3-dot menu for delete)
// Company cards use the pressHandler callback (for 3-dot menu behavior)
const cardOnPress = isUserPersonalCard
? () => Navigation.navigate(ROUTES.SETTINGS_WALLET_PERSONAL_CARD_DETAILS.getRoute(String(card.cardID)))
: (e: GestureResponderEvent | KeyboardEvent | undefined) =>
pressHandler({
event: e,
cardData: card,
icon: {
icon,
iconStyles: [styles.cardIcon],
iconWidth: variables.cardIconWidth,
iconHeight: variables.cardIconHeight,
},
cardID: card.cardID,
});
const cardOnPress =
isUserPersonalCard && !isCSVCard
? () => Navigation.navigate(ROUTES.SETTINGS_WALLET_PERSONAL_CARD_DETAILS.getRoute(String(card.cardID)))
: (e: GestureResponderEvent | KeyboardEvent | undefined) =>
pressHandler({
event: e,
cardData: card,
icon: {
icon,
iconStyles: [styles.cardIcon],
iconWidth: variables.cardIconWidth,
iconHeight: variables.cardIconHeight,
},
cardID: card.cardID,
});

assignedCardsGrouped.push({
key: card.cardID.toString(),
Expand All @@ -278,7 +279,7 @@ function PaymentMethodList({
interactive: !isDisabled,
disabled: isDisabled,
shouldShowRightIcon,
shouldShowThreeDotsMenu: !isUserPersonalCard,
shouldShowThreeDotsMenu: !isUserPersonalCard || isCSVCard,
errors: card.errors,
canDismissError: false,
pendingAction: card.pendingAction,
Expand Down
Loading
Loading