fix: resolve invite downloads by platform (#2001)

Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Kalvin C
2026-07-17 07:55:50 -07:00
committed by GitHub
co-authored by npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7
parent 084e442d2d
commit 57316a378e
3 changed files with 331 additions and 2 deletions
+10 -2
View File
@@ -1,4 +1,8 @@
import buzzAppIcon from "@/assets/app-icon@3x.png";
import {
BUZZ_RELEASES_URL,
resolveBuzzDownloadUrl,
} from "@/shared/lib/buzz-download";
import { relayWsUrl } from "@/shared/lib/relay-url";
import { Button } from "@/shared/ui/button";
import * as React from "react";
@@ -7,7 +11,6 @@ import remarkGfm from "remark-gfm";
import { InviteJoinPolicyNotice } from "./InviteJoinPolicyNotice";
const DOWNLOAD_URL = "https://github.com/block/buzz/releases/latest";
type JoinPolicy = {
terms_markdown?: string;
privacy_markdown?: string;
@@ -28,6 +31,11 @@ export function InvitePage({ code }: { code: string }) {
const [ageConfirmed, setAgeConfirmed] = React.useState(false);
const [agreementConfirmed, setAgreementConfirmed] = React.useState(false);
const [opening, setOpening] = React.useState(false);
const [downloadUrl, setDownloadUrl] = React.useState(BUZZ_RELEASES_URL);
React.useEffect(() => {
resolveBuzzDownloadUrl().then(setDownloadUrl);
}, []);
React.useEffect(() => {
fetch("/api/join-policy")
@@ -150,7 +158,7 @@ export function InvitePage({ code }: { code: string }) {
Don&apos;t have the app?{" "}
<a
className="ml-1 font-medium text-black underline-offset-4 hover:text-black/70 hover:underline focus-visible:underline"
href={DOWNLOAD_URL}
href={downloadUrl}
rel="noreferrer"
target="_blank"
>
+190
View File
@@ -0,0 +1,190 @@
export const BUZZ_RELEASES_URL = "https://github.com/block/buzz/releases";
const BUZZ_RELEASES_API_URL =
"https://api.github.com/repos/block/buzz/releases?per_page=10";
const CACHE_KEY = "buzz.latestDownload.v1";
const CACHE_TTL_MS = 60 * 60 * 1000;
export type BuzzDownloadPlatform = {
operatingSystem: "linux" | "macos" | "windows" | "unknown";
architecture: "arm64" | "x64" | "unknown";
};
type GitHubRelease = {
draft: boolean;
prerelease: boolean;
assets: Array<{ name: string; browser_download_url: string }>;
};
type UserAgentData = {
platform?: string;
mobile?: boolean;
getHighEntropyValues?: (
hints: string[],
) => Promise<{ architecture?: string; bitness?: string }>;
};
function normalizeOperatingSystem(
navigatorValue: Navigator,
userAgentData?: UserAgentData,
): BuzzDownloadPlatform["operatingSystem"] {
const userAgent = navigatorValue.userAgent.toLowerCase();
const platform = (
userAgentData?.platform ??
navigatorValue.platform ??
""
).toLowerCase();
// Compatibility tokens are treacherous: iPadOS can report MacIntel and a
// Macintosh UA, while Android and ChromeOS expose Linux platform strings.
// Reject non-desktop devices before admitting desktop-looking signals.
const isIPadDesktopMode =
platform === "macintel" && navigatorValue.maxTouchPoints > 1;
const isUnsupportedDevice =
userAgentData?.mobile === true ||
isIPadDesktopMode ||
/android|iphone|ipad|ipod|mobile|tablet|windows phone|iemobile|opera mini|opera mobi|webos|blackberry|bb10|kindle|silk|kaios|cros/.test(
userAgent,
);
if (isUnsupportedDevice) return "unknown";
if (
platform === "macos" ||
platform.startsWith("mac") ||
userAgent.includes("macintosh")
)
return "macos";
if (
platform === "windows" ||
platform.startsWith("win") ||
userAgent.includes("windows nt")
)
return "windows";
if (
platform === "linux" ||
platform.startsWith("linux") ||
userAgent.includes("linux")
)
return "linux";
return "unknown";
}
function normalizeArchitecture(
value: string,
): BuzzDownloadPlatform["architecture"] {
const normalized = value.toLowerCase();
if (/arm|aarch64/.test(normalized)) return "arm64";
if (/x86|x64|amd64|64/.test(normalized)) return "x64";
return "unknown";
}
export async function detectBuzzDownloadPlatform(
navigatorValue: Navigator,
): Promise<BuzzDownloadPlatform> {
const userAgentData = (
navigatorValue as Navigator & { userAgentData?: UserAgentData }
).userAgentData;
const operatingSystem = normalizeOperatingSystem(
navigatorValue,
userAgentData,
);
let architecture = normalizeArchitecture(navigatorValue.userAgent);
if (userAgentData?.getHighEntropyValues) {
try {
const values = await userAgentData.getHighEntropyValues([
"architecture",
"bitness",
]);
architecture = normalizeArchitecture(
`${values.architecture ?? ""} ${values.bitness ?? ""}`,
);
} catch {
// Privacy settings may reject high-entropy client hints. The matcher
// below applies the safest compatible fallback for the detected OS.
}
}
return { operatingSystem, architecture };
}
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;
case "windows":
return /_x64-setup[^/]*\.exe$/i;
case "linux":
return platform.architecture === "arm64"
? undefined
: /_amd64\.AppImage$/i;
default:
return undefined;
}
}
export function selectBuzzDownloadUrl(
releases: GitHubRelease[],
platform: BuzzDownloadPlatform,
): string | undefined {
const pattern = assetPattern(platform);
if (!pattern) return undefined;
for (const release of releases) {
if (release.draft || release.prerelease) continue;
const asset = release.assets.find(({ name }) => pattern.test(name));
if (asset) return asset.browser_download_url;
}
return undefined;
}
export async function resolveBuzzDownloadUrl(): Promise<string> {
const platform = await detectBuzzDownloadPlatform(navigator);
try {
const cached = JSON.parse(sessionStorage.getItem(CACHE_KEY) ?? "null") as {
expiresAt: number;
platform: BuzzDownloadPlatform;
url: string;
} | null;
if (
cached &&
cached.expiresAt > Date.now() &&
cached.platform.operatingSystem === platform.operatingSystem &&
cached.platform.architecture === platform.architecture
) {
return cached.url;
}
} catch {
// Storage is only an optimization.
}
try {
const response = await fetch(BUZZ_RELEASES_API_URL, {
headers: { Accept: "application/vnd.github+json" },
});
if (!response.ok) return BUZZ_RELEASES_URL;
const url = selectBuzzDownloadUrl(
(await response.json()) as GitHubRelease[],
platform,
);
if (!url) return BUZZ_RELEASES_URL;
try {
sessionStorage.setItem(
CACHE_KEY,
JSON.stringify({
expiresAt: Date.now() + CACHE_TTL_MS,
platform,
url,
}),
);
} catch {
// Storage is only an optimization.
}
return url;
} catch {
return BUZZ_RELEASES_URL;
}
}
+131
View File
@@ -27,8 +27,51 @@ test("invite requires age and legal consent before opening Buzz", async ({
}),
});
});
await page.route("https://api.github.com/**", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
headers: { "Access-Control-Allow-Origin": "*" },
body: JSON.stringify([
{ draft: false, prerelease: false, assets: [] },
{
draft: false,
prerelease: false,
assets: [
{
name: "Buzz_0.4.9_aarch64.dmg",
browser_download_url:
"https://github.com/block/buzz/releases/download/v0.4.9/Buzz_0.4.9_aarch64.dmg",
},
{
name: "Buzz_0.4.9_x64.dmg",
browser_download_url:
"https://github.com/block/buzz/releases/download/v0.4.9/Buzz_0.4.9_x64.dmg",
},
{
name: "Buzz_0.4.9_amd64.AppImage",
browser_download_url:
"https://github.com/block/buzz/releases/download/v0.4.9/Buzz_0.4.9_amd64.AppImage",
},
{
name: "Buzz_0.4.9_x64-setup_alpha-unsigned.exe",
browser_download_url:
"https://github.com/block/buzz/releases/download/v0.4.9/Buzz_0.4.9_x64-setup_alpha-unsigned.exe",
},
],
},
]),
});
});
await page.goto("/invite/demo-code");
await expect(
page.getByRole("link", { name: "Download it now" }),
).toHaveAttribute(
"href",
"https://github.com/block/buzz/releases/download/v0.4.9/Buzz_0.4.9_x64-setup_alpha-unsigned.exe",
);
const ageConfirmation = page.getByLabel("I am 18 years of age or older.");
const agreementConfirmation = page.getByLabel(
"I agree to the Buzz Terms of Service and Privacy Policy.",
@@ -73,3 +116,91 @@ test("invite requires age and legal consent before opening Buzz", async ({
expect(consentBox?.y).toBeLessThan(acceptButtonBox?.y ?? 0);
expect(consentBox?.width).toBe(acceptButtonBox?.width);
});
test("invite download falls back for mobile and non-desktop devices", async ({
browser,
}) => {
const unsupportedDevices = [
{
name: "iPhone Safari",
platform: "iPhone",
userAgent:
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15",
maxTouchPoints: 5,
},
{
name: "iPadOS desktop mode",
platform: "MacIntel",
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15",
maxTouchPoints: 5,
},
{
name: "Android phone",
platform: "Linux armv8l",
userAgent:
"Mozilla/5.0 (Linux; Android 15; Pixel 9 Pro) AppleWebKit/537.36 Mobile",
maxTouchPoints: 5,
},
{
name: "ChromeOS",
platform: "Linux x86_64",
userAgent: "Mozilla/5.0 (X11; CrOS x86_64 16093.68.0) AppleWebKit/537.36",
maxTouchPoints: 0,
},
];
for (const device of unsupportedDevices) {
const context = await browser.newContext({ userAgent: device.userAgent });
await context.addInitScript(({ platform, maxTouchPoints }) => {
Object.defineProperties(navigator, {
platform: { configurable: true, value: platform },
maxTouchPoints: { configurable: true, value: maxTouchPoints },
userAgentData: {
configurable: true,
value: { platform, mobile: maxTouchPoints > 0 },
},
});
}, device);
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: 200,
contentType: "application/json",
headers: { "Access-Control-Allow-Origin": "*" },
body: JSON.stringify([
{
draft: false,
prerelease: false,
assets: [
{
name: "Buzz_0.4.9_x64.dmg",
browser_download_url:
"https://github.com/block/buzz/releases/download/v0.4.9/Buzz_0.4.9_x64.dmg",
},
{
name: "Buzz_0.4.9_amd64.AppImage",
browser_download_url:
"https://github.com/block/buzz/releases/download/v0.4.9/Buzz_0.4.9_amd64.AppImage",
},
],
},
]),
});
});
await page.goto("/invite/demo-code");
await expect(
page.getByRole("link", { name: "Download it now" }),
device.name,
).toHaveAttribute("href", "https://github.com/block/buzz/releases");
await context.close();
}
});