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
+
+
+ );
+ }
+
return (
-
+
+
+ Scan QR Code
+
+
+
+
+
+ setModalVisible(false)}
+ >
+
+
+
+ {scannedData?.startsWith('lnbc') ? 'Pay this invoice?' : 'Receive this eCash?'}
+
+
+ setModalVisible(false)} />
+
+
+
+ {scannedData && (
+
+ Scanned Data: {scannedData}
+
+
+ )}
+ {scanned && setScanned(false)} />}
+
+ Close Scanner
+
+
);
};
+const {width} = Dimensions.get('window');
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 20,
+ },
+ permissionContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 20,
+ },
+ header: {
+ position: 'absolute',
+ top: 50,
+ width: '100%',
+ alignItems: 'center',
+ zIndex: 10,
+ },
+ headerText: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ },
+ cameraContainer: {
+ width: width * 0.8,
+ height: width * 0.8,
+ overflow: 'hidden',
+ borderRadius: 20,
+ borderWidth: 2,
+ borderColor: '#fff',
+ marginBottom: 20,
+ },
+ camera: {
+ flex: 1,
+ },
+ overlay: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ borderRadius: 20,
+ },
+ resultContainer: {
+ padding: 10,
+ backgroundColor: '#fff',
+ borderRadius: 10,
+ alignItems: 'center',
+ width: width * 0.8,
+ },
+ resultText: {
+ fontSize: 16,
+ marginBottom: 10,
+ },
+ cancelText: {
+ color: 'red',
+ fontSize: 16,
+ marginTop: 20,
+ },
+ modalContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ },
+ modalContent: {
+ width: '80%',
+ padding: 20,
+ backgroundColor: '#fff',
+ borderRadius: 10,
+ alignItems: 'center',
+ },
+ modalText: {
+ fontSize: 18,
+ marginBottom: 20,
+ },
+});
+
export default ScanCashuQRCode;
diff --git a/package.json b/package.json
index 008f24ba..a722b3f3 100644
--- a/package.json
+++ b/package.json
@@ -56,4 +56,4 @@
"packageManager": "pnpm@8.15.9",
"pnpm": {
}
-}
\ No newline at end of file
+}
diff --git a/tsconfig.json b/tsconfig.json
index d077d69c..bd8b2fdb 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,29 +1,28 @@
{
- "compilerOptions": {
- "rootDir": ".",
- "target": "ESNext",
- "lib": ["dom", "dom.iterable", "esnext"],
- "allowJs": true,
- "skipLibCheck": true,
- "forceConsistentCasingInFileNames": true,
- "noEmit": true,
- "esModuleInterop": true,
- "noUnusedLocals": false,
- "noFallthroughCasesInSwitch": true,
- "useUnknownInCatchVariables": false,
- "moduleResolution": "Bundler",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "downlevelIteration": true,
- "allowSyntheticDefaultImports": true,
- "jsx": "preserve",
- "module": "ES2015",
- "declaration": true,
- "sourceMap": true,
- "baseUrl": "./",
-
- },
- "include": ["./packages/**/*", "./apps/**/*"],
- "exclude": ["node_modules"]
- }
-
\ No newline at end of file
+ "compilerOptions": {
+ "rootDir": ".",
+ "target": "ESNext",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "noUnusedLocals": false,
+ "noFallthroughCasesInSwitch": true,
+ "useUnknownInCatchVariables": false,
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "downlevelIteration": true,
+ "allowSyntheticDefaultImports": true,
+ "jsx": "preserve",
+ "module": "ES2015",
+ "declaration": true,
+ "sourceMap": true,
+ "baseUrl": "./",
+
+ },
+ "include": ["./packages/**/*", "./apps/**/*"],
+ "exclude": ["node_modules"]
+}