From d9385d94419697037ff5a991afe1b9ceff0a8dfd Mon Sep 17 00:00:00 2001 From: Adam Masiarek Date: Sat, 15 Aug 2026 14:05:26 -0400 Subject: [PATCH] fix(feedback): don't let a missing widget turn Feedback into a no-op MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openFeedback() reaches into the feedback widget's iframe and clicks the button inside it, with no guard at any step: const launcherFrame = document.getElementById("launcher-frame"); const button = (launcherFrame as HTMLIFrameElement).contentWindow.document .getElementsByClassName("launcher-button")[0]; (button as HTMLButtonElement).click(); If the widget hasn't loaded — blocked, slow, or prevented by a browser's tracking protection, which is the usual story on iOS — then launcherFrame is null and this throws a TypeError. The Feedback menu item does nothing at all, and the only trace is a console error the user never sees. Reading the iframe's document can also throw outright. Each step is now optional-chained inside a try, and when the widget cannot be reached the click falls back to a mailto: to elections@equal.vote rather than silently failing. I could not reproduce #1080 on an iPhone — no device — so this is a fix for the failure mode the code makes possible, not a confirmed reproduction of the one in the video. If the widget IS loading on that phone and the button is being clicked, the cause is elsewhere and this only removes a way for it to fail quietly. --- packages/frontend/src/components/util.tsx | 29 +++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/frontend/src/components/util.tsx b/packages/frontend/src/components/util.tsx index 23ed5b023..b3be9af50 100644 --- a/packages/frontend/src/components/util.tsx +++ b/packages/frontend/src/components/util.tsx @@ -311,14 +311,29 @@ export const truncName = (name, maxSize) => { return name.slice(0, maxSize - 3).concat("..."); }; +// Fallback when the feedback widget isn't there to click. +export const FEEDBACK_EMAIL = 'elections@equal.vote'; + export const openFeedback = () => { - // simulate clicking the feedback button - const launcherFrame = document.getElementById("launcher-frame"); - const button = - (launcherFrame as HTMLIFrameElement).contentWindow.document.getElementsByClassName( - "launcher-button" - )[0]; - (button as HTMLButtonElement).click(); + // Simulate clicking the widget's own button. Every step here can fail on a + // browser that hasn't loaded the widget — the iframe may be absent, blocked, + // or still loading, and reading its document can throw outright. Unguarded, + // any of those turned the Feedback menu item into a no-op with nothing but a + // console error (#1080, reported on iOS). + try { + const launcherFrame = document.getElementById("launcher-frame") as HTMLIFrameElement | null; + const button = launcherFrame?.contentWindow?.document + ?.getElementsByClassName("launcher-button")[0] as HTMLButtonElement | undefined; + if (button) { + button.click(); + return; + } + } catch (err) { + console.warn('Feedback widget could not be opened', err); + } + + // Rather than appear broken, hand the user a way to reach us. + window.location.href = `mailto:${FEEDBACK_EMAIL}?subject=${encodeURIComponent('BetterVoting feedback')}`; }; export function scrollToElement(e, opts: { behavior?: ScrollBehavior; delay?: number; cancelOnUserInput?: boolean } = {}) {