From 933ce6bb2b78cd6a9e16d72a2b56e6b293c6df04 Mon Sep 17 00:00:00 2001 From: atesta103 Date: Fri, 24 Jul 2026 14:22:35 +0200 Subject: [PATCH 1/2] feat(dialog): let the ticket dialog close on backdrop click MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an opt-in closeOnBackdropClick prop to the shared Dialog component, off by default so every other dialog in the app keeps its current behavior — several (the "called" reminder, the moderation warning) deliberately force an explicit acknowledgment, and a stray backdrop click must not dismiss those. Enabled only on the ticket dialog, which is purely informational and has no confirmation to force. Detects the backdrop click the standard way for a native : a click on the ::backdrop pseudo-element registers as a click on the element itself (event.target === event.currentTarget), while a click on any actual content is a click on a descendant. Co-Authored-By: Claude Opus 4.8 --- app/[slug]/wait/[ticketId]/WaitClient.tsx | 6 +++++- components/ui/Dialog.tsx | 21 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/[slug]/wait/[ticketId]/WaitClient.tsx b/app/[slug]/wait/[ticketId]/WaitClient.tsx index 2433d30..9274e82 100644 --- a/app/[slug]/wait/[ticketId]/WaitClient.tsx +++ b/app/[slug]/wait/[ticketId]/WaitClient.tsx @@ -445,7 +445,11 @@ function WaitClient({ merchant, ticketId }: WaitClientProps) { )} {canDownloadTicket && ( - setTicketDialogOpen(false)}> + setTicketDialogOpen(false)} + closeOnBackdropClick + > setTicketDialogOpen(false)}>Votre ticket
diff --git a/components/ui/Dialog.tsx b/components/ui/Dialog.tsx index 2c74092..d069c12 100644 --- a/components/ui/Dialog.tsx +++ b/components/ui/Dialog.tsx @@ -9,9 +9,17 @@ type DialogProps = { onClose: () => void children: React.ReactNode className?: string + /** + * Lets a click on the backdrop dismiss the dialog, in addition to Escape + * and any explicit close control. Off by default: several dialogs in this + * app (e.g. the "called" reminder, the moderation warning) deliberately + * force an explicit acknowledgment, and a stray backdrop click must not + * dismiss those. Opt in only for dialogs that are purely informational. + */ + closeOnBackdropClick?: boolean } -function Dialog({ open, onClose, children, className }: DialogProps) { +function Dialog({ open, onClose, children, className, closeOnBackdropClick = false }: DialogProps) { const dialogRef = useRef(null) const previousFocusRef = useRef(null) @@ -48,6 +56,17 @@ function Dialog({ open, onClose, children, className }: DialogProps) { return ( { + // A click on the ::backdrop registers as a click on the + // element itself (target === currentTarget); + // a click on any actual content is a click on a + // descendant, so this only fires for the backdrop. + if (e.target === e.currentTarget) handleClose() + } + : undefined + } className={cn( "m-auto w-[calc(100%-2rem)] max-w-md rounded-lg border border-border-default bg-surface-card p-0 shadow-xl", "backdrop:bg-surface-overlay", From fc2fa5e3d6c0d35378da89783a415fb4972522e9 Mon Sep 17 00:00:00 2001 From: atesta103 Date: Fri, 24 Jul 2026 14:26:21 +0200 Subject: [PATCH 2/2] fix(wait): download the ticket as a blob URL instead of a data URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking "Télécharger l'image" opened Safari's "View or Download" picker instead of saving directly — a known Safari quirk where a data: URL with an image MIME type on an link gets treated as content to preview, not a file to save, regardless of the download attribute. Switches from toPng (a data: URL) to toBlob + URL.createObjectURL. A blob: URL is what the download attribute reliably triggers a direct save for across Safari and other browsers. The object URL is revoked after a 1s delay rather than immediately after click() — revoking synchronously risks freeing it before the browser has actually started reading it, which Safari in particular can be slow to do. Co-Authored-By: Claude Opus 4.8 --- app/[slug]/wait/[ticketId]/WaitClient.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/[slug]/wait/[ticketId]/WaitClient.tsx b/app/[slug]/wait/[ticketId]/WaitClient.tsx index 9274e82..f703c9d 100644 --- a/app/[slug]/wait/[ticketId]/WaitClient.tsx +++ b/app/[slug]/wait/[ticketId]/WaitClient.tsx @@ -14,7 +14,7 @@ import { BellRing, Smartphone, MessageSquare, AlertCircle, Download } from "luci import { playHapticBuzz, playSound, unlockAudio, type SoundChoice } from "@/lib/utils/notifications" import { getBusinessWording } from "@/lib/utils/business-wording" import { buildRecoverUrl } from "@/lib/utils/ticket-download" -import { toPng } from "html-to-image" +import { toBlob } from "html-to-image" type NotificationChannels = { sound: boolean @@ -95,11 +95,21 @@ function WaitClient({ merchant, ticketId }: WaitClientProps) { setIsDownloading(true) setDownloadError(null) try { - const dataUrl = await toPng(ticketCardRef.current, { pixelRatio: 2 }) + const blob = await toBlob(ticketCardRef.current, { pixelRatio: 2 }) + if (!blob) throw new Error("toBlob returned null") + // A blob: object URL triggers a direct save reliably; a raw data: + // URL on the same does not — Safari treats an image + // data: URL as content to preview and offers "View or Download" + // instead of saving it, which is what surfaced this in testing. + const objectUrl = URL.createObjectURL(blob) const link = document.createElement("a") - link.href = dataUrl + link.href = objectUrl link.download = `ticket-${merchant.slug}.png` link.click() + // Revoking immediately can race the browser actually reading the + // blob (Safari in particular can be slow to pick it up) — give it + // a moment before freeing the URL. + setTimeout(() => URL.revokeObjectURL(objectUrl), 1000) } catch (err) { console.error("[WaitClient] Ticket image export failed:", err) setDownloadError(