import React, { useEffect, useState } from 'react'; import { Image, View, TouchableOpacity, StatusBar, Platform, StyleSheet, TextInput, Alert } from 'react-native'; import { CameraScreen, Camera } from 'react-native-camera-kit'; import { Icon } from 'react-native-elements'; import { launchImageLibrary } from 'react-native-image-picker'; import { decodeUR, extractSingleWorkload, BlueURDecoder } from '../../blue_modules/ur'; import { useNavigation, useRoute, useIsFocused, useTheme } from '@react-navigation/native'; import loc from '../../loc'; import { BlueLoading, BlueText, BlueButton, BlueSpacing40 } from '../../BlueComponents'; import { openPrivacyDesktopSettings } from '../../class/camera'; import alert from '../../components/Alert'; const LocalQRCode = require('@remobile/react-native-qrcode-local-image'); const createHash = require('create-hash'); const fs = require('../../blue_modules/fs'); const Base43 = require('../../blue_modules/base43'); const bitcoin = require('bitcoinjs-lib'); let decoder = false; const styles = StyleSheet.create({ root: { flex: 1, backgroundColor: '#000000', }, closeTouch: { width: 40, height: 40, backgroundColor: 'rgba(0,0,0,0.4)', justifyContent: 'center', borderRadius: 20, position: 'absolute', right: 16, top: 44, }, closeImage: { alignSelf: 'center', }, imagePickerTouch: { width: 40, height: 40, backgroundColor: 'rgba(0,0,0,0.4)', justifyContent: 'center', borderRadius: 20, position: 'absolute', left: 24, bottom: 48, }, filePickerTouch: { width: 40, height: 40, backgroundColor: 'rgba(0,0,0,0.4)', justifyContent: 'center', borderRadius: 20, position: 'absolute', left: 96, bottom: 48, }, openSettingsContainer: { flex: 1, justifyContent: 'center', alignContent: 'center', alignItems: 'center', }, backdoorButton: { width: 40, height: 40, backgroundColor: 'rgba(0,0,0,0.1)', position: 'absolute', }, backdoorInputWrapper: { position: 'absolute', left: '5%', top: '0%', width: '90%', height: '70%', backgroundColor: 'white' }, progressWrapper: { position: 'absolute', alignSelf: 'center', alignItems: 'center', top: '50%', padding: 8, borderRadius: 8 }, backdoorInput: { height: '50%', marginTop: 5, marginHorizontal: 20, borderWidth: 1, borderRadius: 4, textAlignVertical: 'top', }, }); const CameraAuthStatus = Object.freeze({ AUTHORIZED: 'AUTHORIZED', NOT_AUTHORIZED: 'NOT_AUTHORIZED', UNKNOWN: 'UNKNOWN', }); const ScanQRCode = () => { const [isLoading, setIsLoading] = useState(false); const navigation = useNavigation(); const route = useRoute(); const showFileImportButton = route.params.showFileImportButton || false; const { launchedBy, onBarScanned, onDismiss, onBarScannerDismissWithoutData = () => {} } = route.params; const scannedCache = {}; const { colors } = useTheme(); const isFocused = useIsFocused(); const [cameraStatus, setCameraStatus] = useState(CameraAuthStatus.UNKNOWN); const [backdoorPressed, setBackdoorPressed] = useState(0); const [urTotal, setUrTotal] = useState(0); const [urHave, setUrHave] = useState(0); const [backdoorText, setBackdoorText] = useState(''); const [backdoorVisible, setBackdoorVisible] = useState(false); const [animatedQRCodeData, setAnimatedQRCodeData] = useState({}); const stylesHook = StyleSheet.create({ openSettingsContainer: { backgroundColor: colors.brandingColor, }, progressWrapper: { backgroundColor: colors.brandingColor, borderColor: colors.foregroundColor, borderWidth: 4 }, backdoorInput: { borderColor: colors.formBorder, borderBottomColor: colors.formBorder, backgroundColor: colors.inputBackgroundColor, color: colors.foregroundColor, }, }); const HashIt = function (s) { return createHash('sha256').update(s).digest().toString('hex'); }; useEffect(() => { (async () => { if (Platform.OS !== 'ios' && Platform.OS !== 'macos') { return; } let isUserAuthorizedCamera; const isCameraAuthorized = await Camera.checkDeviceCameraAuthorizationStatus(); switch (isCameraAuthorized) { case true: setCameraStatus(CameraAuthStatus.AUTHORIZED); break; case false: setCameraStatus(CameraAuthStatus.NOT_AUTHORIZED); isUserAuthorizedCamera = await Camera.requestDeviceCameraAuthorization(); if (isUserAuthorizedCamera) { setCameraStatus(CameraAuthStatus.AUTHORIZED); } break; case -1: setCameraStatus(CameraAuthStatus.UNKNOWN); isUserAuthorizedCamera = await Camera.requestDeviceCameraAuthorization(); if (isUserAuthorizedCamera) { setCameraStatus(CameraAuthStatus.AUTHORIZED); } break; } })(); }, []); const _onReadUniformResourceV2 = part => { if (!decoder) decoder = new BlueURDecoder(); try { decoder.receivePart(part); if (decoder.isComplete()) { const data = decoder.toString(); decoder = false; // nullify for future use (?) if (launchedBy) { navigation.navigate(launchedBy); } onBarScanned({ data }); } else { setUrTotal(100); setUrHave(Math.floor(decoder.estimatedPercentComplete() * 100)); } } catch (error) { console.warn(error); setIsLoading(true); Alert.alert(loc.send.scan_error, loc._.invalid_animated_qr_code_fragment, [ { text: loc._.ok, onPress: () => { setIsLoading(false); }, style: 'default', }, { cancelabe: false }, ]); } }; /** * * @deprecated remove when we get rid of URv1 support */ const _onReadUniformResource = ur => { try { const [index, total] = extractSingleWorkload(ur); animatedQRCodeData[index + 'of' + total] = ur; setUrTotal(total); setUrHave(Object.values(animatedQRCodeData).length); if (Object.values(animatedQRCodeData).length === total) { const payload = decodeUR(Object.values(animatedQRCodeData)); // lets look inside that data let data = false; if (Buffer.from(payload, 'hex').toString().startsWith('psbt')) { // its a psbt, and whoever requested it expects it encoded in base64 data = Buffer.from(payload, 'hex').toString('base64'); } else { // its something else. probably plain text is expected data = Buffer.from(payload, 'hex').toString(); } if (launchedBy) { navigation.navigate(launchedBy); } onBarScanned({ data }); } else { setAnimatedQRCodeData(animatedQRCodeData); } } catch (error) { console.warn(error); setIsLoading(true); Alert.alert(loc.send.scan_error, loc._.invalid_animated_qr_code_fragment, [ { text: loc._.ok, onPress: () => { setIsLoading(false); }, style: 'default', }, { cancelabe: false }, ]); } }; const onBarCodeRead = ret => { const h = HashIt(ret.data); if (scannedCache[h]) { // this QR was already scanned by this ScanQRCode, lets prevent firing duplicate callbacks return; } scannedCache[h] = +new Date(); if (ret.data.toUpperCase().startsWith('UR:CRYPTO-ACCOUNT')) { return _onReadUniformResourceV2(ret.data); } if (ret.data.toUpperCase().startsWith('UR:CRYPTO-PSBT')) { return _onReadUniformResourceV2(ret.data); } if (ret.data.toUpperCase().startsWith('UR:BYTES')) { const splitted = ret.data.split('/'); if (splitted.length === 3 && splitted[1].includes('-')) { return _onReadUniformResourceV2(ret.data); } } if (ret.data.toUpperCase().startsWith('UR')) { return _onReadUniformResource(ret.data); } // is it base43? stupid electrum desktop try { const hex = Base43.decode(ret.data); bitcoin.Psbt.fromHex(hex); // if it doesnt throw - all good if (launchedBy) { navigation.navigate(launchedBy); } onBarScanned({ data: Buffer.from(hex, 'hex').toString('base64') }); return; } catch (_) {} if (!isLoading) { setIsLoading(true); try { if (launchedBy) { navigation.navigate(launchedBy); } onBarScanned(ret.data); } catch (e) { console.log(e); } } setIsLoading(false); }; const showFilePicker = async () => { setIsLoading(true); const { data } = await fs.showFilePickerAndReadFile(); if (data) onBarCodeRead({ data }); setIsLoading(false); }; const showImagePicker = () => { if (!isLoading) { setIsLoading(true); launchImageLibrary( { title: null, mediaType: 'photo', takePhotoButtonTitle: null, maxHeight: 800, maxWidth: 600, selectionLimit: 1, }, response => { if (response.didCancel) { setIsLoading(false); } else { const asset = response.assets[0]; if (asset.uri) { const uri = asset.uri.toString().replace('file://', ''); LocalQRCode.decode(uri, (error, result) => { if (!error) { onBarCodeRead({ data: result }); } else { alert(loc.send.qr_error_no_qrcode); setIsLoading(false); } }); } else { setIsLoading(false); } } }, ); } }; const dismiss = () => { onBarScannerDismissWithoutData(); if (launchedBy) { navigation.navigate(launchedBy); } else { navigation.goBack(); } if (onDismiss) onDismiss(); }; return isLoading ? ( ) : ( ); }; export default ScanQRCode;