import { downloadImage } from '../imageUtils'; /** * Sharing a try-on, without the image ever leaving the device. * * The Web Share API hands the file straight to the OS share sheet, so there is * no upload and no link to a copy of the render on our infrastructure. Where * it is missing we fall back to a plain download, then to copying the product * link, in that order: the shopper keeps a usable action either way. */ export type ShareOutcome = | 'shared' | 'downloaded' | 'copied' /** The shopper opened the share sheet and closed it. Not an error. */ | 'dismissed' | 'failed'; export interface ShareRequest { /** The render, as a data URL. */ image: string; filename: string; title: string; /** Product page, used by the link fallbacks. */ url?: string; } export function tryOnFilename(productTitle?: string, at: number = Date.now()): string { const slug = productTitle ? productTitle.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') : ''; return `virtual-tryon-${slug || 'result'}-${at}.png`; } /** * Decoded by hand rather than with `fetch(dataUrl)`, so that the only place * this widget ever calls fetch with an image stays the try-on request itself. */ function dataUrlToFile(dataUrl: string, filename: string): File | null { try { const separator = dataUrl.indexOf(','); if (separator === -1) return null; const meta = dataUrl.slice(0, separator); const encoded = dataUrl.slice(separator + 1); if (!encoded) return null; const type = /:(.*?);/.exec(meta)?.[1] ?? 'image/png'; const binary = atob(encoded); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i += 1) { bytes[i] = binary.charCodeAt(i); } return new File([bytes], filename, { type }); } catch (e) { console.warn('[share] Could not turn the render into a file:', e); return null; } } function isDismissal(error: unknown): boolean { return (error as { name?: string } | null)?.name === 'AbortError'; } function absoluteUrl(url: string): string { try { return new URL(url, window.location.href).href; } catch { return url; } } export async function shareTryOn({ image, filename, title, url }: ShareRequest): Promise { const nav: Navigator | undefined = typeof navigator === 'undefined' ? undefined : navigator; const file = dataUrlToFile(image, filename); if (nav?.share && file && nav.canShare?.({ files: [file] })) { try { await nav.share({ files: [file], title }); return 'shared'; } catch (error) { if (isDismissal(error)) return 'dismissed'; // Anything else (no user gesture, unsupported payload) falls through. } } if (nav?.share && url) { try { await nav.share({ title, url: absoluteUrl(url) }); return 'shared'; } catch (error) { if (isDismissal(error)) return 'dismissed'; } } try { downloadImage(image, filename); return 'downloaded'; } catch (error) { console.warn('[share] Download fallback failed:', error); } if (url && nav?.clipboard?.writeText) { try { await nav.clipboard.writeText(absoluteUrl(url)); return 'copied'; } catch (error) { console.warn('[share] Clipboard fallback failed:', error); } } return 'failed'; }