From da0b549b9251b3f14ee5745c8db010de1a6bf6e1 Mon Sep 17 00:00:00 2001 From: Scott Deeter Date: Thu, 30 Jul 2026 14:26:02 -0700 Subject: [PATCH] Revert "fix: block removing a workspace member who holds an active Expensify Card" This reverts the merge of https://github.com/Expensify/App/pull/96267 (commit 34339030c889edaa035d0454f2f4485735cd76ed) to unblock the 9.4.46 deploy. The Expensify Card removal guard is derived from workspace card data in Onyx that the Members flow never fetches. When that data is absent, getAllCardsForWorkspace returns an empty list and the guard evaluates to false, so it silently fails to block instead of showing the modal. Conflicts resolved in WorkspaceMembersPage and WorkspaceMemberDetailsPage to preserve the RuleBot guard and getReimburserEmail changes that landed after the reverted PR. Co-authored-by: Claude --- src/languages/de.ts | 2 - src/languages/en.ts | 2 - src/languages/es.ts | 2 - src/languages/fr.ts | 2 - src/languages/it.ts | 2 - src/languages/ja.ts | 2 - src/languages/nl.ts | 2 - src/languages/pl.ts | 2 - src/languages/pt-BR.ts | 2 - src/languages/zh-hans.ts | 2 - src/libs/CardUtils.ts | 8 --- src/pages/workspace/WorkspaceMembersPage.tsx | 33 +----------- .../members/WorkspaceMemberDetailsPage.tsx | 19 ++----- tests/unit/CardUtilsTest.ts | 51 ------------------- 14 files changed, 4 insertions(+), 127 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 457bd54531c5..7f0ce695acb5 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6670,8 +6670,6 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`, peopleAdmins: 'Personaladministratoren', paymentsAdmins: 'Zahlungsadmins', members: 'Mitglieder', - removeMemberPromptExpensifyCard: ({memberName}: {memberName: string}) => - `Sie können ${memberName} nicht aus diesem Workspace entfernen, solange diese Person eine Expensify Karte hat. Bitte deaktivieren Sie die Karte unter Workspace > Expensify Karte und versuchen Sie es dann erneut.`, }, card: { getStartedIssuing: 'Beginne, indem du deine erste virtuelle oder physische Karte ausstellst.', diff --git a/src/languages/en.ts b/src/languages/en.ts index 50cad54d7e0f..0ba06c98eb8e 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6768,8 +6768,6 @@ const translations = { `${memberName} has outstanding expense reports to approve. Please ask them to approve, or take control of their reports before removing them from the workspace.`, removeMemberPromptReimburser: ({memberName}: {memberName: string}) => `You can't remove ${memberName} from this workspace. Please set a new reimburser in Workflows > Make or track payments, then try again.`, - removeMemberPromptExpensifyCard: ({memberName}: {memberName: string}) => - `You can't remove ${memberName} from this workspace while they have an Expensify Card. Please deactivate their card in Workspace > Expensify Card, then try again.`, removeMemberPromptExporter: ({memberName, workspaceOwner}: {memberName: string; workspaceOwner: string}) => `If you remove ${memberName} from this workspace, we'll replace them as the preferred exporter with ${workspaceOwner}, the workspace owner.`, removeMemberPromptTechContact: ({memberName, workspaceOwner}: {memberName: string; workspaceOwner: string}) => diff --git a/src/languages/es.ts b/src/languages/es.ts index 41a3a30c24e9..c8e3506b0a54 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6512,8 +6512,6 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, peopleAdmins: 'Administradores de personas', paymentsAdmins: 'Administradores de pagos', members: 'Miembros', - removeMemberPromptExpensifyCard: ({memberName}: {memberName: string}) => - `No puedes eliminar a ${memberName} de este espacio de trabajo mientras tenga una Tarjeta Expensify. Desactiva su tarjeta en Espacio de trabajo > Tarjeta Expensify y vuelve a intentarlo.`, }, accounting: { settings: 'configuración', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index e246c48104e2..6cc611c60e2b 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -6695,8 +6695,6 @@ Le forfait Control commence à 9 $ par Membre actif et par mois.`, peopleAdmins: 'Administrateurs des personnes', paymentsAdmins: 'Administrateurs des paiements', members: 'Membres', - removeMemberPromptExpensifyCard: ({memberName}: {memberName: string}) => - `Vous ne pouvez pas supprimer ${memberName} de cet espace de travail tant qu’iel possède une Carte Expensify. Veuillez désactiver sa carte dans Espace de travail > Carte Expensify, puis réessayer.`, }, card: { getStartedIssuing: 'Commencez par émettre votre première carte virtuelle ou physique.', diff --git a/src/languages/it.ts b/src/languages/it.ts index 795cecbd7832..f3f7053757fa 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6646,8 +6646,6 @@ Il piano Control parte da 9 $ al mese per ogni membro attivo.`, peopleAdmins: 'Amministratori persone', paymentsAdmins: 'Amministratori pagamenti', members: 'Membri', - removeMemberPromptExpensifyCard: ({memberName}: {memberName: string}) => - `Non puoi rimuovere ${memberName} da questo spazio di lavoro finché ha una Carta Expensify. Disattiva prima la sua carta in Spazio di lavoro > Carta Expensify, quindi riprova.`, }, card: { getStartedIssuing: 'Inizia emettendo la tua prima carta virtuale o fisica.', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 49a993fabdc0..989b2a72bac8 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6566,8 +6566,6 @@ Control プランは、アクティブメンバー1人あたり月額 $9 から peopleAdmins: 'People 管理者', paymentsAdmins: '支払い管理者', members: 'メンバー', - removeMemberPromptExpensifyCard: ({memberName}: {memberName: string}) => - `${memberName}さんはExpensify カードをお持ちの間、このワークスペースから削除できません。ワークスペース > Expensify カードでカードを無効化してから、もう一度お試しください。`, }, card: { getStartedIssuing: 'まずは最初のバーチャルカードまたは物理カードを発行しましょう。', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 67a53d6ca2e9..a2d65224aba6 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6632,8 +6632,6 @@ Het Control-abonnement begint bij $9 per actieve deelnemer per maand.`, peopleAdmins: 'People-beheerders', paymentsAdmins: 'Betalingsbeheerders', members: 'Leden', - removeMemberPromptExpensifyCard: ({memberName}: {memberName: string}) => - `Je kunt ${memberName} niet uit deze werkruimte verwijderen zolang diegene een Expensify Kaart heeft. Deactiveer eerst hun kaart in Werkruimte > Expensify Kaart en probeer het daarna opnieuw.`, }, card: { getStartedIssuing: 'Begin met het uitgeven van je eerste virtuele of fysieke kaart.', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index d331e83895e3..dc54a747f85f 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -6613,8 +6613,6 @@ Plan Control zaczyna się od 9 USD za aktywnego członka miesięcznie.`, peopleAdmins: 'Administratorzy osób', paymentsAdmins: 'Administratorzy płatności', members: 'Członkowie', - removeMemberPromptExpensifyCard: ({memberName}: {memberName: string}) => - `Nie możesz usunąć użytkownika ${memberName} z tego workspace, dopóki ma Kartę Expensify. Dezaktywuj jego kartę w Workspace > Karta Expensify, a następnie spróbuj ponownie.`, }, card: { getStartedIssuing: 'Zacznij od wydania swojej pierwszej wirtualnej lub fizycznej karty.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 83961d99247d..05321b48ece5 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6630,8 +6630,6 @@ O plano Control começa em US$ 9 por membro ativo por mês.`, peopleAdmins: 'Administradores de pessoas', paymentsAdmins: 'Administradores de pagamentos', members: 'Membros', - removeMemberPromptExpensifyCard: ({memberName}: {memberName: string}) => - `Você não pode remover ${memberName} deste workspace enquanto essa pessoa tiver um Cartão Expensify. Desative o cartão em Workspace > Cartão Expensify e tente novamente.`, }, card: { getStartedIssuing: 'Comece emitindo seu primeiro cartão virtual ou físico.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 798328f37cb8..a2442f05aa36 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6417,8 +6417,6 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM peopleAdmins: '人员管理员', paymentsAdmins: '付款管理员', members: '成员', - removeMemberPromptExpensifyCard: ({memberName}: {memberName: string}) => - `在 ${memberName} 仍持有 Expensify 卡时,您无法将其从此工作区中移除。请在“工作区 > Expensify 卡”中停用其卡片后重试。`, }, card: { getStartedIssuing: '从发放您的第一张虚拟卡或实体卡开始使用。', diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 3619415ecda9..1621f1241225 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1293,13 +1293,6 @@ function isSmartLimitEnabled(cardsList: CardList) { return hasAssignedCardMatching(cardsList, (card) => card.nameValuePairs?.limitType === CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART); } -function hasActiveExpensifyCardAssigned(workspaceCards: CardList | undefined, accountID: number): boolean { - return hasAssignedCardMatching( - workspaceCards, - (card) => card.accountID === accountID && card.bank === CONST.EXPENSIFY_CARD.BANK && !isTravelCard(card) && card.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - ); -} - const CUSTOM_FEEDS = [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD, CONST.COMPANY_CARD.FEED_BANK_NAME.VISA, CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX, CONST.COMPANY_CARD.FEED_BANK_NAME.CSV]; function collectUsedCSVFeedSlotNumbersFromCompanyCards(companyCards: CompanyFeeds | undefined, csvPrefix: string): number[] { @@ -2147,7 +2140,6 @@ export { getDomainByFundID, isPolicyIDInLinkedExpensifyCardPolicyList, filterAllInactiveCards, - hasActiveExpensifyCardAssigned, hasAssignedCardMatching, forEachAssignedCard, isActiveCard, diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index da88775c9cdc..cafa5b984e40 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -13,10 +13,8 @@ import Text from '@components/Text'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import TextLink from '@components/TextLink'; -import useCardFeeds from '@hooks/useCardFeeds'; import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; import useHRSyncResultsModal from '@hooks/useHRSyncResultsModal'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -46,7 +44,6 @@ import { } from '@libs/actions/Policy/Member'; import {removeApprovalWorkflow as removeApprovalWorkflowAction, updateApprovalWorkflow} from '@libs/actions/Workflow'; import {isRuleBotEnforcingRules} from '@libs/AgentRulesUtils'; -import {getAllCardsForWorkspace, hasActiveExpensifyCardAssigned} from '@libs/CardUtils'; import {getLatestErrorMessageField} from '@libs/ErrorUtils'; import {getConnectedHRProvider, showMergeHRManualSyncLimitModalIfReached} from '@libs/HRUtils'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; @@ -145,13 +142,6 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers const isFocused = useIsFocused(); const policyID = route.params.policyID; const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`); - const [cardFeeds] = useCardFeeds(policyID); - const [cardList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}`); - const expensifyCardSettings = useExpensifyCardFeeds(policyID); - const workspaceCards = useMemo( - () => getAllCardsForWorkspace(policy?.policyAccountID ?? CONST.DEFAULT_NUMBER_ID, cardList, cardFeeds, expensifyCardSettings), - [policy?.policyAccountID, cardList, cardFeeds, expensifyCardSettings], - ); const [invitedEmailsToAccountIDsDraft] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${policyID}`); const illustrations = useMemoizedLazyIllustrations(['ReceiptWrangler', 'EmptyShelves']); @@ -172,16 +162,6 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers const canSelectMultiple = canWriteMembers && (shouldUseNarrowLayout ? isMobileSelectionModeEnabled : true); - const blockedCardholderName = useMemo(() => { - const cardholderEmail = selectedEmployees.find((selectedEmployee) => hasActiveExpensifyCardAssigned(workspaceCards, policyMemberEmailsToAccountIDs[selectedEmployee])); - - if (!cardholderEmail) { - return; - } - - return getDisplayNameForParticipant({accountID: policyMemberEmailsToAccountIDs[cardholderEmail], formatPhoneNumber, translate}); - }, [selectedEmployees, workspaceCards, policyMemberEmailsToAccountIDs, formatPhoneNumber, translate]); - const confirmModalPrompt = useMemo(() => { const approverEmail = selectedEmployees.find((selectedEmployee) => isPolicyApprover(policy, selectedEmployee)); @@ -295,17 +275,6 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers showRuleBotGuardModal('remove', policyID); return; } - if (blockedCardholderName) { - showConfirmModal({ - shouldShowCancelButton: false, - success: true, - title: translate('workspace.people.removeMembersTitle', {count: selectedEmployees.length}), - prompt: translate('workspace.people.removeMemberPromptExpensifyCard', {memberName: blockedCardholderName}), - confirmText: translate('common.buttonConfirm'), - cancelText: translate('common.cancel'), - }); - return; - } showConfirmModal({ danger: true, @@ -326,7 +295,7 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers removeUsers(); }); - }, [blockedCardholderName, confirmModalPrompt, removeUsers, selectedEmployees, policyMemberEmailsToAccountIDs, policy, policyID, showConfirmModal, showRuleBotGuardModal, translate]); + }, [confirmModalPrompt, removeUsers, selectedEmployees, policyMemberEmailsToAccountIDs, policy, policyID, showConfirmModal, showRuleBotGuardModal, translate]); /** Opens the member details page */ const openMemberDetails = useCallback( diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index e502b7e3770d..35f4a5fea0da 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -30,15 +30,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {setPolicyPreventSelfApproval} from '@libs/actions/Policy/Policy'; import {removeApprovalWorkflow as removeApprovalWorkflowAction, updateApprovalWorkflow} from '@libs/actions/Workflow'; import {isRuleBotEnforcingRules} from '@libs/AgentRulesUtils'; -import { - getAllCardsForWorkspace, - getCardFeedIcon, - getCardFeedWithDomainID, - getPlaidInstitutionIconUrl, - hasActiveExpensifyCardAssigned, - lastFourNumbersFromCardName, - maskCardNumber, -} from '@libs/CardUtils'; +import {getAllCardsForWorkspace, getCardFeedIcon, getCardFeedWithDomainID, getPlaidInstitutionIconUrl, lastFourNumbersFromCardName, maskCardNumber} from '@libs/CardUtils'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import {getPersonalDetailByEmail, getPhoneNumber, temporaryGetDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; @@ -155,7 +147,6 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const phoneNumber = getPhoneNumber(details); const reimburserEmail = getReimburserEmail(policy); const isReimburser = !!reimburserEmail && reimburserEmail === memberLogin; - const hasActiveExpensifyCard = hasActiveExpensifyCardAssigned(workspaceCards, accountID); const {isAccountLocked} = useLockedAccountState(); const {showLockedAccountModal} = useLockedAccountActions(); @@ -189,11 +180,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM let confirmModalPrompt = translate('workspace.people.removeMembersWarningPrompt', displayName, policyOwnerDisplayName); - if (hasActiveExpensifyCard) { - confirmModalPrompt = translate('workspace.people.removeMemberPromptExpensifyCard', { - memberName: displayName, - }); - } else if (isTechnicalContact) { + if (isTechnicalContact) { confirmModalPrompt = translate('workspace.people.removeMemberPromptTechContact', { memberName: displayName, workspaceOwner: policyOwnerDisplayName, @@ -282,7 +269,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM showRuleBotGuardModal('remove', policyID); return; } - if (hasActiveExpensifyCard || isReimburser) { + if (isReimburser) { showConfirmModal({ shouldShowCancelButton: false, success: true, diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index 7136f2f722b4..7c7fc5078d4d 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -54,7 +54,6 @@ import { getSelectedFeed, getTranslationKeyForCardStatus, getYearFromExpirationDateString, - hasActiveExpensifyCardAssigned, hasAssignedCardMatching, hasIssuedExpensifyCard, hasOnlyOneCardToAssign, @@ -2235,56 +2234,6 @@ describe('CardUtils', () => { }); }); - describe('hasActiveExpensifyCardAssigned', () => { - const cardholderAccountID = 11; - - it('returns true when the member holds an Expensify Card on the workspace', () => { - const cards = createMock({ - '1': {cardID: 1, accountID: cardholderAccountID, bank: CONST.EXPENSIFY_CARD.BANK}, - }); - expect(hasActiveExpensifyCardAssigned(cards, cardholderAccountID)).toBe(true); - }); - - it('returns false for an undefined card list', () => { - expect(hasActiveExpensifyCardAssigned(undefined, cardholderAccountID)).toBe(false); - }); - - it('returns false when the Expensify Card belongs to another member', () => { - const cards = createMock({ - '1': {cardID: 1, accountID: 22, bank: CONST.EXPENSIFY_CARD.BANK}, - }); - expect(hasActiveExpensifyCardAssigned(cards, cardholderAccountID)).toBe(false); - }); - - it('returns false when the member only holds a company card', () => { - const cards = createMock({ - '1': {cardID: 1, accountID: cardholderAccountID, bank: CONST.COMPANY_CARD.FEED_BANK_NAME.VISA}, - }); - expect(hasActiveExpensifyCardAssigned(cards, cardholderAccountID)).toBe(false); - }); - - it('returns false when the member only holds a travel card', () => { - const cards = createMock({ - '1': {cardID: 1, accountID: cardholderAccountID, bank: CONST.EXPENSIFY_CARD.BANK, nameValuePairs: {feedCountry: CONST.TRAVEL.PROGRAM_TRAVEL_US}}, - }); - expect(hasActiveExpensifyCardAssigned(cards, cardholderAccountID)).toBe(false); - }); - - it('returns false when the Expensify Card is already pending removal', () => { - const cards = createMock({ - '1': {cardID: 1, accountID: cardholderAccountID, bank: CONST.EXPENSIFY_CARD.BANK, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, - }); - expect(hasActiveExpensifyCardAssigned(cards, cardholderAccountID)).toBe(false); - }); - - it('skips the cardList bucket of cards still available to assign', () => { - const cards = createMock({ - cardList: {'CREDIT CARD...1234': 'encrypted-value'} as Record, - }); - expect(hasActiveExpensifyCardAssigned(cards, cardholderAccountID)).toBe(false); - }); - }); - describe('forEachAssignedCard', () => { const cardsList = createMock({ '1': {cardID: 1, state: CONST.EXPENSIFY_CARD.STATE.OPEN},