diff --git a/src/components/QrCodeScanner/QrCodeScanner.tsx b/src/components/QrCodeScanner/QrCodeScanner.tsx index b62b61597e8..cc49ed05397 100644 --- a/src/components/QrCodeScanner/QrCodeScanner.tsx +++ b/src/components/QrCodeScanner/QrCodeScanner.tsx @@ -1,4 +1,4 @@ -import { Alert, AlertIcon, Box, Button, Flex } from '@chakra-ui/react' +import { Alert, AlertIcon, Box, Button, Center, Flex, Spinner } from '@chakra-ui/react' import type { Html5QrcodeError, Html5QrcodeResult, @@ -6,7 +6,7 @@ import type { QrcodeSuccessCallback, } from 'html5-qrcode/cjs/core' import { Html5QrcodeErrorTypes } from 'html5-qrcode/cjs/core' -import { useCallback, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { useTranslate } from 'react-polyglot' import { DialogBackButton } from '../Modal/components/DialogBackButton' @@ -17,6 +17,8 @@ import { DialogHeader } from '@/components/Modal/components/DialogHeader' import { DialogTitle } from '@/components/Modal/components/DialogTitle' import { SlideTransition } from '@/components/SlideTransition' import { Text } from '@/components/Text' +import { openNativeQRScanner } from '@/context/WalletProvider/MobileWallet/mobileMessageHandlers' +import { isMobile } from '@/lib/globals' export type DOMExceptionCallback = (errorMessage: string) => void @@ -47,9 +49,35 @@ export const QrCodeScanner = ({ }) => { const translate = useTranslate() const [scanError, setScanError] = useState(null) + const [isNativeScannerLoading, setIsNativeScannerLoading] = useState(isMobile) const error = addressError ?? scanError + // Use native scanner when in mobile app + useEffect(() => { + if (!isMobile) return + + setIsNativeScannerLoading(true) + + openNativeQRScanner() + .then(data => { + // Create a minimal result object for compatibility + const mockResult = { decodedText: data } as Html5QrcodeResult + onSuccess(data, mockResult) + }) + .catch(e => { + setIsNativeScannerLoading(false) + if (e.message === 'QR scan cancelled') { + onBack() + } else { + setScanError(e.message) + if (onError) { + onError(e.message, { type: Html5QrcodeErrorTypes.UNKWOWN_ERROR } as Html5QrcodeError) + } + } + }) + }, [onSuccess, onBack, onError]) + const handleScanSuccess: QrcodeSuccessCallback = useCallback( (decodedText, _result) => { onSuccess(decodedText, _result) @@ -73,6 +101,27 @@ export const QrCodeScanner = ({ const handlePermissionsButtonClick = useCallback(() => setScanError(null), []) + // Show loading state while waiting for native scanner + if (isMobile && isNativeScannerLoading) { + return ( + + + + + + + {translate('modals.send.scanQrCode')} + + + +
+ +
+
+
+ ) + } + return ( diff --git a/src/context/WalletProvider/MobileWallet/mobileMessageHandlers.ts b/src/context/WalletProvider/MobileWallet/mobileMessageHandlers.ts index e9b40e69133..e0ee7313077 100644 --- a/src/context/WalletProvider/MobileWallet/mobileMessageHandlers.ts +++ b/src/context/WalletProvider/MobileWallet/mobileMessageHandlers.ts @@ -258,3 +258,47 @@ export const getAppleAttributionData = (): Promise => { return postMessage({ cmd: 'console', ...params }) } + +/** + * Open the native QR scanner in the mobile app. + * Returns a Promise that resolves with the scanned data, or rejects if cancelled/timeout. + */ +export const openNativeQRScanner = (): Promise => { + return new Promise((resolve, reject) => { + const eventListener = (event: MessageEvent) => { + if (event.data?.type === 'qrScanResult') { + window.removeEventListener('message', eventListener) + resolve(event.data.data) + } else if (event.data?.type === 'qrScanCancelled') { + window.removeEventListener('message', eventListener) + reject(new Error('QR scan cancelled')) + } + } + + // 60 second timeout for scanning + const timeoutId = setTimeout(() => { + window.removeEventListener('message', eventListener) + reject(new Error('QR scan timed out')) + }, 60000) + + window.addEventListener('message', eventListener) + + window.ReactNativeWebView?.postMessage( + JSON.stringify({ + cmd: 'openNativeQRScanner', + id: `${Date.now()}-openNativeQRScanner`, + }), + ) + + // Clean up timeout if scan completes + const originalListener = eventListener + const wrappedListener = (event: MessageEvent) => { + if (event.data?.type === 'qrScanResult' || event.data?.type === 'qrScanCancelled') { + clearTimeout(timeoutId) + } + return originalListener(event) + } + window.removeEventListener('message', eventListener) + window.addEventListener('message', wrappedListener) + }) +}