fix native qr code scanner

This commit is contained in:
hzrd149 2025-03-13 22:52:43 +00:00
parent ceddaf1e9d
commit 81b20f9056
5 changed files with 235 additions and 157 deletions

130
pnpm-lock.yaml generated
View File

@ -105,34 +105,34 @@ importers:
version: 0.7.2
applesauce-accounts:
specifier: next
version: 0.0.0-next-20250313155042(typescript@5.8.2)
version: 0.0.0-next-20250313225050(typescript@5.8.2)
applesauce-actions:
specifier: next
version: 0.0.0-next-20250313155042(typescript@5.8.2)
version: 0.0.0-next-20250313225050(typescript@5.8.2)
applesauce-content:
specifier: next
version: 0.0.0-next-20250313155042(typescript@5.8.2)
version: 0.0.0-next-20250313225050(typescript@5.8.2)
applesauce-core:
specifier: next
version: 0.0.0-next-20250313155042(typescript@5.8.2)
version: 0.0.0-next-20250313225050(typescript@5.8.2)
applesauce-factory:
specifier: next
version: 0.0.0-next-20250313155042(typescript@5.8.2)
version: 0.0.0-next-20250313225050(typescript@5.8.2)
applesauce-loaders:
specifier: next
version: 0.0.0-next-20250313155042(typescript@5.8.2)
version: 0.0.0-next-20250313225050(typescript@5.8.2)
applesauce-react:
specifier: next
version: 0.0.0-next-20250313155042(react-dom@19.0.0(react@19.0.0))(typescript@5.8.2)
version: 0.0.0-next-20250313225050(react-dom@19.0.0(react@19.0.0))(typescript@5.8.2)
applesauce-relay:
specifier: next
version: 0.0.0-next-20250313155042(typescript@5.8.2)
version: 0.0.0-next-20250313225050(typescript@5.8.2)
applesauce-signers:
specifier: next
version: 0.0.0-next-20250313155042(typescript@5.8.2)
version: 0.0.0-next-20250313225050(typescript@5.8.2)
applesauce-wallet:
specifier: next
version: 0.0.0-next-20250313155042(typescript@5.8.2)
version: 0.0.0-next-20250313225050(typescript@5.8.2)
bech32:
specifier: ^2.0.0
version: 2.0.0
@ -2205,35 +2205,35 @@ packages:
engines: {node: '>=8.0.0'}
hasBin: true
applesauce-accounts@0.0.0-next-20250313155042:
resolution: {integrity: sha512-2bWwif44iIi/3ZQJwgTmMT9lIi+8m43W18FGc3esgnuXdux0gb36Nyk3xMD+4HIvxa1E/KJnl2MeNnQf5/rBFA==}
applesauce-accounts@0.0.0-next-20250313225050:
resolution: {integrity: sha512-vuMJNPAyQosyHLyyzUIVgn03WqheJFrsrc3twkrUoAp53nIOX43TO2H9i487tOmF7ia5HH1C+/KN1vI9fzKxGw==}
applesauce-actions@0.0.0-next-20250313155042:
resolution: {integrity: sha512-F/yQ1su5njzvmC09SbzyCJpgRC/t7URt3ZogEXFas9r1k3rllodXhC5ZM3b+QUU1XQUQQcX1ZBHBK6oTzF20Qw==}
applesauce-actions@0.0.0-next-20250313225050:
resolution: {integrity: sha512-fCDuhPxeUpjSZgz9v4BqGy3eFT0iNU185jwc1pB50dwDAWrnVJLwlUTWalIhQvWCrzRQqyc7Fu5dcwV0Z1ECZg==}
applesauce-content@0.0.0-next-20250313155042:
resolution: {integrity: sha512-JMvpH9a7s5dTFvk5ey4t/wd1K0YRId3IHGa8hMKEoa7ZRm3PHmCDKhK8KFgjAKF2nd6erIIOkCyfDmEO+7p+vQ==}
applesauce-content@0.0.0-next-20250313225050:
resolution: {integrity: sha512-vf4duzqtRKLLV9bDezCm4RASwLJmhqrzzyfH7T+l3ddih9So6mVMpES8FyG8NqDpFoM/0pHMBAXzwh5cUK+ySw==}
applesauce-core@0.0.0-next-20250313155042:
resolution: {integrity: sha512-HZeDganvR9kdAA7qexnkEXzSG+bdqMGOvUWgljZrvhjoAwazwX7XaOyj2vVmyQdmA8jY0V6pz+5RRTDgqCrx5A==}
applesauce-core@0.0.0-next-20250313225050:
resolution: {integrity: sha512-Z8/ukCDFmYbTxehLB0nlD+ewo06OgD1c0jZyDrtNWt9VpXz6PjRKss3UGlx5/IAttEE+wwc9+KHvPOrtVG3udQ==}
applesauce-factory@0.0.0-next-20250313155042:
resolution: {integrity: sha512-4xLAhram5hxgFkw2ATRIrAAg0r7FAVFLbwRfn6rxasYjrC0NMGGURWULWpHs/o3L2VQGM0iDcdhEYKvn+zIIrQ==}
applesauce-factory@0.0.0-next-20250313225050:
resolution: {integrity: sha512-faTv970d9/sLqOsK/s0EDXXY1Wr3N2I3Lub5XY6N4bqQtkoYr4G0TdZHlxW5MkyiSx7HT5IF6nP1ZU/qpzqCsw==}
applesauce-loaders@0.0.0-next-20250313155042:
resolution: {integrity: sha512-oyLU8fNObK/bfrLS099dfE3HlLj2Btc/MDBhN4FGBCnu8E3ya/r6bFyt0NOcUhxelJle4qi/pWMaRNUhiHdaqQ==}
applesauce-loaders@0.0.0-next-20250313225050:
resolution: {integrity: sha512-ebnUfTVxP1Pwm501iogVGC/Q2Rwe8cUIkRPjxr3hfeOYXVXrp+2rKY61gIYTG4grNeaFXomJe1c6k8tszCe9YA==}
applesauce-react@0.0.0-next-20250313155042:
resolution: {integrity: sha512-ry8xatCIBQm/+1SlG8NMchFCeQMOSPZwUAnO/qw1etW4X5c8PuIkYdQCOQYBoLp37YW0nXGgxImNwxhiz5FoGA==}
applesauce-react@0.0.0-next-20250313225050:
resolution: {integrity: sha512-N2fbW23kndMSYBakA/iTdkHDeougtXQLAEjNcvUG6EOSnSvOTQ3YQuP8qPBUc7xAwclZ5wAmdy7JC/EGVB2L4Q==}
applesauce-relay@0.0.0-next-20250313155042:
resolution: {integrity: sha512-0ZL1iHm0FcsFkO2d7Vjl06mBC2QugtD0+coHy1xP0fs92zGQKnoTXyAS6B3M014vg2x+mH1hwHuOCaQ9rjeEbw==}
applesauce-relay@0.0.0-next-20250313225050:
resolution: {integrity: sha512-dqN6ZMfAUmLjJ5A2zCcmxwIEIHX/rV5WwfnChoflujcTRflfTquUg6Q8vpOTkpfEKh2cm0Tci2GZLwEEiShryQ==}
applesauce-signers@0.0.0-next-20250313155042:
resolution: {integrity: sha512-yvaxUFgmZhbN28B/6skhgoZxdF4/aU9Y2DXoh4ZvmSiJaNJLv1MVE2s7c/vblcsyO7OqQCYKA78jryDLHh3l+w==}
applesauce-signers@0.0.0-next-20250313225050:
resolution: {integrity: sha512-o+kuvGp2eCtpEoVz3uGpDBgzq/vXp028lhRGDI+wL3nwI/ki77i7uADTzd8ylrq7eNvc3rdnebjZM9CpP1Y0vw==}
applesauce-wallet@0.0.0-next-20250313155042:
resolution: {integrity: sha512-a5wk1LgHk4dr0B9+Jc2aIy4SglWv7y95Y/jtArWFLA75K3J0Y00NgJ69xsp5ZOQfwBV/iDfQTfOlIqSD9u+HLA==}
applesauce-wallet@0.0.0-next-20250313225050:
resolution: {integrity: sha512-wpaJOYJTXBuYkDA20m86+rhBaKStUDWxqoMsSX+Cd0oPg1bS7wfpKcn/U353kqjvCU4osInzGwhCupg362khAg==}
arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
@ -3059,8 +3059,8 @@ packages:
engines: {node: '>=0.10.0'}
hasBin: true
electron-to-chromium@1.5.116:
resolution: {integrity: sha512-mufxTCJzLBQVvSdZzX1s5YAuXsN1M4tTyYxOOL1TcSKtIzQ9rjIrm7yFK80rN5dwGTePgdoABDSHpuVtRQh0Zw==}
electron-to-chromium@1.5.117:
resolution: {integrity: sha512-G4+CYIJBiQ72N0gi868tmG4WsD8bwLE9XytBdfgXO5zdlTlvOP2ABzWYILYxCIHmsbm2HjBSgm/E/H/QfcnIyQ==}
elementtree@0.1.7:
resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==}
@ -3310,8 +3310,8 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
force-graph@1.49.3:
resolution: {integrity: sha512-blBqeFq3vdIzqGgvWrML9xA2R0nS5nvjHsEt9lcWVZ29IcdWQ6wa4G0CG/Uv8bP9olwpsJPZSJe3W8vNhiMCnQ==}
force-graph@1.49.4:
resolution: {integrity: sha512-TMbbXg3n0pjI8cmgNlv1IKEGewnd9LdwKVJ4cj4XzZXqP/Q5aSjsyuxzIITtkfDJ+KDsiLql1FHu19Lqrq41uQ==}
engines: {node: '>=12'}
formidable@3.5.2:
@ -8480,10 +8480,10 @@ snapshots:
dependencies:
entities: 2.2.0
applesauce-accounts@0.0.0-next-20250313155042(typescript@5.8.2):
applesauce-accounts@0.0.0-next-20250313225050(typescript@5.8.2):
dependencies:
'@noble/hashes': 1.7.1
applesauce-signers: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-signers: 0.0.0-next-20250313225050(typescript@5.8.2)
nanoid: 5.1.3
nostr-tools: 2.10.4(typescript@5.8.2)
rxjs: 7.8.2
@ -8491,22 +8491,22 @@ snapshots:
- supports-color
- typescript
applesauce-actions@0.0.0-next-20250313155042(typescript@5.8.2):
applesauce-actions@0.0.0-next-20250313225050(typescript@5.8.2):
dependencies:
applesauce-core: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-factory: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313225050(typescript@5.8.2)
applesauce-factory: 0.0.0-next-20250313225050(typescript@5.8.2)
nostr-tools: 2.10.4(typescript@5.8.2)
transitivePeerDependencies:
- supports-color
- typescript
applesauce-content@0.0.0-next-20250313155042(typescript@5.8.2):
applesauce-content@0.0.0-next-20250313225050(typescript@5.8.2):
dependencies:
'@cashu/cashu-ts': 2.0.0-rc1
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
'@types/unist': 3.0.3
applesauce-core: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313225050(typescript@5.8.2)
mdast-util-find-and-replace: 3.0.2
nostr-tools: 2.10.4(typescript@5.8.2)
remark: 15.0.1
@ -8517,7 +8517,7 @@ snapshots:
- supports-color
- typescript
applesauce-core@0.0.0-next-20250313155042(typescript@5.8.2):
applesauce-core@0.0.0-next-20250313225050(typescript@5.8.2):
dependencies:
'@noble/hashes': 1.7.1
'@scure/base': 1.2.4
@ -8532,19 +8532,19 @@ snapshots:
- supports-color
- typescript
applesauce-factory@0.0.0-next-20250313155042(typescript@5.8.2):
applesauce-factory@0.0.0-next-20250313225050(typescript@5.8.2):
dependencies:
applesauce-content: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-content: 0.0.0-next-20250313225050(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313225050(typescript@5.8.2)
nanoid: 5.1.3
nostr-tools: 2.10.4(typescript@5.8.2)
transitivePeerDependencies:
- supports-color
- typescript
applesauce-loaders@0.0.0-next-20250313155042(typescript@5.8.2):
applesauce-loaders@0.0.0-next-20250313225050(typescript@5.8.2):
dependencies:
applesauce-core: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313225050(typescript@5.8.2)
nanoid: 5.1.3
nostr-tools: 2.10.4(typescript@5.8.2)
rx-nostr: 3.5.0
@ -8553,13 +8553,13 @@ snapshots:
- supports-color
- typescript
applesauce-react@0.0.0-next-20250313155042(react-dom@19.0.0(react@19.0.0))(typescript@5.8.2):
applesauce-react@0.0.0-next-20250313225050(react-dom@19.0.0(react@19.0.0))(typescript@5.8.2):
dependencies:
applesauce-accounts: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-actions: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-content: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-factory: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-accounts: 0.0.0-next-20250313225050(typescript@5.8.2)
applesauce-actions: 0.0.0-next-20250313225050(typescript@5.8.2)
applesauce-content: 0.0.0-next-20250313225050(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313225050(typescript@5.8.2)
applesauce-factory: 0.0.0-next-20250313225050(typescript@5.8.2)
nostr-tools: 2.10.4(typescript@5.8.2)
observable-hooks: 4.2.4(react-dom@19.0.0(react@19.0.0))(react@18.3.1)(rxjs@7.8.2)
react: 18.3.1
@ -8569,9 +8569,9 @@ snapshots:
- supports-color
- typescript
applesauce-relay@0.0.0-next-20250313155042(typescript@5.8.2):
applesauce-relay@0.0.0-next-20250313225050(typescript@5.8.2):
dependencies:
applesauce-core: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313225050(typescript@5.8.2)
nanoid: 5.1.3
nostr-tools: 2.10.4(typescript@5.8.2)
rxjs: 7.8.2
@ -8579,12 +8579,12 @@ snapshots:
- supports-color
- typescript
applesauce-signers@0.0.0-next-20250313155042(typescript@5.8.2):
applesauce-signers@0.0.0-next-20250313225050(typescript@5.8.2):
dependencies:
'@noble/hashes': 1.7.1
'@noble/secp256k1': 1.7.1
'@scure/base': 1.2.4
applesauce-core: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313225050(typescript@5.8.2)
debug: 4.4.0
nanoid: 5.1.3
nostr-tools: 2.10.4(typescript@5.8.2)
@ -8592,14 +8592,14 @@ snapshots:
- supports-color
- typescript
applesauce-wallet@0.0.0-next-20250313155042(typescript@5.8.2):
applesauce-wallet@0.0.0-next-20250313225050(typescript@5.8.2):
dependencies:
'@cashu/cashu-ts': 2.0.0-rc1
'@gandlaf21/bc-ur': 1.1.12
'@noble/hashes': 1.7.1
applesauce-actions: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-factory: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-actions: 0.0.0-next-20250313225050(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313225050(typescript@5.8.2)
applesauce-factory: 0.0.0-next-20250313225050(typescript@5.8.2)
nostr-tools: 2.10.4(typescript@5.8.2)
rxjs: 7.8.2
transitivePeerDependencies:
@ -8825,7 +8825,7 @@ snapshots:
browserslist@4.24.4:
dependencies:
caniuse-lite: 1.0.30001704
electron-to-chromium: 1.5.116
electron-to-chromium: 1.5.117
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.24.4)
@ -9526,7 +9526,7 @@ snapshots:
dependencies:
jake: 10.9.2
electron-to-chromium@1.5.116: {}
electron-to-chromium@1.5.117: {}
elementtree@0.1.7:
dependencies:
@ -9844,7 +9844,7 @@ snapshots:
dependencies:
is-callable: 1.2.7
force-graph@1.49.3:
force-graph@1.49.4:
dependencies:
'@tweenjs/tween.js': 25.0.0
accessor-fn: 1.5.1
@ -11615,7 +11615,7 @@ snapshots:
react-force-graph-2d@1.27.0(react@19.0.0):
dependencies:
force-graph: 1.49.3
force-graph: 1.49.4
prop-types: 15.8.1
react: 19.0.0
react-kapsule: 2.5.6(react@19.0.0)

View File

@ -0,0 +1,112 @@
import { Suspense, lazy, useCallback, useEffect, useRef, useState } from "react";
import { filter, merge, Subject } from "rxjs";
import {
Button,
IconButton,
IconButtonProps,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalOverlay,
Progress,
useDisclosure,
useToast,
} from "@chakra-ui/react";
import { receiveAnimated } from "applesauce-wallet/helpers/animated-qr";
import { logger } from "../../helpers/debug";
import { QrCodeIcon } from "../icons";
const BarcodeScannerComponent = lazy(() => import("react-qr-barcode-scanner"));
const log = logger.extend("QRCodeScanner");
export default function AnimatedQRCodeScannerButton({
onResult,
...props
}: { onResult: (data: string) => void } & Omit<IconButtonProps, "icon" | "aria-label">) {
const toast = useToast();
const modal = useDisclosure();
const [progress, setProgress] = useState<number>();
const [subject, setSubject] = useState<Subject<string>>();
const openModal = useCallback(() => {
setSubject(new Subject());
modal.onOpen();
}, [modal.onOpen, setSubject]);
const [stopStream, setStopStream] = useState(false);
const closeModal = useCallback(() => {
// Stop the QR Reader stream (fixes issue where the browser freezes when closing the modal) and then dismiss the modal one tick later
setStopStream(true);
setTimeout(() => modal.onClose(), 0);
}, [setStopStream, modal.onClose]);
const result = useRef(onResult);
result.current = onResult;
// listen to the scanning stream
useEffect(() => {
if (subject) {
setProgress(undefined);
const normal = subject.pipe(filter((part) => !part.startsWith("ur:bytes")));
const animated = subject.pipe(receiveAnimated);
const sub = merge(normal, animated).subscribe({
next: (part) => {
if (typeof part === "number") {
// progress
setProgress(part);
} else if (part) {
// close the javascript scanner
closeModal();
// wait for steam to be stopped before returning data
setTimeout(() => {
result.current(part);
}, 0);
}
},
error: (err) => {
if (err instanceof Error) toast({ status: "error", description: err.message });
closeModal();
},
});
return () => sub.unsubscribe();
}
}, [subject, closeModal, setProgress]);
return (
<>
<IconButton onClick={openModal} icon={<QrCodeIcon boxSize={6} />} aria-label="Qr Scanner" {...props} />
{modal.isOpen && (
<Suspense fallback={null}>
<Modal isOpen={modal.isOpen} onClose={closeModal}>
<ModalOverlay />
<ModalContent>
<ModalBody p="2">
<BarcodeScannerComponent
stopStream={stopStream}
onUpdate={(err, result) => {
if (subject && result && result.getText()) subject.next(result.getText());
}}
onError={(err) => {
if (!subject) return;
if (err instanceof Error) subject.error(err);
else subject.error(new Error(err));
}}
/>
</ModalBody>
<ModalFooter px="2" pb="2" pt="0" alignItems="center" gap="2">
{progress !== undefined && <Progress hasStripe value={progress * 100} w="full" />}
<Button onClick={closeModal}>Cancel</Button>
</ModalFooter>
</ModalContent>
</Modal>
</Suspense>
)}
</>
);
}

View File

@ -1,12 +1,11 @@
import { Barcode, BarcodeScannerPlugin } from "@capacitor-mlkit/barcode-scanning";
import { from, Observable, switchMap } from "rxjs";
import { PluginListenerHandle } from "@capacitor/core";
import { ScanResult, type Barcode } from "@capacitor-mlkit/barcode-scanning";
import { Observable, Subject } from "rxjs";
import { logger } from "../../helpers/debug";
const log = logger.extend("NativeQrCodeScanner");
export async function getNativeScanner(): Promise<BarcodeScannerPlugin> {
export async function installNativeScanner(): Promise<boolean> {
const { BarcodeScanner, GoogleBarcodeScannerModuleInstallState } = await import("@capacitor-mlkit/barcode-scanning");
const { available } = await BarcodeScanner.isGoogleBarcodeScannerModuleAvailable();
@ -52,27 +51,38 @@ export async function getNativeScanner(): Promise<BarcodeScannerPlugin> {
const granted = camera === "granted" || camera === "limited";
if (!granted) throw new Error("Camera access denied");
return BarcodeScanner;
return true;
}
export function getNativeScanStream(scanner: BarcodeScannerPlugin): Observable<Barcode> {
return new Observable<Barcode>((observer) => {
const sub = scanner.addListener("barcodesScanned", (event) => {
for (const barcode of event.barcodes) {
observer.next(barcode);
}
});
export async function getScanningStream(): Promise<Observable<Barcode>> {
const { BarcodeScanner } = await import("@capacitor-mlkit/barcode-scanning");
scanner.startScan();
let handle: PluginListenerHandle | undefined = undefined;
sub.then((e) => (handle = e));
return () => {
if (handle) handle.remove();
else sub.then((handle) => handle.remove);
scanner.stopScan();
};
const subject = new Subject<Barcode>();
await BarcodeScanner.addListener("barcodesScanned", (event) => {
for (const barcode of event.barcodes) {
subject.next(barcode);
}
});
await BarcodeScanner.addListener("scanError", (event) => {
subject.error(new Error(event.message));
});
return subject;
}
export async function startScanning(): Promise<void> {
const { BarcodeScanner } = await import("@capacitor-mlkit/barcode-scanning");
await BarcodeScanner.startScan();
}
export async function stopScanning(): Promise<void> {
const { BarcodeScanner } = await import("@capacitor-mlkit/barcode-scanning");
await BarcodeScanner.removeAllListeners();
await BarcodeScanner.stopScan();
}
export async function scanSingle(): Promise<ScanResult> {
const { BarcodeScanner } = await import("@capacitor-mlkit/barcode-scanning");
return await BarcodeScanner.scan();
}

View File

@ -1,5 +1,4 @@
import { Suspense, lazy, useCallback, useEffect, useState } from "react";
import { filter, map, merge, Observable, Subject } from "rxjs";
import { Suspense, lazy, useState } from "react";
import {
Button,
IconButton,
@ -9,16 +8,14 @@ import {
ModalContent,
ModalFooter,
ModalOverlay,
Progress,
useDisclosure,
useToast,
} from "@chakra-ui/react";
import { receiveAnimated } from "applesauce-wallet/helpers/animated-qr";
import { CAP_IS_NATIVE } from "../../env";
import { logger } from "../../helpers/debug";
import { QrCodeIcon } from "../icons";
import { getNativeScanner, getNativeScanStream } from "./native-scanner";
import { installNativeScanner, scanSingle } from "./native-scanner";
const BarcodeScannerComponent = lazy(() => import("react-qr-barcode-scanner"));
const log = logger.extend("QRCodeScanner");
@ -30,70 +27,35 @@ export default function QRCodeScannerButton({
const toast = useToast();
const modal = useDisclosure();
const [progress, setProgress] = useState<number>();
const [stream, setStream] = useState<Observable<string> | Subject<string>>();
const openModal = useCallback(() => {
setStream(new Subject());
modal.onOpen();
}, [modal.onOpen, setStream]);
const [stopStream, setStopStream] = useState(false);
const closeModal = useCallback(() => {
const closeModal = (result?: string) => {
// Stop the QR Reader stream (fixes issue where the browser freezes when closing the modal) and then dismiss the modal one tick later
setStopStream(true);
setTimeout(() => modal.onClose(), 0);
}, [setStopStream, modal.onClose]);
setTimeout(() => {
modal.onClose();
if (result) onResult(result);
}, 0);
};
const openNative = useCallback(async () => {
const scanner = await getNativeScanner();
const stream = getNativeScanStream(scanner);
setStream(stream.pipe(map((barcode) => barcode.rawValue)));
}, [setStream]);
const handleClick = useCallback(async () => {
const handleClick = async () => {
if (CAP_IS_NATIVE) {
try {
await openNative();
await installNativeScanner();
try {
const result = await scanSingle();
onResult(result.barcodes[0].rawValue);
} catch (error) {
// user cancel
}
} catch (error) {
log(error);
if (import.meta.env.DEV && error instanceof Error) toast({ status: "error", description: error.message });
openModal();
modal.onOpen();
}
} else openModal();
}, [openModal, openNative]);
// listen to the scanning stream
useEffect(() => {
if (stream) {
setProgress(undefined);
const normal = stream.pipe(filter((part) => !part.startsWith("ur:bytes")));
const animated = stream.pipe(receiveAnimated);
const sub = merge(normal, animated).subscribe({
next: (part) => {
if (typeof part === "number") {
// progress
setProgress(part);
} else if (part) {
// close the javascript scanner
closeModal();
// wait for steam to be stopped before returning data
setTimeout(() => {
onResult(part);
}, 0);
}
},
error: (err) => {
if (err instanceof Error) toast({ status: "error", description: err.message });
closeModal();
},
});
return () => sub.unsubscribe();
}
}, [stream, closeModal, onResult, setProgress]);
} else modal.onOpen();
};
return (
<>
@ -106,20 +68,14 @@ export default function QRCodeScannerButton({
<ModalBody p="2">
<BarcodeScannerComponent
stopStream={stopStream}
onUpdate={(err, result) => {
if (stream instanceof Subject && result && result.getText()) stream.next(result.getText());
}}
onError={(err) => {
if (!(stream instanceof Subject)) return;
if (err instanceof Error) stream.error(err);
else stream.error(new Error(err));
onUpdate={(_err, result) => {
if (result && result.getText()) closeModal(result.getText());
}}
/>
</ModalBody>
<ModalFooter px="2" pb="2" pt="0" alignItems="center" gap="2">
{progress !== undefined && <Progress hasStripe value={progress * 100} w="full" />}
<Button onClick={closeModal}>Cancel</Button>
<Button onClick={() => closeModal()}>Cancel</Button>
</ModalFooter>
</ModalContent>
</Modal>

View File

@ -8,8 +8,8 @@ import { WALLET_KIND } from "applesauce-wallet/helpers";
import { ECashIcon } from "../../../components/icons";
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
import useEventUpdate from "../../../hooks/use-event-update";
import QRCodeScannerButton from "../../../components/qr-code/qr-code-scanner-button";
import RouterLink from "../../../components/router-link";
import AnimatedQRCodeScannerButton from "../../../components/qr-code/animated-qr-scanner-button";
export default function WalletBalanceCard({ pubkey, ...props }: { pubkey: string } & Omit<CardProps, "children">) {
const navigate = useNavigate();
@ -39,7 +39,7 @@ export default function WalletBalanceCard({ pubkey, ...props }: { pubkey: string
<Button as={RouterLink} w="full" size="lg" to="/wallet/send">
Send
</Button>
<QRCodeScannerButton onResult={handleScan} size="lg" />
<AnimatedQRCodeScannerButton onResult={handleScan} size="lg" />
<Button as={RouterLink} w="full" size="lg" to="/wallet/receive">
Receive
</Button>