// Supported clients: iOS Safari, Android Chrome, PC Chrome/Edge/Firefox. // Gated on required API presence; anything missing → hard stop. export function supported(): string | null { if (typeof window === 'undefined') return 'window unavailable'; if (typeof createImageBitmap !== 'function') return 'createImageBitmap'; if (typeof Worker === 'undefined') return 'Worker'; if (typeof indexedDB === 'undefined') return 'IndexedDB'; const hasOffscreen = typeof OffscreenCanvas !== 'undefined' && typeof OffscreenCanvas.prototype.convertToBlob === 'function'; const hasCanvasBlob = typeof HTMLCanvasElement !== 'undefined' && typeof HTMLCanvasElement.prototype.toBlob === 'function'; if (!hasOffscreen && !hasCanvasBlob) return 'canvas PNG encode'; const ua = navigator.userAgent; const isIOS = /iPad|iPhone|iPod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); const isAndroid = /Android/i.test(ua); const isSafari = /Safari/i.test(ua) && !/Chrome|CriOS|Edg|Firefox|FxiOS/i.test(ua); const isChrome = /Chrome|CriOS/i.test(ua) && !/Edg/i.test(ua); const isEdge = /Edg/i.test(ua); const isFirefox = /Firefox|FxiOS/i.test(ua); if (isIOS && isSafari) return null; if (isAndroid && isChrome) return null; if (!isIOS && !isAndroid && (isChrome || isEdge || isFirefox)) return null; return 'unsupported browser'; } // v4 UUID. crypto.randomUUID() is only guaranteed on secure contexts // (HTTPS/localhost); the UAT host is plain HTTP on a LAN IP, where it is // undefined. Fall back to crypto.getRandomValues (available on HTTP) so the // client-generated ids (APP-20) work in both contexts. export function uuid(): string { const c = globalThis.crypto; if (c && typeof c.randomUUID === 'function') return c.randomUUID(); if (c && typeof c.getRandomValues === 'function') { const b = new Uint8Array(16); c.getRandomValues(b); b[6] = (b[6] & 0x0f) | 0x40; // version 4 b[8] = (b[8] & 0x3f) | 0x80; // variant 10 const h = Array.from(b, (x) => x.toString(16).padStart(2, '0')).join(''); return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`; } return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (ch) => { const r = (Math.random() * 16) | 0; const v = ch === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); }