Implement P11: Square Web Payments SDK new-card tokenization
Re-enable new-card entry across all 8 flows via Square Web Payments SDK cnon: nonces (backend was already P11-ready): - Add square.ts SDK loader (env-gated on VITE_SQUARE_APPLICATION_ID/LOCATION_ID, sandbox vs prod URL auto-derived from app-ID prefix) + SquareCardInput.svelte (tokenize() via bind:this, onReady state, CardEntryUnavailable fallback) - CardSelection.svelte: replace newCardDisabled gate with new-card toggle + SquareCardInput; expose tokenize() for parent flows - Wire new-card mode into tip x3, booking payment (UserPaymentModal), deposit (BookingFlow incl. guest), Buy a Gift Card + Add a Card (account), and admin till online_square (GiftCardsManagement create/topup) - Retry-safe: each flow caches the one-shot nonce and reuses it on retry so the backend idempotency key dedups instead of re-tokenizing - Docs: README, Gap Backlog P11, Feature Catalog, Technical Manual, P11 plan
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
// Env vars (frontend build-time, public — safe for the browser):
|
||||
// VITE_SQUARE_APPLICATION_ID Square Web Payments application ID (client-side public)
|
||||
// VITE_SQUARE_LOCATION_ID Square location ID
|
||||
// VITE_SQUARE_ENVIRONMENT 'sandbox' | 'production' (optional; auto-derived from
|
||||
// the application ID prefix when omitted)
|
||||
|
||||
const APP_ID = (import.meta.env.VITE_SQUARE_APPLICATION_ID as string | undefined) ?? '';
|
||||
const LOCATION_ID = (import.meta.env.VITE_SQUARE_LOCATION_ID as string | undefined) ?? '';
|
||||
|
||||
export interface SquareConfig {
|
||||
appId: string;
|
||||
locationId: string;
|
||||
}
|
||||
|
||||
/** True when both Square application + location IDs are configured at build time. */
|
||||
export function isSquareConfigured(): boolean {
|
||||
return APP_ID !== '' && LOCATION_ID !== '';
|
||||
}
|
||||
|
||||
export function getSquareConfig(): SquareConfig | null {
|
||||
if (!isSquareConfigured()) return null;
|
||||
return { appId: APP_ID, locationId: LOCATION_ID };
|
||||
}
|
||||
|
||||
function sdkUrl(): string {
|
||||
const env = (import.meta.env.VITE_SQUARE_ENVIRONMENT as string | undefined) ?? '';
|
||||
const isSandbox = env === 'sandbox' || (APP_ID !== '' && APP_ID.startsWith('sandbox-'));
|
||||
return isSandbox
|
||||
? 'https://sandbox.web.squarecdn.com/v1/square.js'
|
||||
: 'https://web.squarecdn.com/v1/square.js';
|
||||
}
|
||||
|
||||
let sdkPromise: Promise<unknown> | null = null;
|
||||
|
||||
/**
|
||||
* Loads the Square.js script once and resolves with the global `Square` object.
|
||||
* The promise is cached so concurrent card forms share a single script load.
|
||||
*/
|
||||
export function loadSquareSdk(): Promise<unknown> {
|
||||
if (typeof window === 'undefined') {
|
||||
return Promise.reject(new Error('Square SDK requires a browser environment'));
|
||||
}
|
||||
const win = window as unknown as { Square?: unknown };
|
||||
if (win.Square) {
|
||||
return Promise.resolve(win.Square);
|
||||
}
|
||||
if (sdkPromise) return sdkPromise;
|
||||
|
||||
sdkPromise = new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = sdkUrl();
|
||||
script.async = true;
|
||||
script.dataset.squareSdk = 'true';
|
||||
script.onload = () => {
|
||||
if (win.Square) {
|
||||
resolve(win.Square);
|
||||
} else {
|
||||
reject(new Error('Square.js loaded but the Square global is missing'));
|
||||
}
|
||||
};
|
||||
script.onerror = () => {
|
||||
sdkPromise = null;
|
||||
reject(new Error('Failed to load Square Web Payments SDK'));
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
return sdkPromise;
|
||||
}
|
||||
|
||||
/** Returns `Square.payments(appId, locationId)` once the SDK is loaded. */
|
||||
export async function getSquarePayments(): Promise<unknown> {
|
||||
const config = getSquareConfig();
|
||||
if (!config) {
|
||||
throw new Error(
|
||||
'Square is not configured — set VITE_SQUARE_APPLICATION_ID and VITE_SQUARE_LOCATION_ID'
|
||||
);
|
||||
}
|
||||
const Square = (await loadSquareSdk()) as {
|
||||
payments: (appId: string, locationId: string) => unknown;
|
||||
};
|
||||
return Square.payments(config.appId, config.locationId);
|
||||
}
|
||||
Reference in New Issue
Block a user