// 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' | 'mock' (optional; // auto-derived from the application ID prefix when // omitted). 'mock' is LOCAL-DEV ONLY: it renders a // token-only mock card form (never a real Square.js // iframe). It is additionally gated on the dev build // (import.meta.env.DEV), so it can never activate in // a production bundle even if the var is mis-set. 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) ?? ''; const SQUARE_ENV = (import.meta.env.VITE_SQUARE_ENVIRONMENT as string | undefined) ?? ''; export interface SquareConfig { appId: string; locationId: string; } /** 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. */ export function isSquareMock(): boolean { return SQUARE_ENV === 'mock' && import.meta.env.DEV; } /** True when a card form can be shown: real Square credentials OR local-dev mock mode. */ export function isSquareConfigured(): boolean { return isSquareMock() || (APP_ID !== '' && LOCATION_ID !== ''); } export function getSquareConfig(): SquareConfig | null { if (!isSquareConfigured()) return null; return { appId: APP_ID, locationId: LOCATION_ID }; } // Pinned SDK version: Square.js is loaded from the /v1/ path, which is // Square's stable major version. Square does not publish SRI hashes, so no // integrity attribute can be set — the /v1/ version pin is the supply-chain // control (S-3). Monitor Square's release notes before major bumps. const SQUARE_SDK_VERSION = 'v1'; 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/${SQUARE_SDK_VERSION}/square.js` : `https://web.squarecdn.com/${SQUARE_SDK_VERSION}/square.js`; } let sdkPromise: Promise | 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 { 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'; // Non-sticky-rejection invariant: a FAILED load attempt must never poison // the cached promise. Concurrent callers during one attempt share the same // in-flight promise (good — single script load), but a REJECTED promise is // cleared here so the next getSquarePayments() call starts a fresh attempt // instead of returning the same error forever. The dead