From d972af08f13182492d02f65ad8de52d7e10b056b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 08:19:06 +0000 Subject: [PATCH] fix(card): ignore the retired Bridge endorsement once a card customer exists buildCardSteps fell back to the bridge.xyz "cards" endorsement for every non-Rain issuer, even when /cards/status already reported a kycStatus. For an old Bridge customer applying for a Wirex card, an approved Bridge endorsement marked the KYC step complete and offered "Activate card" before they had verified with Sumsub. The endorsement now only applies to Bridge-only users, who have no card customer and so no kycStatus. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PmSmmwX1kxkmMBEP7pVKPG --- .../__tests__/buildCardSteps.test.ts | 39 ++++++++++++++++++- hooks/useCardSteps/stepHelpers.ts | 16 +++++--- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/hooks/useCardSteps/__tests__/buildCardSteps.test.ts b/hooks/useCardSteps/__tests__/buildCardSteps.test.ts index 41cee84f8..75f0431bb 100644 --- a/hooks/useCardSteps/__tests__/buildCardSteps.test.ts +++ b/hooks/useCardSteps/__tests__/buildCardSteps.test.ts @@ -1,5 +1,11 @@ +import { EndorsementStatus } from '@/components/BankTransfer/enums'; import { buildCardSteps } from '@/hooks/useCardSteps/stepHelpers'; -import { CardProvider, KycStatus, RainApplicationStatus } from '@/lib/types'; +import { + BridgeCustomerEndorsement, + CardProvider, + KycStatus, + RainApplicationStatus, +} from '@/lib/types'; const noop = () => {}; @@ -274,3 +280,34 @@ describe('buildCardSteps - a blocked activation', () => { expect(activate?.onPress).toBeDefined(); }); }); + +describe('buildCardSteps - a retired Bridge endorsement', () => { + // An old bridge.xyz customer keeps their "cards" endorsement on Bridge's side. + const approvedBridgeEndorsement = { + name: 'cards', + status: EndorsementStatus.APPROVED, + } as unknown as BridgeCustomerEndorsement; + + const kycStepFor = (kycStatus?: KycStatus) => + buildCardSteps(approvedBridgeEndorsement, undefined, false, undefined, noop, noop, noop, { + cardIssuer: CardProvider.WIREX, + kycStatus, + depositRequired: false, + }).find(s => s.key === 'kyc'); + + it('does not complete KYC for a Wirex applicant who has not verified', () => { + // The backend reports a card customer, so its kycStatus is the answer — + // the Bridge approval is for a card that no longer exists. + const kyc = kycStepFor(KycStatus.NOT_STARTED); + + expect(kyc?.completed).toBe(false); + expect(kyc?.buttonText).toBe('Continue verification'); + expect(kyc?.onPress).toBeDefined(); + }); + + it('still honours the endorsement for a Bridge-only user with no card customer', () => { + const kyc = kycStepFor(undefined); + + expect(kyc?.completed).toBe(true); + }); +}); diff --git a/hooks/useCardSteps/stepHelpers.ts b/hooks/useCardSteps/stepHelpers.ts index f6a93ecec..df36ad83b 100644 --- a/hooks/useCardSteps/stepHelpers.ts +++ b/hooks/useCardSteps/stepHelpers.ts @@ -63,6 +63,12 @@ export function buildCardSteps( isSubmittingPendingApplication?: boolean; }, ): Step[] { + // The Bridge endorsement is a fallback for Bridge-only users, who have no card + // customer and so no `kycStatus`. Once `/cards/status` reports one, the + // application is on Rain or Wirex and bridge.xyz has no say in it: an old + // Bridge "cards" approval would otherwise mark a Wirex applicant's KYC step + // done (and offer "Activate card") before they have verified with Sumsub. + const legacyEndorsement = options?.kycStatus != null ? undefined : cardsEndorsement; const stepOptions = options?.cardIssuer != null || options?.kycStatus != null ? { @@ -72,9 +78,9 @@ export function buildCardSteps( kycWarnings: options?.kycWarnings, } : undefined; - const description = getStepDescription(cardsEndorsement, customerRejectionReasons, stepOptions); - const buttonText = getStepButtonText(cardsEndorsement, stepOptions); - const isButtonDisabled = isStepButtonDisabled(cardsEndorsement, stepOptions); + const description = getStepDescription(legacyEndorsement, customerRejectionReasons, stepOptions); + const buttonText = getStepButtonText(legacyEndorsement, stepOptions); + const isButtonDisabled = isStepButtonDisabled(legacyEndorsement, stepOptions); const isRainKycApproved = options?.cardIssuer === CardProvider.RAIN && @@ -94,7 +100,7 @@ export function buildCardSteps( // kycStatus is the canonical backend decision for every non-Rain issuer, // with the Bridge endorsement kept as the legacy fallback. options?.kycStatus === KycStatus.APPROVED || - cardsEndorsement?.status === EndorsementStatus.APPROVED; + legacyEndorsement?.status === EndorsementStatus.APPROVED; // Deliberately does NOT repeat the failure reason. `CardStatusBanner` already // carries it — as a headline, with the detail and a support action — and @@ -163,7 +169,7 @@ export function buildCardSteps( : description, completed: kycStepComplete, status: kycStepComplete ? 'completed' : 'pending', - endorsementStatus: cardsEndorsement?.status, + endorsementStatus: legacyEndorsement?.status, buttonText: showHoldStep ? undefined : buttonText, onPress: showHoldStep || isButtonDisabled ? undefined : kycStepOnPress, };