From 2612f71c577a4148acf6086f9c966e3692c79390 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Wed, 28 Oct 2026 20:29:53 -0400 Subject: [PATCH] fix(call): send theme changes only once the call frame is ready (postMessage origin error) Reported from the desktop app console on joining a call: "Failed to execute postMessage on DOMWindow: The target origin provided (http://127.0.0.1:44548) does not match the recipient window origin (http://localhost:44548)". useCallThemeSync calls embed.setTheme as soon as the call view mounts. With waitForIframeLoad: false that send goes out while the iframe is still on its initial about:blank, which has the app origin, so the browser refuses the message addressed to the call origin and the request hangs to a timeout. The theme was right anyway (it is in the frame URL), so this was noise plus a pending request. setTheme now only records the theme until the widget API is ready; on ready it sends the latest theme if it differs from the one the frame loaded with. Reproduced with the app on localhost and the call frame on 127.0.0.1 (as on desktop) against the local LiveKit stack: joining logged the error before, none after. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5 --- src/app/plugins/call/CallEmbed.ts | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/app/plugins/call/CallEmbed.ts b/src/app/plugins/call/CallEmbed.ts index 7d87b4109..f029bfaec 100644 --- a/src/app/plugins/call/CallEmbed.ts +++ b/src/app/plugins/call/CallEmbed.ts @@ -133,6 +133,15 @@ export class CallEmbed { private themeKind: ElementCallThemeKind = 'dark'; + // The theme the frame was loaded with (its URL `theme` param), and whether + // the widget API is ready. Until it is, the iframe may still be on its + // initial about:blank (our own origin), so a send to the call's origin is + // refused ("target origin … does not match the recipient window's origin") + // and the request hangs to a timeout. Theme changes wait for ready. + private loadedThemeKind: ElementCallThemeKind = 'dark'; + + private widgetReady = false; + // Watchdog: detects an iframe that never reaches a usable state. private loadWatchdog?: ReturnType; @@ -360,6 +369,7 @@ export class CallEmbed { this.iframe = iframe; this.container = container; this.themeKind = themeKind; + this.loadedThemeKind = themeKind; const controlState = initialControlState ?? new CallControlState(true, false, true); this.control = new CallControl(controlState, call, iframe); @@ -375,6 +385,13 @@ export class CallEmbed { // happy path (onCallJoined) clears it too; these cover earlier signals so a // user sitting in the lobby/prescreen isn't flagged as an error. this.disposables.push(this.onReady(() => this.settleLoad())); + this.disposables.push( + this.onReady(() => { + this.widgetReady = true; + // A theme change made while the frame was loading. + if (this.themeKind !== this.loadedThemeKind) this.sendTheme(); + }), + ); this.disposables.push(this.onCapabilitiesNotified(() => this.settleLoad())); this.disposables.push(this.onPreparingError(() => this.settleLoad('iframe'))); @@ -399,12 +416,18 @@ export class CallEmbed { return this.room.roomId; } - public setTheme(theme: ElementCallThemeKind) { + public setTheme(theme: ElementCallThemeKind): Promise { this.themeKind = theme; + // Before ready, the URL theme applies and the change is sent on ready. + if (!this.widgetReady) return Promise.resolve(); + return this.sendTheme(); + } + + private sendTheme(): Promise { return this.call.transport - .send(WidgetApiToWidgetAction.ThemeChange, { name: theme }) + .send(WidgetApiToWidgetAction.ThemeChange, { name: this.themeKind }) .catch(() => { - // Widget transport not ready yet; theme URL param is the fallback + // The frame went away mid-send; nothing to do. }); }