Frontend: verified-only save-card gating + shared nonce-staleness helper

canSaveCardsForRole(role) in square.ts is the single source of truth for the
save-card product rule (verified_email, admin — never affiliate). All four
predicate sites (account page, UserBookingModal, BookingFlow, TipPayment) were
wrong before, excluding admin and including affiliate. The worst gap was
BookingFlow passing canSaveCards={authStore.isAuthenticated} to the Pay-Early
modal, which let unverified users save cards — it now passes the derived value.

isNonceStale() + NONCE_STALENESS_MS replace the 240s staleness check duplicated
five times, keeping the amount-bound re-tokenization semantics identical.
This commit is contained in:
2026-08-22 00:34:49 +01:00
parent d9c2c5ac2c
commit 05bb142cfd
6 changed files with 57 additions and 30 deletions
+26
View File
@@ -18,6 +18,32 @@ export interface SquareConfig {
locationId: string;
}
// Square card nonces are single-use, and the SCA verification token issued with
// them is amount-bound. A cached nonce older than this is treated as stale and
// forces a fresh tokenization before the charge attempt.
export const NONCE_STALENESS_MS = 240_000;
/**
* True when a user's role is allowed to save cards for reuse. Only VERIFIED
* accounts (verified_email, admin) may save cards — guests, unverified accounts
* and affiliates must never see the save-card option. Single source of truth so
* the predicate can't drift between booking, account and tip surfaces.
*/
export function canSaveCardsForRole(role: string | undefined): boolean {
return role === 'verified_email' || role === 'admin';
}
/** True when a cached card nonce can no longer be reused: it was tokenized for a
* different amount than `amount`, or it is older than NONCE_STALENESS_MS. */
export function isNonceStale(
nonceTokenizedAt: number,
nonceTokenizedFor: number,
amount: number,
now: number = Date.now()
): boolean {
return nonceTokenizedFor !== amount || now - nonceTokenizedAt > NONCE_STALENESS_MS;
}
/** True when the frontend runs in local-dev mock mode: VITE_SQUARE_ENVIRONMENT === 'mock'
* AND the dev build (import.meta.env.DEV). The DEV gate makes the mock structurally
* impossible in any production bundle — even if the env var is mis-set at build time. */