From 03a667661eeec91811ecdaf231edad29ada2f1aa Mon Sep 17 00:00:00 2001 From: lucaronin Date: Wed, 27 May 2026 18:56:36 +0200 Subject: [PATCH] fix: guard whiteboard permission rejections --- src/components/TldrawWhiteboard.test.tsx | 32 +++++++++++++++++ src/components/TldrawWhiteboard.tsx | 46 ++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/components/TldrawWhiteboard.test.tsx b/src/components/TldrawWhiteboard.test.tsx index d452aa23..63362ba1 100644 --- a/src/components/TldrawWhiteboard.test.tsx +++ b/src/components/TldrawWhiteboard.test.tsx @@ -125,6 +125,13 @@ function mockEditor(): Editor { } as unknown as Editor } +function dispatchUnhandledRejection(reason: unknown): Event { + const event = new Event('unhandledrejection', { cancelable: true }) + Object.defineProperty(event, 'reason', { value: reason }) + window.dispatchEvent(event) + return event +} + function measuredTextElement(): HTMLElement { const element = document.createElement('div') element.textContent = 'Label' @@ -246,6 +253,31 @@ describe('TldrawWhiteboard', () => { expect(() => editor.textMeasure.measureElementTextNodeSpans(measuredTextElement())).toThrow('top') }) + it('suppresses whiteboard platform permission rejections while mounted', () => { + render( + + ) + + const cleanup = renderedTldrawProps().onMount(mockEditor()) + const denied = { + name: 'NotAllowedError', + message: 'The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.', + } + + expect(dispatchUnhandledRejection(denied).defaultPrevented).toBe(true) + expect(dispatchUnhandledRejection(new Error('save failed')).defaultPrevented).toBe(false) + + cleanup() + expect(dispatchUnhandledRejection(denied).defaultPrevented).toBe(false) + }) + it('resets the drawing store when switching to a blank board snapshot', () => { const boardASnapshot = { records: { shape: 'from-board-a' } } const { rerender } = render( diff --git a/src/components/TldrawWhiteboard.tsx b/src/components/TldrawWhiteboard.tsx index 4de2e373..f78a321f 100644 --- a/src/components/TldrawWhiteboard.tsx +++ b/src/components/TldrawWhiteboard.tsx @@ -101,6 +101,50 @@ function ignoreTldrawUserPreferencesUpdate(preferences: TLUserPreferences) { void preferences } +function rejectionName(error: unknown): string { + if (error instanceof Error) return error.name + if (typeof error !== 'object' || error === null || !('name' in error)) return '' + + const { name } = error + return typeof name === 'string' ? name : '' +} + +function rejectionMessage(error: unknown): string { + if (error instanceof Error) return error.message + if (typeof error === 'string') return error + if (typeof error !== 'object' || error === null || !('message' in error)) return '' + + const { message } = error + return typeof message === 'string' ? message : '' +} + +function isWhiteboardPlatformPermissionRejection(reason: unknown): boolean { + const name = rejectionName(reason).toLowerCase() + const message = rejectionMessage(reason).toLowerCase() + if (name === 'notallowederror') return true + + return message.includes('notallowederror') || ( + message.includes('not allowed') + && ( + message.includes('permission') + || message.includes('platform') + || message.includes('user agent') + ) + ) +} + +function installTldrawPlatformPermissionGuard(): () => void { + const handleUnhandledRejection = (event: PromiseRejectionEvent) => { + if (!isWhiteboardPlatformPermissionRejection(event.reason)) return + event.preventDefault() + } + + window.addEventListener('unhandledrejection', handleUnhandledRejection) + return () => { + window.removeEventListener('unhandledrejection', handleUnhandledRejection) + } +} + function parseSnapshot(source: string): TLStoreSnapshot | null { if (!source.trim()) return null @@ -211,8 +255,10 @@ function installZoomAwareViewport(editor: Editor): () => void { function installWhiteboardRuntimeGuards(editor: Editor): () => void { const cleanupTextMeasurementGuard = installTldrawTextMeasurementGuard(editor) const cleanupZoomAwareViewport = installZoomAwareViewport(editor) + const cleanupPlatformPermissionGuard = installTldrawPlatformPermissionGuard() return () => { + cleanupPlatformPermissionGuard() cleanupZoomAwareViewport() cleanupTextMeasurementGuard() }