Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions src/components/QrCodeScanner/QrCodeScanner.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
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,
QrcodeErrorCallback,
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'
Expand All @@ -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

Expand Down Expand Up @@ -47,9 +49,35 @@ export const QrCodeScanner = ({
}) => {
const translate = useTranslate()
const [scanError, setScanError] = useState<DOMException['message'] | null>(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)
Expand All @@ -73,6 +101,27 @@ export const QrCodeScanner = ({

const handlePermissionsButtonClick = useCallback(() => setScanError(null), [])

// Show loading state while waiting for native scanner
if (isMobile && isNativeScannerLoading) {
return (
<SlideTransition>
<DialogHeader>
<DialogHeader.Left>
<DialogBackButton onClick={onBack} />
</DialogHeader.Left>
<DialogHeader.Middle>
<DialogTitle>{translate('modals.send.scanQrCode')}</DialogTitle>
</DialogHeader.Middle>
</DialogHeader>
<DialogBody>
<Center minHeight='298px'>
<Spinner size='xl' color='blue.500' />
</Center>
</DialogBody>
</SlideTransition>
)
}
Comment on lines +105 to +123
Copy link
Collaborator

@NeOMakinG NeOMakinG Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a world where the users didn't update the app, we need to use useMobileFeaturesCompatibility with the next minor version of the mobile app (just take current +1 minor, I'll push a PR for that) and skip this loader and the message logic if the current mobile app isn't compatible


return (
<SlideTransition>
<DialogHeader>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,47 @@ export const getAppleAttributionData = (): Promise<AppleSearchAdsAttributionData
export const sendMobileConsole = (params: MobileConsoleParams): Promise<void> => {
return postMessage<void>({ 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<string> => {
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)
})
}