From 8aeb00bfc6fce2f021a3b03bf4a39f50aef56e6d Mon Sep 17 00:00:00 2001 From: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> Date: Sat, 18 Jul 2026 15:25:44 -0700 Subject: [PATCH 1/2] fix(web): ask Safari users for Mac type Co-authored-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> --- web/src/features/invite/ui/InvitePage.tsx | 122 +++++++++++++++++++++- web/src/shared/lib/buzz-download.ts | 19 ++-- web/tests/e2e/smoke.spec.ts | 46 ++++++++ 3 files changed, 178 insertions(+), 9 deletions(-) diff --git a/web/src/features/invite/ui/InvitePage.tsx b/web/src/features/invite/ui/InvitePage.tsx index 8071c22741b..de12e219d49 100644 --- a/web/src/features/invite/ui/InvitePage.tsx +++ b/web/src/features/invite/ui/InvitePage.tsx @@ -1,7 +1,9 @@ import buzzAppIcon from "@/assets/app-icon@3x.png"; import { BUZZ_RELEASES_URL, - resolveBuzzDownloadUrl, + type BuzzDownloadPlatform, + detectBuzzDownloadPlatform, + resolveBuzzDownloadUrlForPlatform, } from "@/shared/lib/buzz-download"; import { relayWsUrl } from "@/shared/lib/relay-url"; import { Button } from "@/shared/ui/button"; @@ -32,9 +34,27 @@ export function InvitePage({ code }: { code: string }) { const [agreementConfirmed, setAgreementConfirmed] = React.useState(false); const [opening, setOpening] = React.useState(false); const [downloadUrl, setDownloadUrl] = React.useState(BUZZ_RELEASES_URL); + const [needsMacChoice, setNeedsMacChoice] = React.useState(false); + const [showMacChoice, setShowMacChoice] = React.useState(false); + const downloadTriggerRef = React.useRef(null); React.useEffect(() => { - resolveBuzzDownloadUrl().then(setDownloadUrl); + let active = true; + detectBuzzDownloadPlatform(navigator).then(async (platform) => { + if (!active) return; + if ( + platform.operatingSystem === "macos" && + platform.architecture === "unknown" + ) { + setNeedsMacChoice(true); + return; + } + const url = await resolveBuzzDownloadUrlForPlatform(platform); + if (active) setDownloadUrl(url); + }); + return () => { + active = false; + }; }, []); React.useEffect(() => { @@ -89,6 +109,26 @@ export function InvitePage({ code }: { code: string }) { ); const showDocument = (title: string, markdown: string) => setDocument({ title, markdown }); + const closeMacChoice = React.useCallback(() => { + setShowMacChoice(false); + window.setTimeout(() => downloadTriggerRef.current?.focus()); + }, []); + const chooseMacDownload = async ( + event: React.MouseEvent, + platform: BuzzDownloadPlatform, + ) => { + event.preventDefault(); + window.location.assign(await resolveBuzzDownloadUrlForPlatform(platform)); + }; + + React.useEffect(() => { + if (!showMacChoice) return; + const closeOnEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") closeMacChoice(); + }; + window.document.addEventListener("keydown", closeOnEscape); + return () => window.document.removeEventListener("keydown", closeOnEscape); + }, [closeMacChoice, showMacChoice]); return (
Don't have the app?{" "} { + if (!needsMacChoice) return; + event.preventDefault(); + setShowMacChoice(true); + }} > Download it now

+ {showMacChoice && ( +
{ + if (event.currentTarget === event.target) closeMacChoice(); + }} + > +
+
+
+

+ Which Mac do you have? +

+

+ Choose based on when your Mac was released. +

+
+ +
+
+ + void chooseMacDownload(event, { + operatingSystem: "macos", + architecture: "arm64", + }) + } + > + Newer Mac + + 2021 or later, or a late-2020 Mac with an Apple M1 chip + + + + void chooseMacDownload(event, { + operatingSystem: "macos", + architecture: "x64", + }) + } + > + Older Mac + + 2019 or earlier, or a 2020 Mac with an Intel processor + + +
+

+ Not sure? Open the Apple menu and choose{" "} + About This Mac. “Chip: Apple M…” means Newer Mac. + “Processor: Intel” means Older Mac. +

+
+
+ )} + {document && (
{ - const platform = await detectBuzzDownloadPlatform(navigator); +export async function resolveBuzzDownloadUrlForPlatform( + platform: BuzzDownloadPlatform, +): Promise { try { const cached = JSON.parse(sessionStorage.getItem(CACHE_KEY) ?? "null") as { expiresAt: number; @@ -188,3 +187,9 @@ export async function resolveBuzzDownloadUrl(): Promise { return BUZZ_RELEASES_URL; } } + +export async function resolveBuzzDownloadUrl(): Promise { + return resolveBuzzDownloadUrlForPlatform( + await detectBuzzDownloadPlatform(navigator), + ); +} diff --git a/web/tests/e2e/smoke.spec.ts b/web/tests/e2e/smoke.spec.ts index 55c8b0a9034..d06caeca1b2 100644 --- a/web/tests/e2e/smoke.spec.ts +++ b/web/tests/e2e/smoke.spec.ts @@ -117,6 +117,52 @@ test("invite requires age and legal consent before opening Buzz", async ({ expect(consentBox?.width).toBe(acceptButtonBox?.width); }); +test("invite asks Safari users to choose their Mac download", async ({ + browser, +}) => { + const context = await browser.newContext({ + userAgent: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Version/26.5 Safari/605.1.15", + }); + await context.addInitScript(() => { + Object.defineProperties(navigator, { + platform: { configurable: true, value: "MacIntel" }, + maxTouchPoints: { configurable: true, value: 0 }, + userAgentData: { configurable: true, value: undefined }, + }); + }); + const page = await context.newPage(); + await page.route("**/api/join-policy", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ policy: null }), + }); + }); + + await page.goto("/invite/demo-code"); + const download = page.getByRole("link", { name: "Download it now" }); + await expect(download).toHaveAttribute("aria-haspopup", "dialog"); + await download.click(); + + const chooser = page.getByRole("dialog", { + name: "Which Mac do you have?", + }); + await expect(chooser).toBeVisible(); + await expect(chooser.getByRole("link", { name: /Newer Mac/ })).toContainText( + "2021 or later, or a late-2020 Mac with an Apple M1 chip", + ); + await expect(chooser.getByRole("link", { name: /Older Mac/ })).toContainText( + "2019 or earlier, or a 2020 Mac with an Intel processor", + ); + await expect(chooser.getByText("About This Mac")).toBeVisible(); + + await page.keyboard.press("Escape"); + await expect(chooser).toBeHidden(); + await expect(download).toBeFocused(); + await context.close(); +}); + test("invite download falls back for mobile and non-desktop devices", async ({ browser, }) => { From 591ffcf9037b43596eb704f643c26e8570a90419 Mon Sep 17 00:00:00 2001 From: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co> Date: Sat, 18 Jul 2026 16:13:32 -0700 Subject: [PATCH 2/2] fix(web): preserve invite during Mac download Co-authored-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co> --- web/src/features/invite/ui/InvitePage.tsx | 22 +++++++++++++++++++--- web/tests/e2e/smoke.spec.ts | 13 +++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/web/src/features/invite/ui/InvitePage.tsx b/web/src/features/invite/ui/InvitePage.tsx index de12e219d49..7415ad1dfe9 100644 --- a/web/src/features/invite/ui/InvitePage.tsx +++ b/web/src/features/invite/ui/InvitePage.tsx @@ -36,6 +36,8 @@ export function InvitePage({ code }: { code: string }) { const [downloadUrl, setDownloadUrl] = React.useState(BUZZ_RELEASES_URL); const [needsMacChoice, setNeedsMacChoice] = React.useState(false); const [showMacChoice, setShowMacChoice] = React.useState(false); + const [choosingMacDownload, setChoosingMacDownload] = React.useState(false); + const choosingMacDownloadRef = React.useRef(false); const downloadTriggerRef = React.useRef(null); React.useEffect(() => { @@ -118,7 +120,19 @@ export function InvitePage({ code }: { code: string }) { platform: BuzzDownloadPlatform, ) => { event.preventDefault(); - window.location.assign(await resolveBuzzDownloadUrlForPlatform(platform)); + if (choosingMacDownloadRef.current) return; + choosingMacDownloadRef.current = true; + setChoosingMacDownload(true); + const downloadWindow = window.open("about:blank", "_blank"); + if (downloadWindow) downloadWindow.opener = null; + setShowMacChoice(false); + try { + const url = await resolveBuzzDownloadUrlForPlatform(platform); + downloadWindow?.location.replace(url); + } finally { + choosingMacDownloadRef.current = false; + setChoosingMacDownload(false); + } }; React.useEffect(() => { @@ -246,7 +260,8 @@ export function InvitePage({ code }: { code: string }) {
void chooseMacDownload(event, { @@ -261,7 +276,8 @@ export function InvitePage({ code }: { code: string }) { void chooseMacDownload(event, { diff --git a/web/tests/e2e/smoke.spec.ts b/web/tests/e2e/smoke.spec.ts index d06caeca1b2..9f901448b8b 100644 --- a/web/tests/e2e/smoke.spec.ts +++ b/web/tests/e2e/smoke.spec.ts @@ -139,6 +139,9 @@ test("invite asks Safari users to choose their Mac download", async ({ body: JSON.stringify({ policy: null }), }); }); + await page.route("https://api.github.com/**", async (route) => { + await route.fulfill({ status: 500 }); + }); await page.goto("/invite/demo-code"); const download = page.getByRole("link", { name: "Download it now" }); @@ -157,6 +160,16 @@ test("invite asks Safari users to choose their Mac download", async ({ ); await expect(chooser.getByText("About This Mac")).toBeVisible(); + const openedPagePromise = context.waitForEvent("page"); + await chooser.getByRole("link", { name: /Newer Mac/ }).click(); + const openedPage = await openedPagePromise; + await expect(chooser).toBeHidden(); + await expect(openedPage).toHaveURL("https://github.com/block/buzz/releases"); + await expect(page).toHaveURL(/\/invite\/demo-code$/); + await openedPage.close(); + + await download.click(); + await expect(chooser).toBeVisible(); await page.keyboard.press("Escape"); await expect(chooser).toBeHidden(); await expect(download).toBeFocused();