fix(web): ask Safari users for Mac type (#2090)

Signed-off-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Bradley Axen
2026-07-19 09:14:21 -07:00
committed by GitHub
co-authored by npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79
parent cab47905a8
commit 820d023305
3 changed files with 207 additions and 9 deletions
+136 -2
View File
@@ -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,29 @@ 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 [choosingMacDownload, setChoosingMacDownload] = React.useState(false);
const choosingMacDownloadRef = React.useRef(false);
const downloadTriggerRef = React.useRef<HTMLAnchorElement>(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 +111,38 @@ 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<HTMLAnchorElement>,
platform: BuzzDownloadPlatform,
) => {
event.preventDefault();
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(() => {
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 (
<div
@@ -157,16 +211,96 @@ export function InvitePage({ code }: { code: string }) {
<p className="flex h-[3.125rem] items-center justify-center rounded-2xl bg-white text-sm text-black/60">
Don&apos;t have the app?{" "}
<a
aria-expanded={needsMacChoice ? showMacChoice : undefined}
aria-haspopup={needsMacChoice ? "dialog" : undefined}
className="ml-1 font-medium text-black underline-offset-4 hover:text-black/70 hover:underline focus-visible:underline"
href={downloadUrl}
ref={downloadTriggerRef}
rel="noreferrer"
target="_blank"
onClick={(event) => {
if (!needsMacChoice) return;
event.preventDefault();
setShowMacChoice(true);
}}
>
Download it now
</a>
</p>
</div>
{showMacChoice && (
<div
aria-label="Which Mac do you have?"
aria-modal="true"
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 text-left"
role="dialog"
onMouseDown={(event) => {
if (event.currentTarget === event.target) closeMacChoice();
}}
>
<div className="w-full max-w-lg rounded-3xl bg-white p-7 text-black shadow-xl sm:p-9">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-2xl font-semibold tracking-tight">
Which Mac do you have?
</h2>
<p className="mt-2 text-sm text-black/60">
Choose based on when your Mac was released.
</p>
</div>
<button
aria-label="Close"
className="text-2xl leading-none text-black/60 hover:text-black"
type="button"
onClick={closeMacChoice}
>
×
</button>
</div>
<div className="mt-6 grid gap-3">
<a
aria-disabled={choosingMacDownload}
className="rounded-2xl border border-black p-5 text-black no-underline hover:bg-black hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-black aria-disabled:pointer-events-none aria-disabled:opacity-50"
href={BUZZ_RELEASES_URL}
onClick={(event) =>
void chooseMacDownload(event, {
operatingSystem: "macos",
architecture: "arm64",
})
}
>
<strong className="block text-lg">Newer Mac</strong>
<span className="mt-1 block text-sm">
2021 or later, or a late-2020 Mac with an Apple M1 chip
</span>
</a>
<a
aria-disabled={choosingMacDownload}
className="rounded-2xl border border-black p-5 text-black no-underline hover:bg-black hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-black aria-disabled:pointer-events-none aria-disabled:opacity-50"
href={BUZZ_RELEASES_URL}
onClick={(event) =>
void chooseMacDownload(event, {
operatingSystem: "macos",
architecture: "x64",
})
}
>
<strong className="block text-lg">Older Mac</strong>
<span className="mt-1 block text-sm">
2019 or earlier, or a 2020 Mac with an Intel processor
</span>
</a>
</div>
<p className="mt-5 text-sm leading-5">
<strong>Not sure?</strong> Open the Apple menu and choose{" "}
<strong>About This Mac</strong>. Chip: Apple M means Newer Mac.
Processor: Intel means Older Mac.
</p>
</div>
</div>
)}
{document && (
<div
aria-label={document.title}
+12 -7
View File
@@ -110,11 +110,9 @@ export async function detectBuzzDownloadPlatform(
function assetPattern(platform: BuzzDownloadPlatform): RegExp | undefined {
switch (platform.operatingSystem) {
case "macos":
// Safari withholds CPU architecture and reports MacIntel on Apple
// Silicon. The Intel build remains compatible there through Rosetta.
return platform.architecture === "arm64"
? /_aarch64\.dmg$/i
: /_x64\.dmg$/i;
if (platform.architecture === "arm64") return /_aarch64\.dmg$/i;
if (platform.architecture === "x64") return /_x64\.dmg$/i;
return undefined;
case "windows":
return /_x64-setup[^/]*\.exe$/i;
case "linux":
@@ -141,8 +139,9 @@ export function selectBuzzDownloadUrl(
return undefined;
}
export async function resolveBuzzDownloadUrl(): Promise<string> {
const platform = await detectBuzzDownloadPlatform(navigator);
export async function resolveBuzzDownloadUrlForPlatform(
platform: BuzzDownloadPlatform,
): Promise<string> {
try {
const cached = JSON.parse(sessionStorage.getItem(CACHE_KEY) ?? "null") as {
expiresAt: number;
@@ -188,3 +187,9 @@ export async function resolveBuzzDownloadUrl(): Promise<string> {
return BUZZ_RELEASES_URL;
}
}
export async function resolveBuzzDownloadUrl(): Promise<string> {
return resolveBuzzDownloadUrlForPlatform(
await detectBuzzDownloadPlatform(navigator),
);
}
+59
View File
@@ -117,6 +117,65 @@ 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.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" });
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();
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();
await context.close();
});
test("invite download falls back for mobile and non-desktop devices", async ({
browser,
}) => {