fix: address review findings (#6602)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinjing
2026-06-28 01:08:40 -07:00
committed by GitHub
co-authored by Orca
parent 9a7b2e1e2e
commit de1ed81d15
11 changed files with 191 additions and 43 deletions
+36
View File
@@ -680,6 +680,42 @@
border-bottom-color: var(--foreground);
}
/* iOS release-channel switch (Preview vs Stable), shown above the install CTA */
.mobile-page-root .mp-channel-toggle {
display: inline-flex;
align-items: center;
gap: 4px;
margin: -8px 0 18px;
flex-wrap: wrap;
}
.mobile-page-root .mp-channel-toggle button {
border: 1px solid var(--border);
background: transparent;
padding: 4px 12px;
border-radius: 999px;
color: var(--muted-foreground);
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition:
background-color 140ms ease,
color 140ms ease,
border-color 140ms ease;
}
.mobile-page-root .mp-channel-toggle button.is-active {
background: color-mix(in srgb, var(--foreground) 8%, transparent);
border-color: color-mix(in srgb, var(--foreground) 20%, transparent);
color: var(--foreground);
}
.mobile-page-root .mp-channel-tagline {
margin-left: 6px;
color: var(--muted-foreground);
font-size: 12px;
}
.mobile-page-root .mp-inline-actions {
display: flex;
align-items: center;
@@ -3,6 +3,7 @@ import { cn } from '../../lib/utils'
import type { MobileNetworkInterface } from '../settings/mobile-network-interface-selection'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
import { AndroidLogo, IosBrandIcon } from './MobileBrandIcons'
import { getChannelTagline, type InstallCopy, type IosChannel } from './mobile-platform-copy'
export { HeroIntro } from './MobileHeroIntro'
export { HeroPaired, type PairedDevice } from './MobileHeroPairedDevices'
import { translate } from '@/i18n/i18n'
@@ -27,7 +28,9 @@ type HeroFlowProps = {
platform: Platform
onPlatformChange: (next: Platform) => void
installQrUrl: string | null
installCopy: { description: string; ctaLabel: string; url: string }
installCopy: InstallCopy
iosChannel: IosChannel
onIosChannelChange: (next: IosChannel) => void
onOpenInstallUrl: () => void
onCopyInstallUrl: () => void
pairQrDataUrl: string | null
@@ -51,6 +54,8 @@ export function HeroFlow({
onPlatformChange,
installQrUrl,
installCopy,
iosChannel,
onIosChannelChange,
onOpenInstallUrl,
onCopyInstallUrl,
pairQrDataUrl,
@@ -110,6 +115,36 @@ export function HeroFlow({
{translate('auto.components.mobile.MobileHero.ac1eb64952', 'Android')}
</button>
</div>
{platform === 'ios' ? (
<div
className="mp-channel-toggle"
role="radiogroup"
aria-label={translate(
'auto.components.mobile.MobileHero.channel.group',
'Release channel'
)}
>
<button
type="button"
role="radio"
aria-checked={iosChannel === 'preview'}
className={cn(iosChannel === 'preview' && 'is-active')}
onClick={() => onIosChannelChange('preview')}
>
{translate('auto.components.mobile.MobileHero.channel.preview', 'Preview')}
</button>
<button
type="button"
role="radio"
aria-checked={iosChannel === 'stable'}
className={cn(iosChannel === 'stable' && 'is-active')}
onClick={() => onIosChannelChange('stable')}
>
{translate('auto.components.mobile.MobileHero.channel.stable', 'Stable')}
</button>
<span className="mp-channel-tagline">{getChannelTagline(iosChannel)}</span>
</div>
) : null}
<div className="mp-inline-actions">
<button type="button" className="mp-ghost-action" onClick={onOpenInstallUrl}>
{installCopy.ctaLabel}
@@ -3,7 +3,7 @@ import { toast } from 'sonner'
import { useMountedRef } from '@/hooks/useMountedRef'
import { useAppStore } from '@/store'
import { type PairedDevice, type Platform, type StepIndex } from './MobileHero'
import { PLATFORM_COPY } from './mobile-platform-copy'
import { getInstallCopy, type IosChannel } from './mobile-platform-copy'
import {
selectRefreshedNetworkAddress,
type MobileNetworkInterface
@@ -19,12 +19,13 @@ import { MobilePageContent } from './MobilePageContent'
import { useMobileInstallQr } from './use-mobile-install-qr'
export default function MobilePage(): React.JSX.Element {
// Why: stage starts unresolved so we don't flash the intro before we know
// whether any devices are already paired.
const [stage, setStage] = useState<FlowStage | null>(null)
const [stepIdx, setStepIdx] = useState<StepIndex>(0)
const [platform, setPlatform] = useState<Platform>('ios')
// Default iOS users to the preview track — it ships daily, so newcomers land
// on the freshest build unless they deliberately pick the public release.
const [iosChannel, setIosChannel] = useState<IosChannel>('preview')
const [pairQrDataUrl, setPairQrDataUrl] = useState<string | null>(null)
const [pairingUrl, setPairingUrl] = useState<string | null>(null)
@@ -42,7 +43,7 @@ export default function MobilePage(): React.JSX.Element {
const closeMobilePage = useAppStore((s) => s.closeMobilePage)
const showMobileButton = useAppStore((s) => s.settings?.showMobileButton !== false)
const updateSettings = useAppStore((s) => s.updateSettings)
const installQrUrl = useMobileInstallQr(stage, platform)
const installQrUrl = useMobileInstallQr(stage, platform, iosChannel)
const setPairingDeviceBaseline = useCallback(
(count: number | null): void => {
@@ -332,12 +333,12 @@ export default function MobilePage(): React.JSX.Element {
}
const openInstallUrl = (): void => {
void window.api.shell.openUrl(PLATFORM_COPY[platform].url)
void window.api.shell.openUrl(getInstallCopy(platform, iosChannel).url)
}
const copyInstallUrl = async (): Promise<void> => {
try {
await window.api.ui.writeClipboardText(PLATFORM_COPY[platform].url)
await window.api.ui.writeClipboardText(getInstallCopy(platform, iosChannel).url)
if (mountedRef.current) {
toast.success(
translate('auto.components.mobile.MobilePage.fad833de8d', 'Install link copied')
@@ -380,6 +381,8 @@ export default function MobilePage(): React.JSX.Element {
handleBack={handleBack}
handleContinue={handleContinue}
installQrUrl={installQrUrl}
iosChannel={iosChannel}
setIosChannel={setIosChannel}
loadNetworkInterfaces={() => void loadNetworkInterfaces()}
networkInterfaces={networkInterfaces}
openInstallUrl={openInstallUrl}
@@ -2,7 +2,7 @@ import { translate } from '@/i18n/i18n'
import type { MobileNetworkInterface } from '../settings/mobile-network-interface-selection'
import { HeroFlow, HeroIntro, HeroPaired, type PairedDevice, type Platform } from './MobileHero'
import type { StepIndex } from './MobileHero'
import { PLATFORM_COPY } from './mobile-platform-copy'
import { getInstallCopy, type IosChannel } from './mobile-platform-copy'
import type { MobilePageStage } from './mobile-page-stage'
import { MobilePageToolbar } from './MobilePageToolbar'
import { PhoneCarousel } from './PhoneCarousel'
@@ -18,6 +18,8 @@ type MobilePageContentProps = {
handleBack: () => void
handleContinue: () => void
installQrUrl: string | null
iosChannel: IosChannel
setIosChannel: (channel: IosChannel) => void
loadNetworkInterfaces: () => void
networkInterfaces: MobileNetworkInterface[]
openInstallUrl: () => void
@@ -49,6 +51,8 @@ export function MobilePageContent({
handleBack,
handleContinue,
installQrUrl,
iosChannel,
setIosChannel,
loadNetworkInterfaces,
networkInterfaces,
openInstallUrl,
@@ -92,7 +96,9 @@ export function MobilePageContent({
platform={platform}
onPlatformChange={setPlatform}
installQrUrl={installQrUrl}
installCopy={PLATFORM_COPY[platform]}
installCopy={getInstallCopy(platform, iosChannel)}
iosChannel={iosChannel}
onIosChannelChange={setIosChannel}
onOpenInstallUrl={openInstallUrl}
onCopyInstallUrl={copyInstallUrl}
pairQrDataUrl={pairQrDataUrl}
@@ -1,28 +1,40 @@
import type { Platform } from './MobileHero'
import { translate } from '@/i18n/i18n'
export const PLATFORM_COPY: Record<
Platform,
{ description: string; ctaLabel: string; url: string }
> = {
ios: {
get description() {
return translate(
'auto.components.mobile.mobile.platform.copy.432db52b73',
'Scan with your iPhone camera to open the App Store.'
)
},
// iOS ships two App Store tracks: the public App Store build (slower, ~weekly)
// and the TestFlight preview build (daily). Android only ships one APK track.
export type IosChannel = 'stable' | 'preview'
export type InstallCopy = { ctaLabel: string; url: string }
const IOS_CHANNEL_COPY: Record<IosChannel, InstallCopy> = {
stable: {
ctaLabel: 'Open App Store',
url: 'https://apps.apple.com/app/orca-ide/id6766130217'
},
android: {
get description() {
return translate(
'auto.components.mobile.mobile.platform.copy.2a532d6fd7',
'Scan with your Android camera to download the latest APK from GitHub Releases.'
)
},
ctaLabel: 'Download APK',
url: 'https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.16/app-release.apk'
preview: {
ctaLabel: 'Open TestFlight',
url: 'https://testflight.apple.com/join/YjeGMQBA'
}
}
const ANDROID_COPY: InstallCopy = {
ctaLabel: 'Download APK',
url: 'https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.16/app-release.apk'
}
export function getInstallCopy(platform: Platform, iosChannel: IosChannel): InstallCopy {
return platform === 'ios' ? IOS_CHANNEL_COPY[iosChannel] : ANDROID_COPY
}
export function getChannelTagline(iosChannel: IosChannel): string {
return iosChannel === 'preview'
? translate(
'auto.components.mobile.mobile.platform.copy.preview.tagline',
'Newest features, updated daily.'
)
: translate(
'auto.components.mobile.mobile.platform.copy.stable.tagline',
'The public release, updated weekly.'
)
}
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import QRCodeBrowser from 'qrcode/lib/browser'
import type { Platform } from './MobileHero'
import { PLATFORM_COPY } from './mobile-platform-copy'
import { getInstallCopy, type IosChannel } from './mobile-platform-copy'
import type { MobilePageStage } from './mobile-page-stage'
async function renderQrDataUrl(text: string): Promise<string> {
@@ -14,7 +14,8 @@ async function renderQrDataUrl(text: string): Promise<string> {
export function useMobileInstallQr(
stage: MobilePageStage | null,
platform: Platform
platform: Platform,
iosChannel: IosChannel
): string | null {
const [installQrUrl, setInstallQrUrl] = useState<string | null>(null)
@@ -28,7 +29,7 @@ export function useMobileInstallQr(
let cancelled = false
void (async () => {
try {
const dataUrl = await renderQrDataUrl(PLATFORM_COPY[platform].url)
const dataUrl = await renderQrDataUrl(getInstallCopy(platform, iosChannel).url)
if (!cancelled) {
setInstallQrUrl(dataUrl)
}
@@ -41,7 +42,7 @@ export function useMobileInstallQr(
return () => {
cancelled = true
}
}, [platform, stage])
}, [platform, iosChannel, stage])
return installQrUrl
}
+13 -2
View File
@@ -9739,7 +9739,12 @@
"a6cffbbb0b": "Generate code",
"e59a252eca": "Regenerate code",
"d0b52871ce": "Your phones are paired.",
"051978a785": "Your phone is paired."
"051978a785": "Your phone is paired.",
"channel": {
"group": "Release channel",
"preview": "Preview",
"stable": "Stable"
}
},
"MobilePage": {
"e17393c6a3": "Phone preview",
@@ -9773,7 +9778,13 @@
"platform": {
"copy": {
"2a532d6fd7": "Scan with your Android camera to download the latest APK from GitHub Releases.",
"432db52b73": "Scan with your iPhone camera to open the App Store."
"432db52b73": "Scan with your iPhone camera to open the App Store.",
"preview": {
"tagline": "Newest features, updated daily."
},
"stable": {
"tagline": "The public release, updated weekly."
}
}
}
},
+13 -2
View File
@@ -9739,7 +9739,12 @@
"a6cffbbb0b": "Generar código",
"e59a252eca": "regenerar código",
"d0b52871ce": "Tus teléfonos están emparejados.",
"051978a785": "Tu teléfono está emparejado."
"051978a785": "Tu teléfono está emparejado.",
"channel": {
"group": "Canal de versión",
"preview": "Vista previa",
"stable": "Estable"
}
},
"MobilePage": {
"e17393c6a3": "Vista previa del teléfono",
@@ -9773,7 +9778,13 @@
"platform": {
"copy": {
"2a532d6fd7": "Escanee con su cámara Android para descargar el último APK de GitHub Releases.",
"432db52b73": "Escanee con la cámara de su iPhone para abrir la App Store."
"432db52b73": "Escanee con la cámara de su iPhone para abrir la App Store.",
"preview": {
"tagline": "Las funciones más recientes, actualizadas a diario."
},
"stable": {
"tagline": "La versión pública, actualizada semanalmente."
}
}
}
},
+13 -2
View File
@@ -9739,7 +9739,12 @@
"a6cffbbb0b": "コードを生成する",
"e59a252eca": "コードを再生成する",
"d0b52871ce": "スマートフォンはペアリングされています。",
"051978a785": "スマートフォンはペアリングされています。"
"051978a785": "スマートフォンはペアリングされています。",
"channel": {
"group": "リリースチャンネル",
"preview": "プレビュー",
"stable": "安定版"
}
},
"MobilePage": {
"e17393c6a3": "スマートフォンプレビュー",
@@ -9773,7 +9778,13 @@
"platform": {
"copy": {
"2a532d6fd7": "Android カメラでスキャンして、GitHub リリースから最新の APK をダウンロードします。",
"432db52b73": "iPhone のカメラでスキャンして App Store を開きます。"
"432db52b73": "iPhone のカメラでスキャンして App Store を開きます。",
"preview": {
"tagline": "最新機能を毎日更新。"
},
"stable": {
"tagline": "一般公開版、毎週更新。"
}
}
}
},
+13 -2
View File
@@ -9739,7 +9739,12 @@
"a6cffbbb0b": "코드 생성",
"e59a252eca": "코드 재생성",
"d0b52871ce": "휴대전화가 페어링되었습니다.",
"051978a785": "휴대전화가 페어링되었습니다."
"051978a785": "휴대전화가 페어링되었습니다.",
"channel": {
"group": "릴리스 채널",
"preview": "미리 보기",
"stable": "안정 버전"
}
},
"MobilePage": {
"e17393c6a3": "휴대폰 미리보기",
@@ -9773,7 +9778,13 @@
"platform": {
"copy": {
"2a532d6fd7": "Android 카메라로 스캔하여 GitHub 릴리스에서 최신 APK를 다운로드하세요.",
"432db52b73": "iPhone 카메라로 스캔하여 App Store를 엽니다."
"432db52b73": "iPhone 카메라로 스캔하여 App Store를 엽니다.",
"preview": {
"tagline": "최신 기능, 매일 업데이트."
},
"stable": {
"tagline": "정식 출시 버전, 매주 업데이트."
}
}
}
},
+13 -2
View File
@@ -9739,7 +9739,12 @@
"a6cffbbb0b": "生成代码",
"e59a252eca": "重新生成代码",
"d0b52871ce": "您的手机已配对。",
"051978a785": "您的手机已配对。"
"051978a785": "您的手机已配对。",
"channel": {
"group": "发布渠道",
"preview": "预览版",
"stable": "稳定版"
}
},
"MobilePage": {
"e17393c6a3": "手机预览",
@@ -9773,7 +9778,13 @@
"platform": {
"copy": {
"2a532d6fd7": "使用 Android 相机扫描以从 GitHub Releases 下载最新的 APK。",
"432db52b73": "使用 iPhone 相机扫描以打开 App Store。"
"432db52b73": "使用 iPhone 相机扫描以打开 App Store。",
"preview": {
"tagline": "最新功能,每日更新。"
},
"stable": {
"tagline": "公开发布版,每周更新。"
}
}
}
},