From 47e9c8add59afe776ba4eaea0741e0ab2d9147b7 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Thu, 16 Jul 2026 15:11:27 +0800 Subject: [PATCH] fix(web): add macOS Intel downloads to landing page (#5517) Co-authored-by: Lambda Co-authored-by: multica-agent --- .../download/all-platforms.test.tsx | 57 +++++++++++++++++++ .../components/download/all-platforms.tsx | 25 ++++++-- .../landing/components/download/hero.test.ts | 29 ++++++++++ .../landing/components/download/hero.tsx | 27 ++++++--- apps/web/features/landing/i18n/en.ts | 14 ++--- apps/web/features/landing/i18n/ja.ts | 14 ++--- apps/web/features/landing/i18n/ko.ts | 14 ++--- apps/web/features/landing/i18n/types.ts | 8 +-- apps/web/features/landing/i18n/zh.ts | 12 ++-- apps/web/features/landing/utils/os-detect.ts | 6 +- .../utils/parse-release-assets.test.ts | 33 +++++++++++ .../landing/utils/parse-release-assets.ts | 12 +++- 12 files changed, 197 insertions(+), 54 deletions(-) create mode 100644 apps/web/features/landing/components/download/all-platforms.test.tsx create mode 100644 apps/web/features/landing/components/download/hero.test.ts create mode 100644 apps/web/features/landing/utils/parse-release-assets.test.ts diff --git a/apps/web/features/landing/components/download/all-platforms.test.tsx b/apps/web/features/landing/components/download/all-platforms.test.tsx new file mode 100644 index 0000000000..6d75480a7b --- /dev/null +++ b/apps/web/features/landing/components/download/all-platforms.test.tsx @@ -0,0 +1,57 @@ +import { render, screen, within } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { AllPlatforms } from "./all-platforms"; + +vi.mock("../../i18n", () => ({ + useLocale: () => ({ + t: { + download: { + allPlatforms: { + title: "All platforms", + macArm64Label: "macOS · Apple Silicon", + macX64Label: "macOS · Intel", + winX64Label: "Windows · x64", + winArm64Label: "Windows · ARM64", + linuxX64Label: "Linux · x64", + linuxArm64Label: "Linux · ARM64", + formatDmg: ".dmg", + formatZip: ".zip", + formatExe: ".exe", + formatAppImage: ".AppImage", + formatDeb: ".deb", + formatRpm: ".rpm", + unavailable: "Not available", + }, + footer: { allReleases: "View all releases" }, + }, + }, + }), +})); + +describe("AllPlatforms", () => { + it("lists Intel macOS downloads separately from Apple Silicon", () => { + render( + , + ); + + const intelLabel = screen.getByText("macOS · Intel"); + const intelRow = intelLabel.parentElement?.parentElement; + expect(intelRow).not.toBeNull(); + expect(within(intelRow!).getByRole("link", { name: ".dmg" })).toHaveAttribute( + "href", + "https://downloads.test/mac-x64.dmg", + ); + expect(within(intelRow!).getByRole("link", { name: ".zip" })).toHaveAttribute( + "href", + "https://downloads.test/mac-x64.zip", + ); + }); +}); diff --git a/apps/web/features/landing/components/download/all-platforms.tsx b/apps/web/features/landing/components/download/all-platforms.tsx index e0d07a185f..46e20a022b 100644 --- a/apps/web/features/landing/components/download/all-platforms.tsx +++ b/apps/web/features/landing/components/download/all-platforms.tsx @@ -35,7 +35,7 @@ export function AllPlatforms({
} - label={d.macLabel} + label={d.macArm64Label} formats={[ { label: d.formatDmg, @@ -48,6 +48,21 @@ export function AllPlatforms({ ]} unavailable={d.unavailable} /> + } + label={d.macX64Label} + formats={[ + { + label: d.formatDmg, + href: assets.macX64Dmg, + }, + { + label: d.formatZip, + href: assets.macX64Zip, + }, + ]} + unavailable={d.unavailable} + /> } label={d.winX64Label} @@ -111,10 +126,8 @@ export function AllPlatforms({ />
-

{d.intelNote}

- {isFallbackNeeded(assets) ? ( -

+

{ + it("links confidently detected Intel Macs to the x64 installers", () => { + const content = resolveContent( + { os: "mac", arch: "x64", archConfident: true }, + { + macArm64Dmg: "https://downloads.test/mac-arm64.dmg", + macArm64Zip: "https://downloads.test/mac-arm64.zip", + macX64Dmg: "https://downloads.test/mac-x64.dmg", + macX64Zip: "https://downloads.test/mac-x64.zip", + }, + false, + createEnDict(true).download.hero, + ); + + expect(content.primary).toEqual({ + href: "https://downloads.test/mac-x64.dmg", + label: "Download (.dmg)", + disabled: false, + }); + expect(content.alt).toEqual({ + href: "https://downloads.test/mac-x64.zip", + label: "or download .zip", + }); + }); +}); diff --git a/apps/web/features/landing/components/download/hero.tsx b/apps/web/features/landing/components/download/hero.tsx index e73a3b2e26..613c49ca66 100644 --- a/apps/web/features/landing/components/download/hero.tsx +++ b/apps/web/features/landing/components/download/hero.tsx @@ -96,7 +96,7 @@ interface HeroContent { type HeroDict = ReturnType["t"]["download"]["hero"]; -function resolveContent( +export function resolveContent( detected: DetectResult | null, assets: DownloadAssets, versionUnavailable: boolean, @@ -111,17 +111,28 @@ function resolveContent( if (detected.os === "mac") { // Only Chromium high-entropy returns arch confidently. Safari // always reports Intel even on Apple Silicon, so we treat - // "non-confident" as arm64 + add a small Intel disclaimer. + // "non-confident" as arm64 + point Intel users to the matrix below. if (detected.arch === "x64" && detected.archConfident) { + const dmg = assets.macX64Dmg; + const zip = assets.macX64Zip; return { title: d.macIntel.title, sub: d.macIntel.sub, - primary: { - href: "#cli", - label: d.macIntel.disabledCta, - disabled: true, - }, - hint: d.macIntel.intelHint, + primary: dmg + ? { + href: dmg, + label: d.macIntel.primary, + disabled: false, + } + : versionUnavailable + ? { href: "#", label: d.macIntel.primary, disabled: true } + : undefined, + alt: zip + ? { + href: zip, + label: d.macIntel.altZip, + } + : undefined, }; } const dmg = assets.macArm64Dmg; diff --git a/apps/web/features/landing/i18n/en.ts b/apps/web/features/landing/i18n/en.ts index 6f0959780b..c1876f1cce 100644 --- a/apps/web/features/landing/i18n/en.ts +++ b/apps/web/features/landing/i18n/en.ts @@ -2271,10 +2271,9 @@ export function createEnDict(allowSignup: boolean): LandingDict { }, macIntel: { title: "Multica for macOS", - sub: "Apple Silicon required — Intel Macs not yet supported.", - disabledCta: "Apple Silicon required", - intelHint: - "On an Intel Mac? Use the CLI below — it runs the same daemon.", + sub: "Intel · bundled daemon, zero setup", + primary: "Download (.dmg)", + altZip: "or download .zip", }, winX64: { title: "Multica for Windows", @@ -2296,12 +2295,13 @@ export function createEnDict(allowSignup: boolean): LandingDict { title: "Choose your platform", sub: "All installers are listed below.", }, - safariMacHint: "On an Intel Mac? Use the CLI below.", + safariMacHint: "On an Intel Mac? Choose the Intel download below.", archFallbackHint: "Wrong architecture? See all formats below.", }, allPlatforms: { title: "All platforms", - macLabel: "macOS · Apple Silicon", + macArm64Label: "macOS · Apple Silicon", + macX64Label: "macOS · Intel", winX64Label: "Windows · x64", winArm64Label: "Windows · ARM64", linuxX64Label: "Linux · x64", @@ -2312,8 +2312,6 @@ export function createEnDict(allowSignup: boolean): LandingDict { formatAppImage: ".AppImage", formatDeb: ".deb", formatRpm: ".rpm", - intelNote: - "Apple Silicon only — Intel Macs not supported in this release.", unavailable: "Not available", }, cli: { diff --git a/apps/web/features/landing/i18n/ja.ts b/apps/web/features/landing/i18n/ja.ts index 9c80653942..f92b5fffd9 100644 --- a/apps/web/features/landing/i18n/ja.ts +++ b/apps/web/features/landing/i18n/ja.ts @@ -1773,10 +1773,9 @@ export function createJaDict(allowSignup: boolean): LandingDict { }, macIntel: { title: "macOS 版 Multica", - sub: "Apple Silicon が必要です。Intel Mac はまだ対応していません。", - disabledCta: "Apple Silicon が必要", - intelHint: - "Intel Mac をお使いですか? 下の CLI をご利用ください。同じデーモンが動きます。", + sub: "Intel · デーモン同梱、設定不要", + primary: "ダウンロード(.dmg)", + altZip: "または .zip をダウンロード", }, winX64: { title: "Windows 版 Multica", @@ -1798,12 +1797,13 @@ export function createJaDict(allowSignup: boolean): LandingDict { title: "プラットフォームを選択", sub: "すべてのインストーラーは下にまとまっています。", }, - safariMacHint: "Intel Mac をお使いですか? 下の CLI をご利用ください。", + safariMacHint: "Intel Mac をお使いですか? 下の Intel 版を選択してください。", archFallbackHint: "アーキテクチャが合いませんか? 下ですべての形式を確認してください。", }, allPlatforms: { title: "すべてのプラットフォーム", - macLabel: "macOS · Apple Silicon", + macArm64Label: "macOS · Apple Silicon", + macX64Label: "macOS · Intel", winX64Label: "Windows · x64", winArm64Label: "Windows · ARM64", linuxX64Label: "Linux · x64", @@ -1814,8 +1814,6 @@ export function createJaDict(allowSignup: boolean): LandingDict { formatAppImage: ".AppImage", formatDeb: ".deb", formatRpm: ".rpm", - intelNote: - "Apple Silicon のみ対応です。このリリースでは Intel Mac には対応していません。", unavailable: "利用できません", }, cli: { diff --git a/apps/web/features/landing/i18n/ko.ts b/apps/web/features/landing/i18n/ko.ts index 8b1fcd361c..8172d45530 100644 --- a/apps/web/features/landing/i18n/ko.ts +++ b/apps/web/features/landing/i18n/ko.ts @@ -1798,10 +1798,9 @@ export function createKoDict(allowSignup: boolean): LandingDict { }, macIntel: { title: "macOS용 Multica", - sub: "Apple Silicon이 필요합니다. Intel Mac은 아직 지원하지 않습니다.", - disabledCta: "Apple Silicon 필요", - intelHint: - "Intel Mac을 사용 중인가요? 아래 CLI를 사용하세요. 같은 데몬이 실행됩니다.", + sub: "Intel · 데몬 포함, 별도 설정 없음", + primary: "다운로드(.dmg)", + altZip: "또는 .zip 다운로드", }, winX64: { title: "Windows용 Multica", @@ -1823,12 +1822,13 @@ export function createKoDict(allowSignup: boolean): LandingDict { title: "플랫폼 선택", sub: "모든 설치 파일은 아래에 정리되어 있습니다.", }, - safariMacHint: "Intel Mac을 사용 중인가요? 아래 CLI를 사용하세요.", + safariMacHint: "Intel Mac을 사용 중인가요? 아래에서 Intel 버전을 선택하세요.", archFallbackHint: "아키텍처가 맞지 않나요? 아래에서 모든 형식을 확인하세요.", }, allPlatforms: { title: "모든 플랫폼", - macLabel: "macOS · Apple Silicon", + macArm64Label: "macOS · Apple Silicon", + macX64Label: "macOS · Intel", winX64Label: "Windows · x64", winArm64Label: "Windows · ARM64", linuxX64Label: "Linux · x64", @@ -1839,8 +1839,6 @@ export function createKoDict(allowSignup: boolean): LandingDict { formatAppImage: ".AppImage", formatDeb: ".deb", formatRpm: ".rpm", - intelNote: - "Apple Silicon만 지원합니다. 이번 릴리스에서는 Intel Mac을 지원하지 않습니다.", unavailable: "사용할 수 없음", }, cli: { diff --git a/apps/web/features/landing/i18n/types.ts b/apps/web/features/landing/i18n/types.ts index 48ba946179..7731ca5dfe 100644 --- a/apps/web/features/landing/i18n/types.ts +++ b/apps/web/features/landing/i18n/types.ts @@ -145,8 +145,8 @@ export type LandingDict = { macIntel: { title: string; sub: string; - disabledCta: string; - intelHint: string; + primary: string; + altZip: string; }; winX64: { title: string; sub: string; primary: string }; winArm64: { title: string; sub: string; primary: string }; @@ -162,7 +162,8 @@ export type LandingDict = { }; allPlatforms: { title: string; - macLabel: string; + macArm64Label: string; + macX64Label: string; winX64Label: string; winArm64Label: string; linuxX64Label: string; @@ -173,7 +174,6 @@ export type LandingDict = { formatAppImage: string; formatDeb: string; formatRpm: string; - intelNote: string; unavailable: string; }; cli: { diff --git a/apps/web/features/landing/i18n/zh.ts b/apps/web/features/landing/i18n/zh.ts index 23bde8ebc7..4e25340f6d 100644 --- a/apps/web/features/landing/i18n/zh.ts +++ b/apps/web/features/landing/i18n/zh.ts @@ -2270,9 +2270,9 @@ export function createZhDict(allowSignup: boolean): LandingDict { }, macIntel: { title: "Multica for macOS", - sub: "需要 Apple Silicon——暂不支持 Intel Mac。", - disabledCta: "需要 Apple Silicon", - intelHint: "在 Intel Mac 上?请使用下方 CLI——底层跑的是同一个 daemon。", + sub: "Intel · 内置守护进程,无需配置", + primary: "下载 (.dmg)", + altZip: "或下载 .zip", }, winX64: { title: "Multica for Windows", @@ -2294,12 +2294,13 @@ export function createZhDict(allowSignup: boolean): LandingDict { title: "选择你的平台", sub: "下方是所有支持的安装包。", }, - safariMacHint: "在 Intel Mac 上?请使用下方 CLI。", + safariMacHint: "在 Intel Mac 上?请在下方选择 Intel 版本。", archFallbackHint: "架构不对?下方是所有可选格式。", }, allPlatforms: { title: "所有平台", - macLabel: "macOS · Apple Silicon", + macArm64Label: "macOS · Apple Silicon", + macX64Label: "macOS · Intel", winX64Label: "Windows · x64", winArm64Label: "Windows · ARM64", linuxX64Label: "Linux · x64", @@ -2310,7 +2311,6 @@ export function createZhDict(allowSignup: boolean): LandingDict { formatAppImage: ".AppImage", formatDeb: ".deb", formatRpm: ".rpm", - intelNote: "仅支持 Apple Silicon——Intel Mac 目前暂不支持。", unavailable: "暂不可用", }, cli: { diff --git a/apps/web/features/landing/utils/os-detect.ts b/apps/web/features/landing/utils/os-detect.ts index 4d7e35679b..f27c510917 100644 --- a/apps/web/features/landing/utils/os-detect.ts +++ b/apps/web/features/landing/utils/os-detect.ts @@ -7,8 +7,8 @@ * Known limitation: Safari on macOS always reports `Intel Mac OS X` * in the UA string even on Apple Silicon, and Safari does not * implement userAgentData. This function therefore returns `arm64` - * as the best default for any Mac — UI surfaces a small "On Intel - * Mac? Use CLI." hint to cover the Intel minority. + * as the best default for any Mac — UI surfaces a small hint that + * points Intel users to the architecture-specific download below. */ export type OSName = "mac" | "windows" | "linux" | "unknown"; @@ -85,7 +85,7 @@ export async function detectOS(): Promise { let arch: Arch = "unknown"; if (os === "mac") { - // Best default. Real Intel Mac users will see the disclaimer. + // Best default. Real Intel Mac users will see the architecture hint. arch = "arm64"; } else if (/arm|aarch/i.test(ua)) { arch = "arm64"; diff --git a/apps/web/features/landing/utils/parse-release-assets.test.ts b/apps/web/features/landing/utils/parse-release-assets.test.ts new file mode 100644 index 0000000000..ce867354e2 --- /dev/null +++ b/apps/web/features/landing/utils/parse-release-assets.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { parseReleaseAssets } from "./parse-release-assets"; + +function asset(name: string) { + return { + name, + browser_download_url: `https://github.test/releases/${name}`, + }; +} + +describe("parseReleaseAssets", () => { + it("keeps both Apple Silicon and Intel macOS installers", () => { + const assets = parseReleaseAssets([ + asset("multica-desktop-0.4.2-mac-arm64.dmg"), + asset("multica-desktop-0.4.2-mac-arm64.zip"), + asset("multica-desktop-0.4.2-mac-x64.dmg"), + asset("multica-desktop-0.4.2-mac-x64.zip"), + asset("multica-desktop-0.4.2-mac-x64.dmg.blockmap"), + asset("latest-x64-mac.yml"), + ]); + + expect(assets).toEqual({ + macArm64Dmg: + "https://github.test/releases/multica-desktop-0.4.2-mac-arm64.dmg", + macArm64Zip: + "https://github.test/releases/multica-desktop-0.4.2-mac-arm64.zip", + macX64Dmg: + "https://github.test/releases/multica-desktop-0.4.2-mac-x64.dmg", + macX64Zip: + "https://github.test/releases/multica-desktop-0.4.2-mac-x64.zip", + }); + }); +}); diff --git a/apps/web/features/landing/utils/parse-release-assets.ts b/apps/web/features/landing/utils/parse-release-assets.ts index 8579e2f04b..9deb7640da 100644 --- a/apps/web/features/landing/utils/parse-release-assets.ts +++ b/apps/web/features/landing/utils/parse-release-assets.ts @@ -21,6 +21,8 @@ export interface GitHubAsset { export interface DownloadAssets { macArm64Dmg?: string; macArm64Zip?: string; + macX64Dmg?: string; + macX64Zip?: string; winX64Exe?: string; winArm64Exe?: string; linuxAmd64AppImage?: string; @@ -63,9 +65,13 @@ export function parseReleaseAssets(raw: GitHubAsset[]): DownloadAssets { const url = asset.browser_download_url; if (platform === "mac") { - if (archLower !== "arm64") continue; // we only ship arm64 today - if (extLower === "dmg") out.macArm64Dmg = url; - else if (extLower === "zip") out.macArm64Zip = url; + if (archLower === "arm64") { + if (extLower === "dmg") out.macArm64Dmg = url; + else if (extLower === "zip") out.macArm64Zip = url; + } else if (archLower === "x64") { + if (extLower === "dmg") out.macX64Dmg = url; + else if (extLower === "zip") out.macX64Zip = url; + } } else if (platform === "windows") { if (extLower !== "exe") continue; if (archLower === "x64") out.winX64Exe = url;