BlueWallet/screen/send/ScanQRCode.js

412 lines
13 KiB
JavaScript
Raw Normal View History

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';
2019-09-21 19:37:37 +02:00
import { Icon } from 'react-native-elements';
2020-12-10 18:40:14 +01:00
import { launchImageLibrary } from 'react-native-image-picker';
2021-07-09 12:52:09 +02:00
import { decodeUR, extractSingleWorkload, BlueURDecoder } from '../../blue_modules/ur';
import { useNavigation, useRoute, useIsFocused, useTheme } from '@react-navigation/native';
2020-07-20 15:38:46 +02:00
import loc from '../../loc';
2020-11-30 05:18:54 +01:00
import { BlueLoading, BlueText, BlueButton, BlueSpacing40 } from '../../BlueComponents';
2020-10-13 07:49:30 +02:00
import { openPrivacyDesktopSettings } from '../../class/camera';
import alert from '../../components/Alert';
2019-09-21 19:37:37 +02:00
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');
2021-07-09 12:52:09 +02:00
let decoder = false;
2018-01-30 23:42:38 +01:00
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,
2020-09-23 18:58:06 +02:00
backgroundColor: 'rgba(0,0,0,0.1)',
position: 'absolute',
},
2020-09-28 15:37:41 +02:00
backdoorInputWrapper: { position: 'absolute', left: '5%', top: '0%', width: '90%', height: '70%', backgroundColor: 'white' },
2020-12-14 22:28:10 +01:00
progressWrapper: { position: 'absolute', alignSelf: 'center', alignItems: 'center', top: '50%', padding: 8, borderRadius: 8 },
2020-09-28 15:37:41 +02:00
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 = () => {
2019-12-28 01:53:34 +01:00
const [isLoading, setIsLoading] = useState(false);
2020-05-31 15:30:01 +02:00
const navigation = useNavigation();
2020-05-27 13:12:17 +02:00
const route = useRoute();
const showFileImportButton = route.params.showFileImportButton || false;
2021-09-09 13:00:11 +02:00
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);
2020-09-28 15:37:41 +02:00
const [backdoorText, setBackdoorText] = useState('');
const [backdoorVisible, setBackdoorVisible] = useState(false);
const [animatedQRCodeData, setAnimatedQRCodeData] = useState({});
const stylesHook = StyleSheet.create({
openSettingsContainer: {
backgroundColor: colors.brandingColor,
},
2020-12-14 22:28:10 +01:00
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;
}
})();
}, []);
2021-07-09 12:52:09 +02:00
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 },
]);
}
};
2019-12-28 01:53:34 +01:00
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);
}
2021-07-09 12:52:09 +02:00
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 (_) {}
2020-05-21 17:36:03 +02:00
if (!isLoading) {
2019-12-28 01:53:34 +01:00
setIsLoading(true);
try {
if (launchedBy) {
2020-05-31 15:30:01 +02:00
navigation.navigate(launchedBy);
2019-12-28 01:53:34 +01:00
}
onBarScanned(ret.data);
2019-12-28 01:53:34 +01:00
} catch (e) {
console.log(e);
}
}
setIsLoading(false);
};
2018-01-30 23:42:38 +01:00
2020-01-01 04:31:04 +01:00
const showFilePicker = async () => {
setIsLoading(true);
const { data } = await fs.showFilePickerAndReadFile();
if (data) onBarCodeRead({ data });
2020-10-05 23:25:14 +02:00
setIsLoading(false);
2020-01-01 04:31:04 +01:00
};
2018-01-30 23:42:38 +01:00
const showImagePicker = () => {
if (!isLoading) {
setIsLoading(true);
2020-12-10 18:40:14 +01:00
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 = () => {
2021-09-09 13:00:11 +02:00
onBarScannerDismissWithoutData();
2020-08-16 05:35:28 +02:00
if (launchedBy) {
navigation.navigate(launchedBy);
} else {
navigation.goBack();
}
2021-02-18 14:37:43 +01:00
if (onDismiss) onDismiss();
};
return isLoading ? (
<View style={styles.root}>
2020-11-30 05:18:54 +01:00
<BlueLoading />
</View>
) : (
<View style={styles.root}>
2020-06-03 04:00:45 +02:00
<StatusBar hidden />
{isFocused && cameraStatus !== CameraAuthStatus.NOT_AUTHORIZED && (
2022-10-31 13:25:26 +01:00
<CameraScreen scanBarcode onReadCode={event => onBarCodeRead({ data: event?.nativeEvent?.codeStringValue })} showFrame={false} />
2019-12-28 01:53:34 +01:00
)}
{cameraStatus === CameraAuthStatus.NOT_AUTHORIZED && (
<View style={[styles.openSettingsContainer, stylesHook.openSettingsContainer]}>
2020-11-22 09:04:04 +01:00
<BlueText>{loc.send.permission_camera_message}</BlueText>
<BlueSpacing40 />
2020-11-22 09:48:16 +01:00
<BlueButton title={loc.send.open_settings} onPress={openPrivacyDesktopSettings} />
</View>
)}
2020-05-30 09:47:32 +02:00
<TouchableOpacity style={styles.closeTouch} onPress={dismiss}>
<Image style={styles.closeImage} source={require('../../img/close-white.png')} />
</TouchableOpacity>
2020-05-30 09:47:32 +02:00
<TouchableOpacity style={styles.imagePickerTouch} onPress={showImagePicker}>
2020-05-21 15:55:03 +02:00
<Icon name="image" type="font-awesome" color="#ffffff" />
2019-12-28 01:53:34 +01:00
</TouchableOpacity>
2020-09-23 18:58:06 +02:00
{showFileImportButton && (
<TouchableOpacity style={styles.filePickerTouch} onPress={showFilePicker}>
2021-05-19 20:47:18 +02:00
<Icon name="file-import" type="font-awesome-5" color="#ffffff" />
2020-09-23 18:58:06 +02:00
</TouchableOpacity>
)}
{urTotal > 0 && (
2020-12-14 22:28:10 +01:00
<View style={[styles.progressWrapper, stylesHook.progressWrapper]} testID="UrProgressBar">
2020-12-15 00:00:35 +01:00
<BlueText>{loc.wallets.please_continue_scanning}</BlueText>
2020-11-22 09:04:04 +01:00
<BlueText>
{urHave} / {urTotal}
2020-11-22 09:04:04 +01:00
</BlueText>
</View>
)}
2020-09-28 15:37:41 +02:00
{backdoorVisible && (
<View style={styles.backdoorInputWrapper}>
2020-11-22 09:04:04 +01:00
<BlueText>Provide QR code contents manually:</BlueText>
2020-09-28 15:37:41 +02:00
<TextInput
testID="scanQrBackdoorInput"
multiline
underlineColorAndroid="transparent"
style={[styles.backdoorInput, stylesHook.backdoorInput]}
2020-09-28 15:37:41 +02:00
autoCorrect={false}
autoCapitalize="none"
spellCheck={false}
selectTextOnFocus={false}
keyboardType={Platform.OS === 'android' ? 'visible-password' : 'default'}
value={backdoorText}
onChangeText={setBackdoorText}
/>
2020-11-22 09:48:16 +01:00
<BlueButton
2020-09-28 15:37:41 +02:00
title="OK"
testID="scanQrBackdoorOkButton"
onPress={() => {
setBackdoorVisible(false);
setBackdoorText('');
2020-09-28 15:37:41 +02:00
if (backdoorText) onBarCodeRead({ data: backdoorText });
2020-09-28 15:37:41 +02:00
}}
/>
</View>
)}
<TouchableOpacity
testID="ScanQrBackdoorButton"
style={styles.backdoorButton}
onPress={async () => {
// 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
setBackdoorPressed(backdoorPressed + 1);
if (backdoorPressed < 5) return;
2020-09-23 18:58:06 +02:00
setBackdoorPressed(0);
2020-09-28 15:37:41 +02:00
setBackdoorVisible(true);
}}
/>
2019-12-28 01:53:34 +01:00
</View>
);
};
2018-03-18 03:48:23 +01:00
2019-12-28 01:53:34 +01:00
export default ScanQRCode;