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')