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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user