diff --git a/web/src/shared/lib/buzz-download.ts b/web/src/shared/lib/buzz-download.ts index 3c198382b4..73dc604003 100644 --- a/web/src/shared/lib/buzz-download.ts +++ b/web/src/shared/lib/buzz-download.ts @@ -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 +> = { + ios: BUZZ_IOS_APP_STORE_URL, + android: BUZZ_ANDROID_PLAY_STORE_URL, +}; + type GitHubRelease = { draft: boolean; prerelease: boolean; @@ -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 || @@ -142,6 +179,11 @@ export function selectBuzzDownloadUrl( export async function resolveBuzzDownloadUrlForPlatform( platform: BuzzDownloadPlatform, ): Promise { + const storeUrl = STORE_URLS[platform.operatingSystem]; + if (storeUrl) { + return storeUrl; + } + try { const cached = JSON.parse(sessionStorage.getItem(CACHE_KEY) ?? "null") as { expiresAt: number; diff --git a/web/tests/e2e/smoke.spec.ts b/web/tests/e2e/smoke.spec.ts index 22e2628165..57c5f86fe3 100644 --- a/web/tests/e2e/smoke.spec.ts +++ b/web/tests/e2e/smoke.spec.ts @@ -260,16 +260,23 @@ 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", @@ -277,6 +284,7 @@ test("invite download falls back for mobile and non-desktop devices", async ({ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15", maxTouchPoints: 5, + expected: APP_STORE_URL, }, { name: "Android phone", @@ -284,16 +292,29 @@ test("invite download falls back for mobile and non-desktop devices", async ({ 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, { @@ -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(); } });