mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-07-20 04:55:41 +00:00
feat(mobile): implement QR code scanner for wallet addresses
- Add expo-camera package for QR scanning - Create QRScannerModal component with camera permission handling - Integrate scanner into WalletScreen scan button - Support substrate: and pezkuwi: URI formats with amount parameter - Add address validation before opening send modal - Configure camera permissions for iOS and Android in app.json
This commit is contained in:
+14
-2
@@ -13,7 +13,10 @@
|
|||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true
|
"supportsTablet": true,
|
||||||
|
"infoPlist": {
|
||||||
|
"NSCameraUsageDescription": "Pezkuwi needs camera access to scan QR codes for wallet addresses and payments."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"package": "io.pezkuwichain.wallet",
|
"package": "io.pezkuwichain.wallet",
|
||||||
@@ -22,8 +25,17 @@
|
|||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
"edgeToEdgeEnabled": true,
|
"edgeToEdgeEnabled": true,
|
||||||
"predictiveBackGestureEnabled": false
|
"predictiveBackGestureEnabled": false,
|
||||||
|
"permissions": ["android.permission.CAMERA"]
|
||||||
},
|
},
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"expo-camera",
|
||||||
|
{
|
||||||
|
"cameraPermission": "Pezkuwi needs camera access to scan QR codes for wallet addresses and payments."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
"web": {
|
"web": {
|
||||||
"favicon": "./assets/favicon.png"
|
"favicon": "./assets/favicon.png"
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -45,6 +45,7 @@
|
|||||||
"@supabase/supabase-js": "^2.90.1",
|
"@supabase/supabase-js": "^2.90.1",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"expo": "~54.0.23",
|
"expo": "~54.0.23",
|
||||||
|
"expo-camera": "~17.0.10",
|
||||||
"expo-image-picker": "~17.0.10",
|
"expo-image-picker": "~17.0.10",
|
||||||
"expo-linear-gradient": "^15.0.7",
|
"expo-linear-gradient": "^15.0.7",
|
||||||
"expo-local-authentication": "^17.0.7",
|
"expo-local-authentication": "^17.0.7",
|
||||||
@@ -113,7 +114,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"expo": {
|
"expo": {
|
||||||
"install": {
|
"install": {
|
||||||
"exclude": ["@types/react"]
|
"exclude": [
|
||||||
|
"@types/react"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,432 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
Modal,
|
||||||
|
TouchableOpacity,
|
||||||
|
Platform,
|
||||||
|
Dimensions,
|
||||||
|
ActivityIndicator,
|
||||||
|
} from 'react-native';
|
||||||
|
import { CameraView, useCameraPermissions, BarcodeScanningResult } from 'expo-camera';
|
||||||
|
import { KurdistanColors } from '../../theme/colors';
|
||||||
|
|
||||||
|
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||||
|
const SCANNER_SIZE = SCREEN_WIDTH * 0.7;
|
||||||
|
|
||||||
|
interface QRScannerModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onScan: (data: string) => void;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QRScannerModal: React.FC<QRScannerModalProps> = ({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
onScan,
|
||||||
|
title = 'Scan QR Code',
|
||||||
|
subtitle = 'Position the QR code within the frame',
|
||||||
|
}) => {
|
||||||
|
const [permission, requestPermission] = useCameraPermissions();
|
||||||
|
const [scanned, setScanned] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
setScanned(false);
|
||||||
|
setIsLoading(true);
|
||||||
|
// Request permission when modal opens
|
||||||
|
if (!permission?.granted) {
|
||||||
|
requestPermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [visible, permission, requestPermission]);
|
||||||
|
|
||||||
|
const handleBarCodeScanned = (result: BarcodeScanningResult) => {
|
||||||
|
if (scanned) return;
|
||||||
|
|
||||||
|
setScanned(true);
|
||||||
|
const { data } = result;
|
||||||
|
|
||||||
|
// Call the onScan callback with the scanned data
|
||||||
|
onScan(data);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCameraReady = () => {
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Web platform fallback
|
||||||
|
if (Platform.OS === 'web') {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
animationType="slide"
|
||||||
|
transparent={true}
|
||||||
|
onRequestClose={onClose}
|
||||||
|
>
|
||||||
|
<View style={styles.centeredView}>
|
||||||
|
<View style={styles.modalView}>
|
||||||
|
<Text style={styles.title}>{title}</Text>
|
||||||
|
<View style={styles.webFallback}>
|
||||||
|
<Text style={styles.webFallbackText}>
|
||||||
|
QR Scanner is not available on web.{'\n'}
|
||||||
|
Please use the mobile app.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
|
||||||
|
<Text style={styles.closeButtonText}>Close</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission not granted yet
|
||||||
|
if (!permission) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
animationType="slide"
|
||||||
|
transparent={true}
|
||||||
|
onRequestClose={onClose}
|
||||||
|
>
|
||||||
|
<View style={styles.centeredView}>
|
||||||
|
<View style={styles.modalView}>
|
||||||
|
<ActivityIndicator size="large" color={KurdistanColors.kesk} />
|
||||||
|
<Text style={styles.permissionText}>Requesting camera permission...</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission denied
|
||||||
|
if (!permission.granted) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
animationType="slide"
|
||||||
|
transparent={true}
|
||||||
|
onRequestClose={onClose}
|
||||||
|
>
|
||||||
|
<View style={styles.centeredView}>
|
||||||
|
<View style={styles.modalView}>
|
||||||
|
<Text style={styles.title}>Camera Permission Required</Text>
|
||||||
|
<Text style={styles.permissionText}>
|
||||||
|
We need your permission to use the camera for scanning QR codes.
|
||||||
|
</Text>
|
||||||
|
<View style={styles.buttonRow}>
|
||||||
|
<TouchableOpacity style={styles.permissionButton} onPress={requestPermission}>
|
||||||
|
<Text style={styles.permissionButtonText}>Grant Permission</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity style={[styles.permissionButton, styles.cancelButton]} onPress={onClose}>
|
||||||
|
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
animationType="slide"
|
||||||
|
transparent={false}
|
||||||
|
onRequestClose={onClose}
|
||||||
|
>
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity style={styles.backButton} onPress={onClose}>
|
||||||
|
<Text style={styles.backButtonText}>✕</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.headerTitle}>{title}</Text>
|
||||||
|
<View style={styles.placeholder} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Camera View */}
|
||||||
|
<View style={styles.cameraContainer}>
|
||||||
|
<CameraView
|
||||||
|
style={StyleSheet.absoluteFillObject}
|
||||||
|
facing="back"
|
||||||
|
barcodeScannerSettings={{
|
||||||
|
barcodeTypes: ['qr'],
|
||||||
|
}}
|
||||||
|
onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
|
||||||
|
onCameraReady={handleCameraReady}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Loading Indicator */}
|
||||||
|
{isLoading && (
|
||||||
|
<View style={styles.loadingOverlay}>
|
||||||
|
<ActivityIndicator size="large" color="#fff" />
|
||||||
|
<Text style={styles.loadingText}>Starting camera...</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Scanner Overlay */}
|
||||||
|
<View style={styles.overlay}>
|
||||||
|
{/* Top overlay */}
|
||||||
|
<View style={styles.overlayTop} />
|
||||||
|
|
||||||
|
{/* Middle row with scanner frame */}
|
||||||
|
<View style={styles.overlayMiddle}>
|
||||||
|
<View style={styles.overlaySide} />
|
||||||
|
<View style={styles.scannerFrame}>
|
||||||
|
{/* Corner indicators */}
|
||||||
|
<View style={[styles.corner, styles.cornerTL]} />
|
||||||
|
<View style={[styles.corner, styles.cornerTR]} />
|
||||||
|
<View style={[styles.corner, styles.cornerBL]} />
|
||||||
|
<View style={[styles.corner, styles.cornerBR]} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.overlaySide} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Bottom overlay */}
|
||||||
|
<View style={styles.overlayBottom}>
|
||||||
|
<Text style={styles.subtitle}>{subtitle}</Text>
|
||||||
|
{scanned && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.rescanButton}
|
||||||
|
onPress={() => setScanned(false)}
|
||||||
|
>
|
||||||
|
<Text style={styles.rescanButtonText}>Tap to Scan Again</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#000',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingTop: Platform.OS === 'ios' ? 50 : 20,
|
||||||
|
paddingBottom: 16,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||||
|
zIndex: 10,
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
backButtonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
width: 40,
|
||||||
|
},
|
||||||
|
cameraContainer: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
loadingOverlay: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
zIndex: 5,
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
color: '#fff',
|
||||||
|
marginTop: 12,
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
overlay: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
overlayTop: {
|
||||||
|
flex: 1,
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||||
|
},
|
||||||
|
overlayMiddle: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
height: SCANNER_SIZE,
|
||||||
|
},
|
||||||
|
overlaySide: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||||
|
},
|
||||||
|
scannerFrame: {
|
||||||
|
width: SCANNER_SIZE,
|
||||||
|
height: SCANNER_SIZE,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
},
|
||||||
|
corner: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
borderColor: KurdistanColors.kesk,
|
||||||
|
},
|
||||||
|
cornerTL: {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
borderTopWidth: 4,
|
||||||
|
borderLeftWidth: 4,
|
||||||
|
borderTopLeftRadius: 8,
|
||||||
|
},
|
||||||
|
cornerTR: {
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
borderTopWidth: 4,
|
||||||
|
borderRightWidth: 4,
|
||||||
|
borderTopRightRadius: 8,
|
||||||
|
},
|
||||||
|
cornerBL: {
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
borderBottomWidth: 4,
|
||||||
|
borderLeftWidth: 4,
|
||||||
|
borderBottomLeftRadius: 8,
|
||||||
|
},
|
||||||
|
cornerBR: {
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
borderBottomWidth: 4,
|
||||||
|
borderRightWidth: 4,
|
||||||
|
borderBottomRightRadius: 8,
|
||||||
|
},
|
||||||
|
overlayBottom: {
|
||||||
|
flex: 1,
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingTop: 30,
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
},
|
||||||
|
rescanButton: {
|
||||||
|
marginTop: 20,
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
paddingVertical: 12,
|
||||||
|
backgroundColor: KurdistanColors.kesk,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
rescanButtonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
// Permission modal styles
|
||||||
|
centeredView: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
|
},
|
||||||
|
modalView: {
|
||||||
|
margin: 20,
|
||||||
|
backgroundColor: '#1a1a2e',
|
||||||
|
borderRadius: 20,
|
||||||
|
padding: 35,
|
||||||
|
alignItems: 'center',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 2,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.25,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 5,
|
||||||
|
width: SCREEN_WIDTH * 0.85,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#fff',
|
||||||
|
marginBottom: 15,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
permissionText: {
|
||||||
|
color: '#aaa',
|
||||||
|
fontSize: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: 20,
|
||||||
|
lineHeight: 24,
|
||||||
|
},
|
||||||
|
buttonRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
permissionButton: {
|
||||||
|
backgroundColor: KurdistanColors.kesk,
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
permissionButtonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
cancelButton: {
|
||||||
|
backgroundColor: '#333',
|
||||||
|
},
|
||||||
|
cancelButtonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
closeButton: {
|
||||||
|
marginTop: 20,
|
||||||
|
paddingHorizontal: 30,
|
||||||
|
paddingVertical: 12,
|
||||||
|
backgroundColor: '#333',
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
closeButtonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
webFallback: {
|
||||||
|
padding: 30,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||||
|
borderRadius: 12,
|
||||||
|
marginVertical: 20,
|
||||||
|
},
|
||||||
|
webFallbackText: {
|
||||||
|
color: '#aaa',
|
||||||
|
fontSize: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: 24,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default QRScannerModal;
|
||||||
@@ -25,6 +25,7 @@ import * as SecureStore from 'expo-secure-store';
|
|||||||
import { KurdistanColors } from '../theme/colors';
|
import { KurdistanColors } from '../theme/colors';
|
||||||
import { usePezkuwi, NetworkType, NETWORKS } from '../contexts/PezkuwiContext';
|
import { usePezkuwi, NetworkType, NETWORKS } from '../contexts/PezkuwiContext';
|
||||||
import { AddTokenModal } from '../components/wallet/AddTokenModal';
|
import { AddTokenModal } from '../components/wallet/AddTokenModal';
|
||||||
|
import { QRScannerModal } from '../components/wallet/QRScannerModal';
|
||||||
import { HezTokenLogo, PezTokenLogo } from '../components/icons';
|
import { HezTokenLogo, PezTokenLogo } from '../components/icons';
|
||||||
import { decodeAddress, checkAddress, encodeAddress } from '@pezkuwi/util-crypto';
|
import { decodeAddress, checkAddress, encodeAddress } from '@pezkuwi/util-crypto';
|
||||||
|
|
||||||
@@ -119,6 +120,7 @@ const WalletScreen: React.FC = () => {
|
|||||||
const [networkSelectorVisible, setNetworkSelectorVisible] = useState(false);
|
const [networkSelectorVisible, setNetworkSelectorVisible] = useState(false);
|
||||||
const [walletSelectorVisible, setWalletSelectorVisible] = useState(false);
|
const [walletSelectorVisible, setWalletSelectorVisible] = useState(false);
|
||||||
const [addTokenModalVisible, setAddTokenModalVisible] = useState(false);
|
const [addTokenModalVisible, setAddTokenModalVisible] = useState(false);
|
||||||
|
const [qrScannerVisible, setQrScannerVisible] = useState(false);
|
||||||
const [tokenSearchVisible, setTokenSearchVisible] = useState(false);
|
const [tokenSearchVisible, setTokenSearchVisible] = useState(false);
|
||||||
const [tokenSearchQuery, setTokenSearchQuery] = useState('');
|
const [tokenSearchQuery, setTokenSearchQuery] = useState('');
|
||||||
const [tokenSettingsVisible, setTokenSettingsVisible] = useState(false);
|
const [tokenSettingsVisible, setTokenSettingsVisible] = useState(false);
|
||||||
@@ -309,6 +311,54 @@ const WalletScreen: React.FC = () => {
|
|||||||
setReceiveModalVisible(true);
|
setReceiveModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle QR code scan result
|
||||||
|
const handleQRScan = (data: string) => {
|
||||||
|
// Try to parse the scanned data
|
||||||
|
let address = data;
|
||||||
|
let amount: string | undefined;
|
||||||
|
|
||||||
|
// Check if it's a Pezkuwi/Substrate URI format (e.g., "substrate:ADDRESS?amount=10")
|
||||||
|
if (data.startsWith('substrate:') || data.startsWith('pezkuwi:')) {
|
||||||
|
const uri = data.replace(/^(substrate:|pezkuwi:)/, '');
|
||||||
|
const [addr, params] = uri.split('?');
|
||||||
|
address = addr;
|
||||||
|
|
||||||
|
if (params) {
|
||||||
|
const urlParams = new URLSearchParams(params);
|
||||||
|
amount = urlParams.get('amount') || undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the address
|
||||||
|
try {
|
||||||
|
const [isValid] = checkAddress(address, 42); // 42 is the SS58 prefix for Pezkuwi
|
||||||
|
if (!isValid) {
|
||||||
|
// Try with generic prefix
|
||||||
|
const [isValidGeneric] = checkAddress(address, -1);
|
||||||
|
if (!isValidGeneric) {
|
||||||
|
showAlert('Invalid QR Code', 'The scanned QR code does not contain a valid Pezkuwi address.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
showAlert('Invalid QR Code', 'The scanned QR code does not contain a valid address.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open send modal with the scanned address
|
||||||
|
setRecipientAddress(address);
|
||||||
|
if (amount) {
|
||||||
|
setSendAmount(amount);
|
||||||
|
}
|
||||||
|
setSelectedToken(tokens[0]); // Default to HEZ
|
||||||
|
setSendModalVisible(true);
|
||||||
|
|
||||||
|
// Show success feedback
|
||||||
|
if (Platform.OS !== 'web') {
|
||||||
|
Alert.alert('Address Scanned', `Address: ${address.slice(0, 8)}...${address.slice(-6)}${amount ? `\nAmount: ${amount}` : ''}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Load saved addresses from storage
|
// Load saved addresses from storage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadAddressBook = async () => {
|
const loadAddressBook = async () => {
|
||||||
@@ -646,7 +696,7 @@ const WalletScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.scanButton}
|
style={styles.scanButton}
|
||||||
onPress={() => showAlert('Scan', 'QR Scanner coming soon')}
|
onPress={() => setQrScannerVisible(true)}
|
||||||
testID="wallet-scan-button"
|
testID="wallet-scan-button"
|
||||||
>
|
>
|
||||||
<Text style={styles.scanIcon}>⊡</Text>
|
<Text style={styles.scanIcon}>⊡</Text>
|
||||||
@@ -1031,6 +1081,15 @@ const WalletScreen: React.FC = () => {
|
|||||||
onTokenAdded={fetchData}
|
onTokenAdded={fetchData}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* QR Scanner Modal */}
|
||||||
|
<QRScannerModal
|
||||||
|
visible={qrScannerVisible}
|
||||||
|
onClose={() => setQrScannerVisible(false)}
|
||||||
|
onScan={handleQRScan}
|
||||||
|
title="Scan Address"
|
||||||
|
subtitle="Scan a wallet address QR code to send funds"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Address Book Modal */}
|
{/* Address Book Modal */}
|
||||||
<Modal visible={addressBookVisible} transparent animationType="slide" onRequestClose={() => setAddressBookVisible(false)}>
|
<Modal visible={addressBookVisible} transparent animationType="slide" onRequestClose={() => setAddressBookVisible(false)}>
|
||||||
<View style={styles.modalOverlay}>
|
<View style={styles.modalOverlay}>
|
||||||
|
|||||||
Reference in New Issue
Block a user