Re-tokenize fresh after a definitive card charge failure in all nonce flows

A cnon: nonce and its SCA verification token are consumed by a definitive charge failure (e.g. declined card) and can never succeed again, but TipPayment, UserBookingModal, UserPaymentModal and the account-page Buy-a-Gift-Card cached them and resubmitted the dead nonce on every retry — a non-retryable failure loop. The nonce/verification-token/amount/timestamp cache is now cleared in each error branch so retries re-tokenize fresh, while the idempotency key is kept for network-timeout dedup.
This commit is contained in:
2026-08-22 00:34:49 +01:00
parent 5605402e13
commit 965da86b64
4 changed files with 38 additions and 0 deletions
@@ -304,6 +304,13 @@
fetchBookingDetails();
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Tip payment failed');
// A definitive charge failure consumes the nonce + SCA verification
// token — clear the cached pair so retries re-tokenize fresh. The
// idempotency key stays for network-timeout dedup.
tipNonce = '';
tipVerificationToken = '';
tipTokenAmount = 0;
tipTokenizedAt = 0;
} finally {
tipProcessing = false;
}
@@ -275,6 +275,14 @@
paymentState = 'error';
const errorMessage = err instanceof Error ? err.message : 'Payment failed';
toast.error(errorMessage);
// A definitive charge failure (e.g. declined card) consumes the nonce
// and SCA verification token — they can never succeed again. Clear the
// cached pair so the next retry re-tokenizes fresh. The idempotency
// key is kept: it's still correct for network-timeout dedup.
tipNonce = '';
tipVerificationToken = '';
tipTokenAmount = 0;
tipTokenizedAt = 0;
}
}
@@ -464,6 +464,15 @@
const msg = _err instanceof Error ? _err.message : 'Payment declined';
error = msg;
toast.error(`${msg}. Please try again or use another card.`);
// A definitive charge failure consumes the nonce + SCA verification
// token — clear the cached pair so retries re-tokenize fresh. The
// idempotency key stays for network-timeout dedup. CardSelection stays
// mounted on error, so this also stops a user who corrects their card
// digits from resubmitting the old nonce for the new card.
newCardNonce = '';
newCardVerificationToken = '';
newCardTokenAmount = 0;
newCardTokenizedAt = 0;
releaseLock();
}
}
+14
View File
@@ -353,10 +353,24 @@
} else {
const errText = await res.text();
toast.error(extractErrorMessage(errText) || 'Failed to purchase gift card');
// A definitive charge failure (e.g. declined card) consumes the
// nonce + SCA verification token — clear the cached pair so the
// next retry re-tokenizes fresh. The idempotency key stays for
// network-timeout dedup.
buyNonce = '';
buyVerificationToken = '';
buyTokenAmount = 0;
buyTokenizedAt = 0;
}
} catch (err) {
console.error('buyGiftCard error:', err);
toast.error('Network error');
// Same for thrown errors (network / malformed response): a retry must
// re-tokenize fresh rather than resubmit a consumed nonce.
buyNonce = '';
buyVerificationToken = '';
buyTokenAmount = 0;
buyTokenizedAt = 0;
} finally {
buyingGiftCard = false;
}