feat: proactive saved-card SCA — challenge runs BEFORE the first charge, never a naked ccof attempt
Square's card.tokenize(verificationDetails, squareCardId) determines the SCA requirement UP FRONT and returns a fresh verification_token (or an explicit outcome), so the customer-initiated saved-card flow now runs it before the first charge attempt instead of the reactive 'attempt naked ccof -> 402 verification_required -> challenge + retry' round-trip. - UserPaymentModal/TipPayment/BookingFlow/account gift-card buy: call runSavedCardSCAProactively before charging; 'verified' carries the token on attempt #1; 'sca-unavailable' demotes to the 2FA gate (the only tokenless path); 'challenge-cancelled'/'sca-failed' never charge and keep the pending row retryable with the same cached idempotency key - The reactive re-challenge hook is removed; a defensive verification-required 402 (stale/consumed token) surfaces VERIFICATION_REQUIRED_MESSAGE and lets the user retry - Admin PaymentModal + till saved-card charges remain MERCHANT-INITIATED (customer_initiated=false, SCA-exempt, no liability shift) — unchanged - square.ts comments updated (saved-card charges now carry a token proactively; SAVED_CARD_VERIFICATION_MESSAGE is the defensive path) - Tests: 98 frontend tests (proactive decision coverage); build clean
This commit is contained in:
@@ -111,15 +111,23 @@ const PAYMENT_DEFINITIVE_STATUS = 402;
|
||||
/**
|
||||
* True when a definitive (402) charge failure on a SAVED CARD should be
|
||||
* surfaced as a card-issuer verification problem rather than a plain decline.
|
||||
* The backend sets customer_details.customer_initiated=true on saved-card
|
||||
* (ccof) charges and classifies issuer-verification rejections — Square's
|
||||
*
|
||||
* This is the DEFENSIVE/unexpected path: customer-initiated saved-card (ccof)
|
||||
* surfaces now run the client-side SCA challenge PROACTIVELY before the first
|
||||
* charge attempt (tokenizeSavedCardWithVerification) and carry a fresh
|
||||
* `verification_token` on the charge — or have demoted to the 2FA gate when
|
||||
* SCA is unavailable — so a naked ccof charge should never reach Square. A
|
||||
* 402 here therefore means the verification token was consumed/expired
|
||||
* between tokenize and charge (or a config drift), and the buyer should be
|
||||
* pointed at the retry affordance rather than silently re-challenged. The
|
||||
* backend sets customer_details.customer_initiated=true on saved-card (ccof)
|
||||
* charges and classifies issuer-verification rejections — Square's
|
||||
* CARD_DECLINED_VERIFICATION_REQUIRED and friends — as definitive 402s, but the
|
||||
* response body is the generic "Payment failed" text with no distinguishing
|
||||
* code. Saved cards skip the client-side tokenizeWithVerification SCA step, so
|
||||
* a 402 on the saved-card path means the issuer still requires verification:
|
||||
* retrying the same saved card can never succeed, and the buyer must pay with
|
||||
* a freshly tokenized card or re-add theirs. New-card (cnon) charges carry
|
||||
* their own SCA verification token, so they are never classified this way.
|
||||
* code. Retrying the same saved card can never succeed, and the buyer must pay
|
||||
* with a freshly tokenized card, re-add theirs, or re-run the SCA challenge.
|
||||
* New-card (cnon) charges carry their own SCA verification token, so they are
|
||||
* never classified this way.
|
||||
*/
|
||||
export function isSavedCardVerificationRequired(status: number, usedSavedCard: boolean): boolean {
|
||||
return usedSavedCard && status === PAYMENT_DEFINITIVE_STATUS;
|
||||
|
||||
Reference in New Issue
Block a user