Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion web/src/shared/lib/buzz-download.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
export const BUZZ_RELEASES_URL = "https://github.com/block/buzz/releases";
export const BUZZ_IOS_APP_STORE_URL =
"https://apps.apple.com/us/app/buzz-chat-with-your-hive/id6779728271";
export const BUZZ_ANDROID_PLAY_STORE_URL =
"https://play.google.com/store/apps/details?id=xyz.block.buzz.mobile";
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";
operatingSystem:
| "linux"
| "macos"
| "windows"
| "ios"
| "android"
| "unknown";
architecture: "arm64" | "x64" | "unknown";
};

// iOS and Android are supported platforms that take a store listing instead of
// a GitHub release asset, so they resolve before the release lookup runs.
const STORE_URLS: Partial<
Record<BuzzDownloadPlatform["operatingSystem"], string>
> = {
ios: BUZZ_IOS_APP_STORE_URL,
android: BUZZ_ANDROID_PLAY_STORE_URL,
};

type GitHubRelease = {
draft: boolean;
prerelease: boolean;
Expand Down Expand Up @@ -39,6 +58,24 @@ function normalizeOperatingSystem(
// Reject non-desktop devices before admitting desktop-looking signals.
const isIPadDesktopMode =
platform === "macintel" && navigatorValue.maxTouchPoints > 1;

// Phones and tablets get a store listing, so classify the two platforms the
// mobile app actually ships on before the catch-all below folds every
// non-desktop device into `unknown`. Fire tablets (kindle/silk) and ChromeOS
// stay `unknown` on purpose: neither is served by the Play Store listing.
if (!/kindle|silk|cros/.test(userAgent)) {
if (
isIPadDesktopMode ||
platform === "ios" ||
/iphone|ipad|ipod/.test(userAgent)
) {
return "ios";
}
if (platform === "android" || /android/.test(userAgent)) {
return "android";
}
}

const isUnsupportedDevice =
userAgentData?.mobile === true ||
isIPadDesktopMode ||
Expand Down Expand Up @@ -142,6 +179,11 @@ export function selectBuzzDownloadUrl(
export async function resolveBuzzDownloadUrlForPlatform(
platform: BuzzDownloadPlatform,
): Promise<string> {
const storeUrl = STORE_URLS[platform.operatingSystem];
if (storeUrl) {
return storeUrl;
}

try {
const cached = JSON.parse(sessionStorage.getItem(CACHE_KEY) ?? "null") as {
expiresAt: number;
Expand Down
29 changes: 25 additions & 4 deletions web/tests/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,40 +260,61 @@ test("invite asks Safari users to choose their Mac download", async ({
await context.close();
});

test("invite download falls back for mobile and non-desktop devices", async ({
const APP_STORE_URL =
"https://apps.apple.com/us/app/buzz-chat-with-your-hive/id6779728271";
const PLAY_STORE_URL =
"https://play.google.com/store/apps/details?id=xyz.block.buzz.mobile";
const RELEASES_URL = "https://github.com/block/buzz/releases";

test("invite download sends each non-desktop device where its app lives", async ({
browser,
}) => {
const unsupportedDevices = [
const nonDesktopDevices = [
{
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,
expected: APP_STORE_URL,
},
{
name: "iPadOS desktop mode",
platform: "MacIntel",
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15",
maxTouchPoints: 5,
expected: APP_STORE_URL,
},
{
name: "Android phone",
platform: "Linux armv8l",
userAgent:
"Mozilla/5.0 (Linux; Android 15; Pixel 9 Pro) AppleWebKit/537.36 Mobile",
maxTouchPoints: 5,
expected: PLAY_STORE_URL,
},
// No store listing serves these two, so they keep the releases-page
// fallback: ChromeOS is not a Play Store target here, and Fire tablets
// ship through Amazon's Appstore.
{
name: "ChromeOS",
platform: "Linux x86_64",
userAgent: "Mozilla/5.0 (X11; CrOS x86_64 16093.68.0) AppleWebKit/537.36",
maxTouchPoints: 0,
expected: RELEASES_URL,
},
{
name: "Fire tablet (Silk)",
platform: "Linux armv8l",
userAgent:
"Mozilla/5.0 (Linux; Android 9; KFMAWI) AppleWebKit/537.36 Silk/126.0 like Chrome/126.0.0.0 Safari/537.36",
maxTouchPoints: 5,
expected: RELEASES_URL,
},
];

for (const device of unsupportedDevices) {
for (const device of nonDesktopDevices) {
const context = await browser.newContext({ userAgent: device.userAgent });
await context.addInitScript(({ platform, maxTouchPoints }) => {
Object.defineProperties(navigator, {
Expand Down Expand Up @@ -343,7 +364,7 @@ test("invite download falls back for mobile and non-desktop devices", async ({
await expect(
page.getByRole("link", { name: "Download it now" }),
device.name,
).toHaveAttribute("href", "https://github.com/block/buzz/releases");
).toHaveAttribute("href", device.expected);
await context.close();
}
});