Refine mobile relay pairing choice

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-07-14 04:43:15 -04:00
co-authored by Orca
parent fe17a4f0b6
commit 950aff8be7
8 changed files with 125 additions and 260 deletions
@@ -65,8 +65,8 @@ describe('MobilePairingConnectionOptions', () => {
const user = userEvent.setup()
render(<MobilePairingConnectionOptions value="local-only" onChange={vi.fn()} />)
expect(screen.getByRole('radio', { name: /connect from anywhere/i })).toBeDisabled()
expect(screen.getByRole('radio', { name: /local network only/i })).toBeChecked()
expect(screen.getByRole('switch', { name: /connect from anywhere/i })).toBeDisabled()
expect(screen.getByRole('switch', { name: /connect from anywhere/i })).not.toBeChecked()
await user.click(screen.getByRole('button', { name: 'Sign in' }))
expect(connect).toHaveBeenCalledOnce()
})
@@ -87,27 +87,26 @@ describe('MobilePairingConnectionOptions', () => {
render(<MobilePairingConnectionOptions value="automatic" onChange={onChange} />)
await waitFor(() => expect(screen.getByText('Ready')).toBeVisible())
await user.click(screen.getByRole('radio', { name: /local network only/i }))
expect(screen.getByRole('switch', { name: /connect from anywhere/i })).toBeChecked()
await user.click(screen.getByRole('switch', { name: /connect from anywhere/i }))
expect(onChange).toHaveBeenCalledWith('local-only')
statusListener?.('standby')
await waitFor(() => expect(screen.getByText('Available')).toBeVisible())
})
it('explains the Relay beta and opens both compatible mobile builds', async () => {
it('shows the Relay beta availability inline and opens both compatible mobile builds', async () => {
const user = userEvent.setup()
render(<MobilePairingConnectionOptions value="local-only" onChange={vi.fn()} />)
await user.hover(screen.getByRole('button', { name: 'About the Orca Relay beta' }))
await waitFor(() =>
expect(screen.getByText(/currently available on the iOS TestFlight preview/i)).toBeVisible()
)
expect(screen.getByText('Beta')).toBeVisible()
expect(screen.getByText('Available on')).toBeVisible()
await user.click(screen.getByRole('button', { name: 'Open TestFlight' }))
await user.click(screen.getByRole('button', { name: 'TestFlight' }))
expect(window.api.shell.openUrl).toHaveBeenCalledWith(
'https://testflight.apple.com/join/YjeGMQBA'
)
await user.click(screen.getByRole('button', { name: 'Download Android APK' }))
await user.click(screen.getByRole('button', { name: 'Android APK' }))
expect(window.api.shell.openUrl).toHaveBeenCalledWith(
'https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.27/app-release.apk'
)
@@ -127,13 +126,12 @@ describe('MobilePairingConnectionOptions', () => {
const props = { compact: true, onChange: vi.fn() }
const { rerender } = render(<MobilePairingConnectionOptions {...props} value="automatic" />)
expect(screen.getByRole('radiogroup').children).toHaveLength(2)
expect(screen.getByText('LAN or Tailscale')).toBeVisible()
expect(screen.getByRole('switch', { name: /connect from anywhere/i })).toBeChecked()
expect(screen.getByText(/direct connection when available/i)).toBeVisible()
await waitFor(() => expect(screen.getByText('Ready')).toBeInTheDocument())
rerender(<MobilePairingConnectionOptions {...props} value="local-only" />)
expect(screen.getByRole('radiogroup').children).toHaveLength(2)
expect(screen.getByRole('switch', { name: /connect from anywhere/i })).not.toBeChecked()
expect(screen.getByText(/without connecting this phone through Orca Relay/i)).toBeVisible()
})
})
@@ -8,6 +8,7 @@ import { useAppStore } from '../../store'
import type { MobileRelayStatus } from '../../../../shared/mobile-relay-status'
import type { MobilePairingConnectionMode } from '../../../../shared/mobile-pairing-connection-mode'
import { MobileRelayBetaAvailability } from './MobileRelayBetaAvailability'
import { SettingsRow, SettingsSwitch } from './SettingsFormControls'
function relayStatusLabel(status: MobileRelayStatus): string {
if (status === 'registered') {
@@ -47,6 +48,33 @@ type CompactConnectionOptionsProps = {
relayStatus: MobileRelayStatus
}
function ConnectionModeSwitch({
value,
onChange,
signedIn
}: Pick<CompactConnectionOptionsProps, 'value' | 'onChange' | 'signedIn'>): React.JSX.Element {
return (
<SettingsRow
label={translate(
'auto.components.settings.MobilePairingConnectionOptions.anywhere',
'Connect from anywhere'
)}
description={<MobileRelayBetaAvailability />}
control={
<SettingsSwitch
checked={value === 'automatic'}
disabled={!signedIn}
ariaLabel={translate(
'auto.components.settings.MobilePairingConnectionOptions.anywhere',
'Connect from anywhere'
)}
onChange={() => onChange(value === 'automatic' ? 'local-only' : 'automatic')}
/>
}
/>
)
}
function CompactConnectionOptions({
value,
onChange,
@@ -58,74 +86,7 @@ function CompactConnectionOptions({
}: CompactConnectionOptionsProps): React.JSX.Element {
return (
<section className="w-full space-y-2">
<h3 className="text-sm font-medium">
{translate(
'auto.components.settings.MobilePairingConnectionOptions.title',
'How should the new phone connect?'
)}
</h3>
<div className="grid grid-cols-2 gap-2" role="radiogroup">
<div
className={cn(
'flex min-h-16 items-center gap-2 rounded-lg border border-border/60 px-3 py-2',
!signedIn && 'opacity-60',
value === 'automatic' && signedIn && 'bg-accent'
)}
>
<input
id="mobile-pairing-connection-mode-automatic-compact"
type="radio"
name="mobile-pairing-connection-mode"
value="automatic"
checked={value === 'automatic'}
disabled={!signedIn}
onChange={() => onChange('automatic')}
className="size-4 shrink-0 accent-primary"
/>
<span className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
<label
htmlFor="mobile-pairing-connection-mode-automatic-compact"
className={cn('text-sm font-medium', signedIn && 'cursor-pointer')}
>
{translate(
'auto.components.settings.MobilePairingConnectionOptions.anywhere',
'Connect from anywhere'
)}
</label>
<MobileRelayBetaAvailability />
</span>
</div>
<label
className={cn(
'flex min-h-16 cursor-pointer items-center gap-2 rounded-lg border border-border/60 px-3 py-2',
value === 'local-only' && 'bg-accent'
)}
>
<input
type="radio"
name="mobile-pairing-connection-mode"
value="local-only"
checked={value === 'local-only'}
onChange={() => onChange('local-only')}
className="size-4 shrink-0 accent-primary"
/>
<span className="min-w-0">
<span className="block text-sm font-medium">
{translate(
'auto.components.settings.MobilePairingConnectionOptions.localOnly',
'Local network only'
)}
</span>
<span className="block text-[11px] text-muted-foreground">
{translate(
'auto.components.settings.MobilePairingConnectionOptions.localShort',
'LAN or Tailscale'
)}
</span>
</span>
</label>
</div>
<ConnectionModeSwitch value={value} onChange={onChange} signedIn={signedIn} />
<div className="flex min-h-9 items-center justify-between gap-3">
<p className="text-xs text-muted-foreground">
@@ -227,115 +188,47 @@ export function MobilePairingConnectionOptions({
return (
<section className="space-y-2">
<h3 className="text-sm font-medium">
{translate(
'auto.components.settings.MobilePairingConnectionOptions.title',
'How should the new phone connect?'
<ConnectionModeSwitch value={value} onChange={onChange} signedIn={signedIn} />
<div className="flex min-h-9 items-center justify-between gap-3">
<p className="text-xs text-muted-foreground">
{!signedIn
? translate(
'auto.components.settings.MobilePairingConnectionOptions.signInDescription',
'Sign in on this desktop to use Orca Relay.'
)
: value === 'automatic'
? translate(
'auto.components.settings.MobilePairingConnectionOptions.automaticDescription',
'Orca uses a direct connection when available and Relay otherwise.'
)
: translate(
'auto.components.settings.MobilePairingConnectionOptions.localDescription',
'Uses LAN or Tailscale without connecting this phone through Orca Relay.'
)}
</p>
{signedIn ? (
<Badge variant="outline" className={cn('shrink-0', value !== 'automatic' && 'invisible')}>
{relayStatusLabel(relayStatus)}
</Badge>
) : configured ? (
<Button
type="button"
size="sm"
className="w-24 shrink-0"
disabled={connecting}
onClick={() => void connect()}
>
{connecting ? <Loader2 className="animate-spin" /> : null}
{translate('auto.components.settings.MobilePairingConnectionOptions.signIn', 'Sign in')}
</Button>
) : (
<Badge variant="outline" className="shrink-0">
{translate(
'auto.components.settings.MobilePairingConnectionOptions.unavailable',
'Unavailable'
)}
</Badge>
)}
</h3>
<div className="space-y-2" role="radiogroup">
<div
className={cn(
'flex items-center gap-3 rounded-lg border border-border/60 px-3',
'py-3',
value === 'automatic' && signedIn && 'bg-accent'
)}
>
<div className="flex min-w-0 flex-1 items-start gap-3">
<input
id="mobile-pairing-connection-mode-automatic"
type="radio"
name="mobile-pairing-connection-mode"
value="automatic"
checked={value === 'automatic'}
disabled={!signedIn}
onChange={() => onChange('automatic')}
className="mt-0.5 size-4 shrink-0 accent-primary"
/>
<span className="min-w-0 space-y-0.5">
<span className="flex flex-wrap items-center gap-2 text-sm font-medium">
<label
htmlFor="mobile-pairing-connection-mode-automatic"
className={cn(signedIn && 'cursor-pointer')}
>
{translate(
'auto.components.settings.MobilePairingConnectionOptions.anywhere',
'Connect from anywhere'
)}
</label>
<MobileRelayBetaAvailability />
</span>
<span className="block text-xs text-muted-foreground">
{signedIn
? translate(
'auto.components.settings.MobilePairingConnectionOptions.automaticDescription',
'Orca uses a direct connection when available and Relay otherwise.'
)
: translate(
'auto.components.settings.MobilePairingConnectionOptions.signInDescription',
'Sign in on this desktop to use Orca Relay.'
)}
</span>
</span>
</div>
{signedIn ? (
<Badge variant="outline" className="shrink-0">
{relayStatusLabel(relayStatus)}
</Badge>
) : configured ? (
<Button
type="button"
size="sm"
className="w-24 shrink-0"
disabled={connecting}
onClick={() => void connect()}
>
{connecting ? <Loader2 className="animate-spin" /> : null}
{translate(
'auto.components.settings.MobilePairingConnectionOptions.signIn',
'Sign in'
)}
</Button>
) : (
<Badge variant="outline" className="shrink-0">
{translate(
'auto.components.settings.MobilePairingConnectionOptions.unavailable',
'Unavailable'
)}
</Badge>
)}
</div>
<label
className={cn(
'flex cursor-pointer items-start gap-3 rounded-lg border border-border/60 px-3',
'py-3',
value === 'local-only' && 'bg-accent'
)}
>
<input
type="radio"
name="mobile-pairing-connection-mode"
value="local-only"
checked={value === 'local-only'}
onChange={() => onChange('local-only')}
className="mt-0.5 size-4 shrink-0 accent-primary"
/>
<span className="min-w-0 space-y-0.5">
<span className="block text-sm font-medium">
{translate(
'auto.components.settings.MobilePairingConnectionOptions.localOnly',
'Local network only'
)}
</span>
<span className="block text-xs text-muted-foreground">
{translate(
'auto.components.settings.MobilePairingConnectionOptions.localDescription',
'Uses LAN or Tailscale without connecting this phone through Orca Relay.'
)}
</span>
</span>
</label>
</div>
</section>
)
@@ -1,8 +1,4 @@
import { Info } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import { badgeVariants } from '../ui/badge'
import { Button } from '../ui/button'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '../ui/hover-card'
const TESTFLIGHT_URL = 'https://testflight.apple.com/join/YjeGMQBA'
const ANDROID_APK_URL =
@@ -10,57 +6,35 @@ const ANDROID_APK_URL =
export function MobileRelayBetaAvailability(): React.JSX.Element {
return (
<HoverCard openDelay={150} closeDelay={100}>
<HoverCardTrigger asChild>
<button
type="button"
aria-label={translate(
'auto.components.settings.MobileRelayBetaAvailability.about',
'About the Orca Relay beta'
)}
className={badgeVariants({
variant: 'outline',
className: 'cursor-pointer hover:bg-accent hover:text-accent-foreground'
})}
>
{translate('auto.components.settings.MobileRelayBetaAvailability.beta', 'Beta')}
<Info />
</button>
</HoverCardTrigger>
<HoverCardContent side="bottom" align="start" className="w-72 space-y-2 p-3">
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.MobileRelayBetaAvailability.availability',
'Orca Relay is currently available on the iOS TestFlight preview and Android APK.'
)}
</p>
<div className="flex items-center gap-3">
<Button
type="button"
variant="link"
size="xs"
className="h-auto p-0"
onClick={() => void window.api.shell.openUrl(TESTFLIGHT_URL)}
>
{translate(
'auto.components.settings.MobileRelayBetaAvailability.testFlight',
'Open TestFlight'
)}
</Button>
<Button
type="button"
variant="link"
size="xs"
className="h-auto p-0"
onClick={() => void window.api.shell.openUrl(ANDROID_APK_URL)}
>
{translate(
'auto.components.settings.MobileRelayBetaAvailability.androidApk',
'Download Android APK'
)}
</Button>
</div>
</HoverCardContent>
</HoverCard>
<span className="inline-flex flex-wrap items-baseline gap-x-1 text-[11px] text-muted-foreground">
<span className="font-medium text-foreground/80">
{translate('auto.components.settings.MobileRelayBetaAvailability.beta', 'Beta')}
</span>
<span aria-hidden="true">—</span>
<span>
{translate(
'auto.components.settings.MobileRelayBetaAvailability.availability',
'Available on'
)}
</span>
<button
type="button"
className="rounded-sm font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
onClick={() => void window.api.shell.openUrl(TESTFLIGHT_URL)}
>
{translate('auto.components.settings.MobileRelayBetaAvailability.testFlight', 'TestFlight')}
</button>
<span aria-hidden="true">/</span>
<button
type="button"
className="rounded-sm font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
onClick={() => void window.api.shell.openUrl(ANDROID_APK_URL)}
>
{translate(
'auto.components.settings.MobileRelayBetaAvailability.androidApk',
'Android APK'
)}
</button>
</span>
)
}
+3 -3
View File
@@ -9079,9 +9079,9 @@
"MobileRelayBetaAvailability": {
"about": "About the Orca Relay beta",
"beta": "Beta",
"availability": "Orca Relay is currently available on the iOS TestFlight preview and Android APK.",
"testFlight": "Open TestFlight",
"androidApk": "Download Android APK"
"availability": "Available on",
"testFlight": "TestFlight",
"androidApk": "Android APK"
}
},
"right": {
+3 -3
View File
@@ -9079,9 +9079,9 @@
"MobileRelayBetaAvailability": {
"about": "Acerca de la beta de Orca Relay",
"beta": "Beta",
"availability": "Orca Relay está disponible actualmente en la versión preliminar de iOS en TestFlight y en el APK de Android.",
"testFlight": "Abrir TestFlight",
"androidApk": "Descargar APK de Android"
"availability": "Disponible en",
"testFlight": "TestFlight",
"androidApk": "APK de Android"
}
},
"right": {
+3 -3
View File
@@ -9079,9 +9079,9 @@
"MobileRelayBetaAvailability": {
"about": "Orca Relay ベータについて",
"beta": "ベータ",
"availability": "Orca Relay は現在、iOS TestFlight プレビュー版と Android APK で利用できます。",
"testFlight": "TestFlight を開く",
"androidApk": "Android APK をダウンロード"
"availability": "利用可能:",
"testFlight": "TestFlight",
"androidApk": "Android APK"
}
},
"right": {
+3 -3
View File
@@ -9079,9 +9079,9 @@
"MobileRelayBetaAvailability": {
"about": "Orca Relay 베타 정보",
"beta": "베타",
"availability": "Orca Relay는 현재 iOS TestFlight 미리 보기 및 Android APK에서 사용할 수 있습니다.",
"testFlight": "TestFlight 열기",
"androidApk": "Android APK 다운로드"
"availability": "이용 가능:",
"testFlight": "TestFlight",
"androidApk": "Android APK"
}
},
"right": {
+3 -3
View File
@@ -9079,9 +9079,9 @@
"MobileRelayBetaAvailability": {
"about": "关于 Orca Relay 测试版",
"beta": "测试版",
"availability": "Orca Relay 目前在 iOS TestFlight 预览版和 Android APK 中提供。",
"testFlight": "打开 TestFlight",
"androidApk": "下载 Android APK"
"availability": "可用平台:",
"testFlight": "TestFlight",
"androidApk": "Android APK"
}
},
"right": {