diff --git a/dapps/pos-app/app.json b/dapps/pos-app/app.json index 2f615635..c0e8d142 100644 --- a/dapps/pos-app/app.json +++ b/dapps/pos-app/app.json @@ -130,6 +130,12 @@ "iosPermissions": ["Bluetooth"] } ], + [ + "expo-camera", + { + "cameraPermission": "WalletConnect Pay uses the camera to scan a Customer API key QR code." + } + ], [ "expo-secure-store", { diff --git a/dapps/pos-app/app/_layout.tsx b/dapps/pos-app/app/_layout.tsx index 382703fc..0b3627e5 100644 --- a/dapps/pos-app/app/_layout.tsx +++ b/dapps/pos-app/app/_layout.tsx @@ -233,6 +233,13 @@ export default Sentry.wrap(function RootLayout() { name="settings" options={{ headerTitle: SettingsHeaderTitle }} /> + state.setScannedValue, + ); + const [assets] = useAssets([require("@/assets/images/close.png")]); + + // Guards against the scanner firing multiple times before the screen pops. + const handledRef = useRef(false); + + // Ask for camera access as soon as the screen mounts. + useEffect(() => { + if (permission && !permission.granted && permission.canAskAgain) { + requestPermission(); + } + }, [permission, requestPermission]); + + const handleBarcodeScanned = (result: BarcodeScanningResult) => { + if (handledRef.current) return; + const value = result.data?.trim(); + if (!value) return; + handledRef.current = true; + setScannedValue(value); + router.back(); + }; + + const close = () => router.back(); + + const isDenied = permission?.granted === false && !permission.canAskAgain; + + return ( + + {isFocused && permission?.granted ? ( + + ) : null} + + {/* Dimmed mask with a transparent square window (four strips around it). */} + + + + + + + + + + + + {isDenied + ? "Camera access is off. Enable it in your device settings to scan." + : "Point your camera at the API key QR code"} + + + + + {/* Close button, top-left, above the safe-area inset. */} + + + + + {isDenied ? ( + + + + ) : null} + + ); +} + +const MASK_COLOR = "rgba(0, 0, 0, 0.7)"; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "black", + }, + overlay: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + }, + mask: { + flex: 1, + backgroundColor: MASK_COLOR, + }, + middleRow: { + flexDirection: "row", + height: SCAN_AREA_SIZE, + }, + window: { + width: SCAN_AREA_SIZE, + height: SCAN_AREA_SIZE, + }, + bottomMask: { + alignItems: "center", + paddingTop: Spacing["spacing-7"], + paddingHorizontal: Spacing["spacing-8"], + }, + instruction: { + textAlign: "center", + }, + closeButton: { + position: "absolute", + left: Spacing["spacing-5"], + width: 38, + height: 38, + borderRadius: BorderRadius["3"], + borderWidth: 1, + alignItems: "center", + justifyContent: "center", + backgroundColor: "rgba(0, 0, 0, 0.3)", + }, + closeIcon: { + width: 20, + height: 20, + }, + deniedActions: { + position: "absolute", + left: Spacing["spacing-5"], + right: Spacing["spacing-5"], + }, +}); diff --git a/dapps/pos-app/app/settings.tsx b/dapps/pos-app/app/settings.tsx index dd615d32..6ca6cca9 100644 --- a/dapps/pos-app/app/settings.tsx +++ b/dapps/pos-app/app/settings.tsx @@ -1,6 +1,7 @@ import { Badge } from "@/components/badge"; import { Button } from "@/components/button"; import { PinModal } from "@/components/pin-modal"; +import { Pressable } from "@/components/pressable"; import { RadioList, RadioOption } from "@/components/radio-list"; import { SettingsBottomSheet } from "@/components/settings-bottom-sheet"; import { SettingsItem } from "@/components/settings-item"; @@ -10,10 +11,12 @@ import { SetupBanner } from "@/components/setup-banner"; import { ThemedText } from "@/components/themed-text"; import { BorderRadius, Spacing } from "@/constants/spacing"; import { useBiometricAuth } from "@/hooks/use-biometric-auth"; +import { useHasCamera } from "@/hooks/use-has-camera"; import { useMerchantFlow } from "@/hooks/use-merchant-flow"; import { useNfcCapabilities } from "@/hooks/use-nfc-capabilities"; import { useTheme } from "@/hooks/use-theme-color"; import { useLogsStore } from "@/store/useLogsStore"; +import { usePendingApiKeyScanStore } from "@/store/usePendingApiKeyScanStore"; import { useSettingsStore } from "@/store/useSettingsStore"; import { usePosBridgeStore } from "@/store/usePosBridgeStore"; import { isRunningInIframe } from "@/utils/is-running-in-iframe"; @@ -33,7 +36,7 @@ import * as Application from "expo-application"; import Constants from "expo-constants"; import { Image } from "expo-image"; import { router } from "expo-router"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Platform, StyleSheet, TextInput, View } from "react-native"; import { ScrollView } from "react-native-gesture-handler"; @@ -91,6 +94,7 @@ export default function SettingsScreen() { const isIframeBridgeConfigured = isIframeSession && isBridgeConfigured; const [activeSheet, setActiveSheet] = useState(null); + const hasCamera = useHasCamera(); // Custom hooks for biometrics and merchant flow const { @@ -118,6 +122,7 @@ export default function SettingsScreen() { resetCustomerApiKeyInput, handleMerchantIdConfirm, handleCustomerApiKeyConfirm, + handleScannedCustomerApiKey, handlePinVerifyComplete, handleBiometricPress, handlePinSetupComplete, @@ -128,6 +133,19 @@ export default function SettingsScreen() { biometricLabel, }); + // Pick up an API key scanned by the full-screen scan route once it pops back, + // then run the auto-save flow (PIN/biometric) here at the settings root. + const scannedApiKey = usePendingApiKeyScanStore( + (state) => state.scannedValue, + ); + const clearScannedApiKey = usePendingApiKeyScanStore((state) => state.clear); + + useEffect(() => { + if (!scannedApiKey) return; + handleScannedCustomerApiKey(scannedApiKey); + clearScannedApiKey(); + }, [scannedApiKey, handleScannedCustomerApiKey, clearScannedApiKey]); + const currencyOptions: RadioOption[] = useMemo( () => CURRENCIES.map((c) => ({ @@ -174,6 +192,11 @@ export default function SettingsScreen() { handleCustomerApiKeyConfirm(); }; + const handleScanApiKeyPress = () => { + closeSheet(); + router.push("/scan-api-key"); + }; + const handleTestModeChange = (enabled: boolean) => { setTestMode(enabled); if (enabled) { @@ -488,29 +511,53 @@ export default function SettingsScreen() { onClose={closeSheet} > - + + + {hasCamera && ( + + + + )} +