import { useFocusEffect, useIsFocused, useNavigation, useRoute } from '@react-navigation/native'; import * as bitcoin from 'bitcoinjs-lib'; import createHash from 'create-hash'; import React, { useCallback, useEffect, useState } from 'react'; import { Platform, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native'; import Base43 from '../../blue_modules/base43'; import * as fs from '../../blue_modules/fs'; import { BlueURDecoder, decodeUR, extractSingleWorkload } from '../../blue_modules/ur'; import { BlueLoading, BlueSpacing40, BlueText } from '../../BlueComponents'; import { openPrivacyDesktopSettings } from '../../class/camera'; import Button from '../../components/Button'; import { useTheme } from '../../components/themes'; import { isCameraAuthorizationStatusGranted } from '../../helpers/scan-qr'; import loc from '../../loc'; import { useSettings } from '../../hooks/context/useSettings'; import CameraScreen from '../../components/CameraScreen'; import SafeArea from '../../components/SafeArea'; import presentAlert from '../../components/Alert'; let decoder = false; const styles = StyleSheet.create({ root: { flex: 1, backgroundColor: '#000000', }, openSettingsContainer: { flex: 1, justifyContent: 'center', alignContent: 'center', alignItems: 'center', }, backdoorButton: { width: 60, height: 60, backgroundColor: 'rgba(0,0,0,0.01)', position: 'absolute', top: 10, left: '50%', transform: [{ translateX: -30 }], }, 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 ScanQRCode = () => { const [isLoading, setIsLoading] = useState(false); const { setIsDrawerShouldHide } = useSettings(); const navigation = useNavigation(); const route = useRoute(); const navigationState = navigation.getState(); const previousRoute = navigationState.routes[navigationState.routes.length - 2]; const defaultLaunchedBy = previousRoute ? previousRoute.name : undefined; const { launchedBy = defaultLaunchedBy, onBarScanned, showFileImportButton } = route.params || {}; const scannedCache = {}; const { colors } = useTheme(); const isFocused = useIsFocused(); 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 [cameraStatusGranted, setCameraStatusGranted] = useState(false); 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, }, }); useEffect(() => { isCameraAuthorizationStatusGranted().then(setCameraStatusGranted); }, []); const HashIt = function (s) { return createHash('sha256').update(s).digest().toString('hex'); }; useFocusEffect( useCallback(() => { setIsDrawerShouldHide(true); return () => { setIsDrawerShouldHide(false); }; }, [setIsDrawerShouldHide]), ); 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) { const merge = true; navigation.navigate({ name: launchedBy, params: { onBarScanned: data }, merge }); } else { onBarScanned && onBarScanned({ data }); } } else { setUrTotal(100); setUrHave(Math.floor(decoder.estimatedPercentComplete() * 100)); } } catch (error) { console.warn(error); setIsLoading(true); presentAlert({ title: loc.send.scan_error, message: loc._.invalid_animated_qr_code_fragment, onPress: () => { setIsLoading(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) { const merge = true; navigation.navigate({ name: launchedBy, params: { onBarScanned: data }, merge }); } else { onBarScanned && onBarScanned({ data }); } } else { setAnimatedQRCodeData(animatedQRCodeData); } } catch (error) { console.warn(error); setIsLoading(true); presentAlert({ title: loc.send.scan_error, message: loc._.invalid_animated_qr_code_fragment, onPress: () => { setIsLoading(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:CRYPTO-OUTPUT')) { 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 const data = Buffer.from(hex, 'hex').toString('base64'); if (launchedBy) { const merge = true; navigation.navigate({ name: launchedBy, params: { onBarScanned: data }, merge }); } else { onBarScanned && onBarScanned({ data }); } return; } catch (_) {} if (!isLoading) { setIsLoading(true); try { if (launchedBy) { const merge = true; navigation.navigate({ name: launchedBy, params: { onBarScanned: ret.data }, merge }); } else { onBarScanned && 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 onShowImagePickerButtonPress = () => { if (!isLoading) { setIsLoading(true); fs.showImagePickerAndReadImage() .then(data => { if (data) onBarCodeRead({ data }); }) .finally(() => setIsLoading(false)); } }; const dismiss = () => { navigation.goBack(); }; const handleReadCode = event => { onBarCodeRead({ data: event?.nativeEvent?.codeStringValue }); }; const handleBackdoorOkPress = () => { setBackdoorVisible(false); setBackdoorText(''); if (backdoorText) onBarCodeRead({ data: backdoorText }); }; // this is an invisible backdoor button on bottom left screen corner // tapping it 10 times fires prompt dialog asking for a string thats gona be passed to onBarCodeRead. // this allows to mock and test QR scanning in e2e tests const handleInvisibleBackdoorPress = async () => { setBackdoorPressed(backdoorPressed + 1); if (backdoorPressed < 5) return; setBackdoorPressed(0); setBackdoorVisible(true); }; const render = isLoading ? ( ) : ( {!cameraStatusGranted ? ( {loc.send.permission_camera_message}