From b6ec2e344a2a918bae12d7ec3def1fb5237e56a0 Mon Sep 17 00:00:00 2001 From: Nathan_akin <85641756+akintewe@users.noreply.github.com> Date: Fri, 25 Oct 2024 18:28:30 +0100 Subject: [PATCH] Implement QR Code Generation and Scanning for Cashu Wallet (#213) * added QRCODE * removed vision camera lib * reverted tsconfig * added expo camera for qrcode scanning * added qrcode send or recieve functionality * added qrcode send or recieve functionality * added qrcode identifiier * removed unecessary folders * made modifications * made modifications * corrected tsconfig * made changes --- apps/mobile/package.json | 8 +- .../modules/Cashu/GenerateInvoiceCashu.tsx | 9 +- .../mobile/src/modules/Cashu/ReceiveEcash.tsx | 72 ++---- apps/mobile/src/modules/Cashu/index.tsx | 16 +- .../src/modules/Cashu/qr/GenerateQRCode.tsx | 25 ++ apps/mobile/src/modules/Cashu/qr/ScanCode.tsx | 230 +++++++++++++++--- package.json | 2 +- tsconfig.json | 55 ++--- 8 files changed, 283 insertions(+), 134 deletions(-) create mode 100644 apps/mobile/src/modules/Cashu/qr/GenerateQRCode.tsx diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 6dd4f117..dfaabb65 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -60,6 +60,7 @@ "@starknet-react/core": "^2.8.2", "@starknet-wc/core": "0.0.4", "@starknet-wc/react": "0.0.4", + "@stripe/stripe-react-native": "0.38.6", "@tanstack/react-query": "^5.40.0", "@tanstack/react-query-persist-client": "5.40.0", "@uniswap/sdk-core": "^5.3.1", @@ -72,7 +73,7 @@ "common": "workspace:*", "crypto-es": "^2.1.0", "events": "^3.3.0", - "expo": "~51.0.28", + "expo": "~51.0.38", "expo-application": "^5.9.1", "expo-auth-session": "^5.5.2", "expo-av": "~14.0.7", @@ -110,7 +111,7 @@ "react-native-pager-view": "6.3.0", "react-native-passkey": "2.1.1", "react-native-portalize": "^1.0.7", - "react-native-qrcode-svg": "^6.3.1", + "react-native-qrcode-svg": "^6.3.12", "react-native-reanimated": "~3.10.1", "react-native-redash": "^18.1.3", "react-native-safe-area-context": "4.10.1", @@ -126,8 +127,7 @@ "text-encoding": "^0.7.0", "viem": "2.x", "wagmi": "^2.12.8", - "zustand": "^4.5.2", - "@stripe/stripe-react-native":"0.38.6" + "zustand": "^4.5.2" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/apps/mobile/src/modules/Cashu/GenerateInvoiceCashu.tsx b/apps/mobile/src/modules/Cashu/GenerateInvoiceCashu.tsx index b3fb074f..47b7d67d 100644 --- a/apps/mobile/src/modules/Cashu/GenerateInvoiceCashu.tsx +++ b/apps/mobile/src/modules/Cashu/GenerateInvoiceCashu.tsx @@ -13,6 +13,7 @@ import {useStyles, useTheme} from '../../hooks'; import {useDialog, useToast} from '../../hooks/modals'; import {SelectedTab} from '../../types/tab'; import {getInvoices, storeInvoices} from '../../utils/storage_cashu'; +import GenerateQRCode from './qr/GenerateQRCode'; import stylesheet from './styles'; export const GenerateInvoiceCashu = () => { @@ -122,8 +123,6 @@ export const GenerateInvoiceCashu = () => { const cashuInvoice: ICashuInvoice = { bolt11: quote?.request?.request, - // quote: quote?.request?.quote, - // state: quote?.request?.state, date: new Date().getTime(), amount: Number(invoiceAmount), mint: mintUrl, @@ -133,12 +132,8 @@ export const GenerateInvoiceCashu = () => { if (invoicesLocal) { const invoices: ICashuInvoice[] = JSON.parse(invoicesLocal); - - console.log('invoices', invoices); storeInvoices([...invoices, cashuInvoice]); } else { - console.log('no old invoicesLocal', invoicesLocal); - storeInvoices([cashuInvoice]); } } catch (error) { @@ -222,6 +217,8 @@ export const GenerateInvoiceCashu = () => { } /> + {/* Ensure the data prop is correctly passed */} + )} diff --git a/apps/mobile/src/modules/Cashu/ReceiveEcash.tsx b/apps/mobile/src/modules/Cashu/ReceiveEcash.tsx index b3b39b83..3f2ca954 100644 --- a/apps/mobile/src/modules/Cashu/ReceiveEcash.tsx +++ b/apps/mobile/src/modules/Cashu/ReceiveEcash.tsx @@ -13,8 +13,8 @@ import {useStyles, useTheme} from '../../hooks'; import {useDialog, useToast} from '../../hooks/modals'; import {SelectedTab} from '../../types/tab'; import {getInvoices, storeInvoices} from '../../utils/storage_cashu'; +import GenerateQRCode from './qr/GenerateQRCode'; // Import the QR code component import stylesheet from './styles'; -// import QRCode from 'qrcode'; // replace with reactnative qrcode lib export const ReceiveEcash = () => { const tabs = ['lightning', 'ecash']; @@ -37,7 +37,6 @@ export const ReceiveEcash = () => { const {isSeedCashuStorage, setIsSeedCashuStorage} = useCashuStore(); const styles = useStyles(stylesheet); - // const [mintUrl, setMintUrl] = useState("https://mint.minibits.cash/Bitcoin") const [quote, setQuote] = useState(); const [infoMint, setMintInfo] = useState(); @@ -149,35 +148,10 @@ export const ReceiveEcash = () => { } }; - // Encode the Cashu token into a string - - // Generate the QR Code from the Cashu token string - useEffect(() => { - // QRCode.toDataURL(ecash) - // .then((url: React.SetStateAction) => { - // setQRCodeUrl(url); // Set the generated QR code URL - // }) - // .catch((err: any) => console.error(err)); - }, [ecash]); - return ( - - - - {/* - - */} + + + {tabs.map((tab) => ( { {activeTab == 'ecash' && ( <> + /> {ecash && ( - + ecash token { right={ handleCopy('ecash')} - style={{ - marginRight: 10, - }} + style={{marginRight: 10}} > } /> - {/* {qrCodeUrl ? ( - - ) : ( - Generating QR Code... - )} */} + {/* Generate QR code for the eCash token */} + )} @@ -253,11 +212,7 @@ export const ReceiveEcash = () => { {quote?.request && ( - + Invoice address { right={ handleCopy('lnbc')} - style={{ - marginRight: 10, - }} + style={{marginRight: 10}} > } /> + + {/* Display the QR code for the invoice */} + )} diff --git a/apps/mobile/src/modules/Cashu/index.tsx b/apps/mobile/src/modules/Cashu/index.tsx index a74db357..20bf316b 100644 --- a/apps/mobile/src/modules/Cashu/index.tsx +++ b/apps/mobile/src/modules/Cashu/index.tsx @@ -22,6 +22,7 @@ import {InvoicesListCashu} from './InvoicesListCashu'; import {MintListCashu} from './MintListCashu'; import {MnemonicCashu} from './MnemonicCashu'; import {NoMintBanner} from './NoMintBanner'; +import ScanCashuQRCode from './qr/ScanCode'; // Adjust the import path as needed import {ReceiveEcash} from './ReceiveEcash'; import {SendEcash} from './SendEcash'; import stylesheet from './styles'; @@ -109,6 +110,16 @@ export const CashuView = () => { const [selectedTab, setSelectedTab] = useState(SelectedTab.CASHU_WALLET); const [showMore, setShowMore] = useState(false); + const [isScannerVisible, setIsScannerVisible] = useState(false); + + const handleQRCodeClick = () => { + setIsScannerVisible(true); + }; + + const handleCloseScanner = () => { + setIsScannerVisible(false); + }; + const handleTabSelected = (tab: string | SelectedTab, screen?: string) => { setSelectedTab(tab as any); if (screen) { @@ -199,7 +210,7 @@ export const CashuView = () => { or - @@ -297,6 +308,9 @@ export const CashuView = () => { )} + + + ); }; diff --git a/apps/mobile/src/modules/Cashu/qr/GenerateQRCode.tsx b/apps/mobile/src/modules/Cashu/qr/GenerateQRCode.tsx new file mode 100644 index 00000000..56b8d292 --- /dev/null +++ b/apps/mobile/src/modules/Cashu/qr/GenerateQRCode.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import {StyleSheet, View} from 'react-native'; +import QRCode from 'react-native-qrcode-svg'; + +interface GenerateQRCodeProps { + data: string; + size?: number; +} + +const GenerateQRCode: React.FC = ({data, size = 200}) => { + return ( + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + justifyContent: 'center', + }, +}); + +export default GenerateQRCode; diff --git a/apps/mobile/src/modules/Cashu/qr/ScanCode.tsx b/apps/mobile/src/modules/Cashu/qr/ScanCode.tsx index 58a40816..63fd4157 100644 --- a/apps/mobile/src/modules/Cashu/qr/ScanCode.tsx +++ b/apps/mobile/src/modules/Cashu/qr/ScanCode.tsx @@ -1,44 +1,202 @@ -import React, {useEffect} from 'react'; -import {Text} from 'react-native'; -import {Camera, useCameraDevices} from 'react-native-vision-camera'; - -const ScanCashuQRCode = () => { - const devices = useCameraDevices(); - const device = devices[0]; - - useEffect(() => { - // Ask for camera permission - const requestCameraPermission = async () => { - const permission = await Camera.requestCameraPermission(); - if (permission === 'denied') { - console.warn('Camera permission denied'); - } - }; - - requestCameraPermission(); - }, []); - - if (device == null) return Loading Camera...; - - const handleScan = (frame: {text: any}) => { - // Assuming `frame` contains the scanned QR code's content - const qrCodeData = frame?.text; // You need a QR code processor library here - if (qrCodeData) { - try { - } catch (error) { - console.error('Error decoding Cashu token:', error); - } +import {BarcodeScanningResult, CameraView, useCameraPermissions} from 'expo-camera'; +import React, {useState} from 'react'; +import { + Button, + Clipboard, + Dimensions, + Modal, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; + +import {useTheme} from '../../../hooks'; +import {useToast} from '../../../hooks/modals'; +import {usePayment} from '../../../hooks/usePayment'; + +interface ScanCashuQRCodeProps { + onClose: () => void; +} + +const ScanCashuQRCode: React.FC = ({onClose}) => { + const [permission, requestPermission] = useCameraPermissions(); + const [scanned, setScanned] = useState(false); + const [scannedData, setScannedData] = useState(null); + const [modalVisible, setModalVisible] = useState(false); + const {handlePayInvoice, handleGenerateEcash} = usePayment(); + const {showToast} = useToast(); + const {theme} = useTheme(); + + const handleScannedCode = ({data}: BarcodeScanningResult) => { + console.log('Scanned data:', data); + if (!data) { + showToast({title: 'Invalid QR code', type: 'error'}); + return; + } + setScanned(true); + setScannedData(data); + setModalVisible(true); + }; + + const handlePay = async () => { + if (scannedData) { + await handlePayInvoice(scannedData); + showToast({title: 'Invoice paid successfully', type: 'success'}); + setModalVisible(false); + onClose(); + } + }; + + const handleReceive = async () => { + if (scannedData) { + await handleGenerateEcash(Number(scannedData.replace('cashu', ''))); + showToast({title: 'eCash received successfully', type: 'success'}); + setModalVisible(false); + onClose(); + } + }; + + const handleCopyToClipboard = () => { + if (scannedData) { + Clipboard.setString(scannedData); + showToast({title: 'Copied to clipboard', type: 'success'}); } }; + if (!permission) { + return Requesting camera permission; + } + + if (!permission.granted) { + return ( + + We need your permission to use the camera +