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();
|
fetchBookingDetails();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err instanceof Error ? err.message : 'Tip payment failed');
|
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 {
|
} finally {
|
||||||
tipProcessing = false;
|
tipProcessing = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,6 +275,14 @@
|
|||||||
paymentState = 'error';
|
paymentState = 'error';
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Payment failed';
|
const errorMessage = err instanceof Error ? err.message : 'Payment failed';
|
||||||
toast.error(errorMessage);
|
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';
|
const msg = _err instanceof Error ? _err.message : 'Payment declined';
|
||||||
error = msg;
|
error = msg;
|
||||||
toast.error(`${msg}. Please try again or use another card.`);
|
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();
|
releaseLock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -353,10 +353,24 @@
|
|||||||
} else {
|
} else {
|
||||||
const errText = await res.text();
|
const errText = await res.text();
|
||||||
toast.error(extractErrorMessage(errText) || 'Failed to purchase gift card');
|
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) {
|
} catch (err) {
|
||||||
console.error('buyGiftCard error:', err);
|
console.error('buyGiftCard error:', err);
|
||||||
toast.error('Network error');
|
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 {
|
} finally {
|
||||||
buyingGiftCard = false;
|
buyingGiftCard = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user