mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
fix(web): route mobile invite downloads to app stores (#7702)
Route the invite page’s existing download link to the App Store on iOS/iPadOS and Google Play on Android. Appearance and app-opening behavior stay unchanged. The browser test checks platform routing using shared URL constants. Web build, lint, and all six smoke tests pass. Rebased onto main to pick up the relay test compilation fix. Refs #3357. Alternative to #3464 with a smaller implementation and no duplicated store URLs in tests. --------- Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
@@ -1,11 +1,22 @@
|
||||
export const BUZZ_RELEASES_URL = "https://github.com/block/buzz/releases";
|
||||
/** Official iOS download destination. */
|
||||
export const BUZZ_IOS_APP_STORE_URL = "https://apps.apple.com/app/id6779728271";
|
||||
/** Official Android download destination. */
|
||||
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";
|
||||
};
|
||||
|
||||
@@ -36,13 +47,17 @@ function normalizeOperatingSystem(
|
||||
|
||||
// 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.
|
||||
// Classify mobile devices before admitting desktop-looking signals.
|
||||
const isIPadDesktopMode =
|
||||
platform === "macintel" && navigatorValue.maxTouchPoints > 1;
|
||||
if (isIPadDesktopMode || /iphone|ipad|ipod/.test(userAgent)) return "ios";
|
||||
// Fire OS identifies as Android but does not ship with Google Play.
|
||||
if (/kindle|silk/.test(userAgent)) return "unknown";
|
||||
if (/android/.test(userAgent) || platform === "android") return "android";
|
||||
|
||||
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(
|
||||
/mobile|tablet|windows phone|iemobile|opera mini|opera mobi|webos|blackberry|bb10|kindle|silk|kaios|cros/.test(
|
||||
userAgent,
|
||||
);
|
||||
if (isUnsupportedDevice) return "unknown";
|
||||
@@ -87,6 +102,9 @@ export async function detectBuzzDownloadPlatform(
|
||||
navigatorValue,
|
||||
userAgentData,
|
||||
);
|
||||
if (operatingSystem === "ios" || operatingSystem === "android") {
|
||||
return { operatingSystem, architecture: "unknown" };
|
||||
}
|
||||
let architecture = normalizeArchitecture(navigatorValue.userAgent);
|
||||
|
||||
if (userAgentData?.getHighEntropyValues) {
|
||||
@@ -142,6 +160,10 @@ export function selectBuzzDownloadUrl(
|
||||
export async function resolveBuzzDownloadUrlForPlatform(
|
||||
platform: BuzzDownloadPlatform,
|
||||
): Promise<string> {
|
||||
if (platform.operatingSystem === "ios") return BUZZ_IOS_APP_STORE_URL;
|
||||
if (platform.operatingSystem === "android")
|
||||
return BUZZ_ANDROID_PLAY_STORE_URL;
|
||||
|
||||
try {
|
||||
const cached = JSON.parse(sessionStorage.getItem(CACHE_KEY) ?? "null") as {
|
||||
expiresAt: number;
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import {
|
||||
BUZZ_ANDROID_PLAY_STORE_URL,
|
||||
BUZZ_IOS_APP_STORE_URL,
|
||||
BUZZ_RELEASES_URL,
|
||||
} from "../../src/shared/lib/buzz-download";
|
||||
|
||||
test("home page loads with Buzz branding", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
@@ -260,12 +265,13 @@ 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 ({
|
||||
test("invite download links to the appropriate platform destination", async ({
|
||||
browser,
|
||||
}) => {
|
||||
const unsupportedDevices = [
|
||||
const devices = [
|
||||
{
|
||||
name: "iPhone Safari",
|
||||
expectedUrl: BUZZ_IOS_APP_STORE_URL,
|
||||
platform: "iPhone",
|
||||
userAgent:
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15",
|
||||
@@ -273,6 +279,7 @@ test("invite download falls back for mobile and non-desktop devices", async ({
|
||||
},
|
||||
{
|
||||
name: "iPadOS desktop mode",
|
||||
expectedUrl: BUZZ_IOS_APP_STORE_URL,
|
||||
platform: "MacIntel",
|
||||
userAgent:
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15",
|
||||
@@ -280,20 +287,30 @@ test("invite download falls back for mobile and non-desktop devices", async ({
|
||||
},
|
||||
{
|
||||
name: "Android phone",
|
||||
expectedUrl: BUZZ_ANDROID_PLAY_STORE_URL,
|
||||
platform: "Linux armv8l",
|
||||
userAgent:
|
||||
"Mozilla/5.0 (Linux; Android 15; Pixel 9 Pro) AppleWebKit/537.36 Mobile",
|
||||
maxTouchPoints: 5,
|
||||
},
|
||||
{
|
||||
name: "Fire tablet",
|
||||
expectedUrl: BUZZ_RELEASES_URL,
|
||||
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,
|
||||
},
|
||||
{
|
||||
name: "ChromeOS",
|
||||
expectedUrl: BUZZ_RELEASES_URL,
|
||||
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) {
|
||||
for (const device of devices) {
|
||||
const context = await browser.newContext({ userAgent: device.userAgent });
|
||||
await context.addInitScript(({ platform, maxTouchPoints }) => {
|
||||
Object.defineProperties(navigator, {
|
||||
@@ -301,7 +318,12 @@ test("invite download falls back for mobile and non-desktop devices", async ({
|
||||
maxTouchPoints: { configurable: true, value: maxTouchPoints },
|
||||
userAgentData: {
|
||||
configurable: true,
|
||||
value: { platform, mobile: maxTouchPoints > 0 },
|
||||
value: {
|
||||
platform,
|
||||
mobile: maxTouchPoints > 0,
|
||||
// Store routing must not wait for desktop architecture hints.
|
||||
getHighEntropyValues: () => new Promise(() => {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
}, device);
|
||||
@@ -343,7 +365,13 @@ 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.expectedUrl);
|
||||
await context.route(device.expectedUrl, (route) =>
|
||||
route.fulfill({ contentType: "text/html", body: "Store destination" }),
|
||||
);
|
||||
const destination = context.waitForEvent("page");
|
||||
await page.getByRole("link", { name: "Download it now" }).click();
|
||||
await expect(await destination).toHaveURL(device.expectedUrl);
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user