diff --git a/mobile/app.json b/mobile/app.json index 5e03ae105b2..e4a8ffd0b22 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -16,7 +16,7 @@ "ios": { "supportsTablet": false, "bundleIdentifier": "com.stably.orca.mobile", - "buildNumber": "10", + "buildNumber": "11", "infoPlist": { "NSLocalNetworkUsageDescription": "Orca connects to the desktop app on your local network.", "NSAppTransportSecurity": { diff --git a/mobile/app/index.tsx b/mobile/app/index.tsx index 933d2a5469d..c5c4579c620 100644 --- a/mobile/app/index.tsx +++ b/mobile/app/index.tsx @@ -567,7 +567,7 @@ export default function HomeScreen() { router.push('/pair-scan')}> - Scan Pairing Code + Pair Desktop diff --git a/mobile/app/pair-scan.tsx b/mobile/app/pair-scan.tsx index 246d3b5e3a0..734787400f2 100644 --- a/mobile/app/pair-scan.tsx +++ b/mobile/app/pair-scan.tsx @@ -1,9 +1,9 @@ import { useState, useRef, useCallback } from 'react' -import { View, Text, StyleSheet, Pressable, ActivityIndicator } from 'react-native' +import { View, Text, StyleSheet, Pressable, ActivityIndicator, Linking } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { CameraView, useCameraPermissions } from 'expo-camera' import { useRouter } from 'expo-router' -import { ChevronLeft, Clipboard as ClipboardIcon } from 'lucide-react-native' +import { ChevronLeft, Clipboard as ClipboardIcon, QrCode } from 'lucide-react-native' import { decodePairingUrl, parsePairingCode } from '../src/transport/pairing' import { connect } from '../src/transport/rpc-client' import { saveHost, getNextHostName } from '../src/transport/host-store' @@ -146,20 +146,46 @@ export default function PairScanScreen() { } if (!permission.granted) { + const canAskAgain = permission.canAskAgain !== false return ( router.back()}> - Camera Permission - - Orca needs camera access to scan the pairing QR code from your desktop. + + {canAskAgain ? 'Pair with desktop' : 'Camera Access Disabled'} - - Grant Camera Access + + {canAskAgain + ? 'Scan the QR code from Orca on your desktop, or paste the pairing code instead.' + : 'Enable camera access in Settings, or paste the pairing code instead.'} + + void Linking.openSettings()} + > + {canAskAgain && } + + {canAskAgain ? 'Continue' : 'Open Settings'} + + + [styles.pasteButton, pressed && styles.pasteButtonPressed]} + onPress={() => setPasteVisible(true)} + > + + Paste code instead + setPasteVisible(false)} + /> ) } @@ -360,6 +386,7 @@ const styles = StyleSheet.create({ marginBottom: spacing.sm }, subtitle: { + maxWidth: 310, fontSize: typography.bodySize, color: colors.textSecondary, textAlign: 'center', @@ -379,6 +406,10 @@ const styles = StyleSheet.create({ lineHeight: 20 }, primaryButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: spacing.xs, backgroundColor: colors.textPrimary, paddingHorizontal: spacing.xl, paddingVertical: spacing.sm + 2, diff --git a/src/renderer/src/lib/ensure-hooks-confirmed.test.ts b/src/renderer/src/lib/ensure-hooks-confirmed.test.ts index af7dbea851d..eff7523b571 100644 --- a/src/renderer/src/lib/ensure-hooks-confirmed.test.ts +++ b/src/renderer/src/lib/ensure-hooks-confirmed.test.ts @@ -7,13 +7,15 @@ import { hashOrcaHookScript } from './orca-hook-trust' const hooksCheckMock = vi.fn() const readIssueCommandMock = vi.fn() -;(globalThis as { window: unknown }).window = { - api: { - hooks: { - check: hooksCheckMock, - readIssueCommand: readIssueCommandMock +function installHooksApiMock(): void { + vi.stubGlobal('window', { + api: { + hooks: { + check: hooksCheckMock, + readIssueCommand: readIssueCommandMock + } } - } + }) } type PendingPrompt = { @@ -48,6 +50,7 @@ describe('ensureHooksConfirmed', () => { beforeEach(() => { hooksCheckMock.mockReset() readIssueCommandMock.mockReset() + installHooksApiMock() __resetTrustPromptChainForTests() }) @@ -171,17 +174,13 @@ describe('ensureHooksConfirmed', () => { const first = ensureHooksConfirmed(state, 'repo-1', 'setup') const second = ensureHooksConfirmed(state, 'repo-1', 'archive') - await flush() - - expect(pending).toHaveLength(1) + await vi.waitFor(() => expect(pending).toHaveLength(1)) expect(pending[0].data.scriptKind).toBe('setup') pending[0].resolve('skip') await expect(first).resolves.toBe('skip') - await flush() - - expect(pending).toHaveLength(2) + await vi.waitFor(() => expect(pending).toHaveLength(2)) expect(pending[1].data.scriptKind).toBe('archive') pending[1].resolve('run')