mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 16:02:55 +00:00
Refine mobile relay pairing choice
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user