Resolve mobile pair-confirm route state before commit (#3240)
* Resolve pair confirm route state before commit * Preserve pair confirm back handling Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
c082f21d2e
commit
f2fa3ee904
|
|
@ -1,12 +1,12 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { View, Text, StyleSheet, Pressable, ActivityIndicator, BackHandler } from 'react-native'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router'
|
||||
import { ChevronLeft } from 'lucide-react-native'
|
||||
import { parsePairingCode } from '../src/transport/pairing'
|
||||
import { resolvePairConfirmRouteState } from '../src/transport/pair-confirm-state'
|
||||
import { connect } from '../src/transport/rpc-client'
|
||||
import { saveHost, getNextHostName } from '../src/transport/host-store'
|
||||
import type { ConnectionLogEntry, PairingOffer, RpcResponse } from '../src/transport/types'
|
||||
import type { ConnectionLogEntry, RpcResponse } from '../src/transport/types'
|
||||
import { colors, spacing, radii, typography } from '../src/theme/mobile-theme'
|
||||
import { ConnectionLog } from '../src/components/ConnectionLog'
|
||||
|
||||
|
|
@ -23,7 +23,6 @@ export default function PairConfirmScreen() {
|
|||
const router = useRouter()
|
||||
const insets = useSafeAreaInsets()
|
||||
const params = useLocalSearchParams<{ code?: string }>()
|
||||
const [offer, setOffer] = useState<PairingOffer | null>(null)
|
||||
const [status, setStatus] = useState<Status>('awaiting-confirm')
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
const [logs, setLogs] = useState<ConnectionLogEntry[]>([])
|
||||
|
|
@ -32,6 +31,15 @@ export default function PairConfirmScreen() {
|
|||
// batch fewer setState calls when entries arrive in bursts.
|
||||
const logsRef = useRef<ConnectionLogEntry[]>([])
|
||||
|
||||
const routeState = resolvePairConfirmRouteState(params.code)
|
||||
const offer = routeState.offer
|
||||
const resolvedStatus =
|
||||
status === 'awaiting-confirm' && routeState.kind === 'error' ? 'error' : status
|
||||
const resolvedErrorMessage =
|
||||
status === 'awaiting-confirm' && routeState.kind === 'error'
|
||||
? routeState.errorMessage
|
||||
: errorMessage
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
router.replace('/')
|
||||
}, [router])
|
||||
|
|
@ -46,21 +54,6 @@ export default function PairConfirmScreen() {
|
|||
}, [cancel])
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!params.code) {
|
||||
setStatus('error')
|
||||
setErrorMessage('Missing pairing code')
|
||||
return
|
||||
}
|
||||
const parsed = parsePairingCode(params.code)
|
||||
if (!parsed) {
|
||||
setStatus('error')
|
||||
setErrorMessage('Not a valid pairing code')
|
||||
return
|
||||
}
|
||||
setOffer(parsed)
|
||||
}, [params.code])
|
||||
|
||||
async function confirm() {
|
||||
if (!offer) return
|
||||
setStatus('connecting')
|
||||
|
|
@ -141,7 +134,7 @@ export default function PairConfirmScreen() {
|
|||
</Pressable>
|
||||
|
||||
<View style={styles.content}>
|
||||
{offer && status === 'awaiting-confirm' && (
|
||||
{offer && resolvedStatus === 'awaiting-confirm' && (
|
||||
<>
|
||||
<Text style={styles.title}>Pair with this desktop?</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
|
|
@ -156,7 +149,7 @@ export default function PairConfirmScreen() {
|
|||
</>
|
||||
)}
|
||||
|
||||
{status === 'connecting' && (
|
||||
{resolvedStatus === 'connecting' && (
|
||||
<>
|
||||
<ActivityIndicator size="large" color={colors.textSecondary} />
|
||||
<Text style={styles.connectingText}>Connecting…</Text>
|
||||
|
|
@ -166,9 +159,9 @@ export default function PairConfirmScreen() {
|
|||
</>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
{resolvedStatus === 'error' && (
|
||||
<>
|
||||
<Text style={styles.errorText}>{errorMessage}</Text>
|
||||
<Text style={styles.errorText}>{resolvedErrorMessage}</Text>
|
||||
{logs.length > 0 && (
|
||||
<View style={styles.logSlot}>
|
||||
<ConnectionLog entries={logs} title="Pairing log" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
import { Buffer } from 'node:buffer'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { resolvePairConfirmRouteState } from './pair-confirm-state'
|
||||
import type { PairingOffer } from './types'
|
||||
|
||||
function encodeOffer(offer: PairingOffer): string {
|
||||
return Buffer.from(JSON.stringify(offer))
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/g, '')
|
||||
}
|
||||
|
||||
describe('resolvePairConfirmRouteState', () => {
|
||||
const offer: PairingOffer = {
|
||||
v: 2,
|
||||
endpoint: 'ws://192.168.1.10:6768',
|
||||
deviceToken: 'token-abc',
|
||||
publicKeyB64: 'pubkey-xyz'
|
||||
}
|
||||
|
||||
it('accepts a valid pairing code', () => {
|
||||
expect(resolvePairConfirmRouteState(encodeOffer(offer))).toEqual({
|
||||
kind: 'ready',
|
||||
offer,
|
||||
errorMessage: ''
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts a full pairing URL', () => {
|
||||
expect(resolvePairConfirmRouteState(`orca://pair#${encodeOffer(offer)}`)).toEqual({
|
||||
kind: 'ready',
|
||||
offer,
|
||||
errorMessage: ''
|
||||
})
|
||||
})
|
||||
|
||||
it('reports a missing pairing code', () => {
|
||||
expect(resolvePairConfirmRouteState(undefined)).toEqual({
|
||||
kind: 'error',
|
||||
offer: null,
|
||||
errorMessage: 'Missing pairing code'
|
||||
})
|
||||
})
|
||||
|
||||
it('reports an invalid pairing code', () => {
|
||||
expect(resolvePairConfirmRouteState('not a pairing code')).toEqual({
|
||||
kind: 'error',
|
||||
offer: null,
|
||||
errorMessage: 'Not a valid pairing code'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { parsePairingCode } from './pairing'
|
||||
import type { PairingOffer } from './types'
|
||||
|
||||
export type PairConfirmRouteState =
|
||||
| { kind: 'ready'; offer: PairingOffer; errorMessage: '' }
|
||||
| { kind: 'error'; offer: null; errorMessage: string }
|
||||
|
||||
export function resolvePairConfirmRouteState(code: string | undefined): PairConfirmRouteState {
|
||||
if (!code) {
|
||||
return { kind: 'error', offer: null, errorMessage: 'Missing pairing code' }
|
||||
}
|
||||
|
||||
const offer = parsePairingCode(code)
|
||||
if (!offer) {
|
||||
return { kind: 'error', offer: null, errorMessage: 'Not a valid pairing code' }
|
||||
}
|
||||
|
||||
return { kind: 'ready', offer, errorMessage: '' }
|
||||
}
|
||||
Loading…
Reference in New Issue