BlueWallet/screen/send/ScanQRCode.js

358 lines
11 KiB
JavaScript
Raw Normal View History

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';
2025-01-06 20:05:55 -04:00
import { Alert, Platform, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
2024-05-20 10:54:13 +01:00
import Base43 from '../../blue_modules/base43';
import * as fs from '../../blue_modules/fs';
import { BlueURDecoder, decodeUR, extractSingleWorkload } from '../../blue_modules/ur';
2024-05-20 10:54:13 +01:00
import { BlueLoading, BlueSpacing40, BlueText } from '../../BlueComponents';
2023-10-17 09:35:10 -04:00
import { openPrivacyDesktopSettings } from '../../class/camera';
2023-11-15 04:40:22 -04:00
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';
2025-01-06 20:05:55 -04:00
import CameraScreen from '../../components/CameraScreen';
2021-07-09 11:52:09 +01:00
let decoder = false;
2018-01-30 22:42:38 +00:00
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',
2025-01-06 20:05:55 -04:00
top: 10,
left: '50%',
transform: [{ translateX: -30 }],
},
2020-09-28 14:37:41 +01:00
backdoorInputWrapper: { position: 'absolute', left: '5%', top: '0%', width: '90%', height: '70%', backgroundColor: 'white' },
2020-12-14 16:28:10 -05:00
progressWrapper: { position: 'absolute', alignSelf: 'center', alignItems: 'center', top: '50%', padding: 8, borderRadius: 8 },
2020-09-28 14:37:41 +01:00
backdoorInput: {
height: '50%',
marginTop: 5,
marginHorizontal: 20,
borderWidth: 1,
borderRadius: 4,
textAlignVertical: 'top',
},
});
const ScanQRCode = () => {
2019-12-27 18:53:34 -06:00
const [isLoading, setIsLoading] = useState(false);
const { setIsDrawerShouldHide } = useSettings();
2020-05-31 16:30:01 +03:00
const navigation = useNavigation();
2020-05-27 14:12:17 +03:00
const route = useRoute();
2025-01-03 06:14:09 -04:00
const navigationState = navigation.getState();
const previousRoute = navigationState.routes[navigationState.routes.length - 2];
const defaultLaunchedBy = previousRoute ? previousRoute.name : undefined;
2025-01-06 20:31:36 -04:00
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);
2020-09-28 14:37:41 +01:00
const [backdoorText, setBackdoorText] = useState('');
const [backdoorVisible, setBackdoorVisible] = useState(false);
const [animatedQRCodeData, setAnimatedQRCodeData] = useState({});
2023-10-17 09:35:10 -04:00
const [cameraStatusGranted, setCameraStatusGranted] = useState(false);
const stylesHook = StyleSheet.create({
openSettingsContainer: {
backgroundColor: colors.brandingColor,
},
2020-12-14 16:28:10 -05:00
progressWrapper: { backgroundColor: colors.brandingColor, borderColor: colors.foregroundColor, borderWidth: 4 },
backdoorInput: {
borderColor: colors.formBorder,
borderBottomColor: colors.formBorder,
backgroundColor: colors.inputBackgroundColor,
color: colors.foregroundColor,
},
});
2023-04-30 20:31:31 +01:00
useEffect(() => {
2023-10-17 09:35:10 -04:00
isCameraAuthorizationStatusGranted().then(setCameraStatusGranted);
2023-04-30 20:31:31 +01:00
}, []);
const HashIt = function (s) {
return createHash('sha256').update(s).digest().toString('hex');
};
useFocusEffect(
useCallback(() => {
setIsDrawerShouldHide(true);
return () => {
setIsDrawerShouldHide(false);
};
}, [setIsDrawerShouldHide]),
);
2021-07-09 11:52:09 +01: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) {
2025-01-03 06:14:09 -04:00
const merge = true;
navigation.navigate({ name: launchedBy, params: { onBarScanned: data }, merge });
} else {
onBarScanned && onBarScanned({ data });
2021-07-09 11:52:09 +01:00
}
} 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',
2021-07-09 11:52:09 +01:00
},
],
2021-07-09 11:52:09 +01:00
{ cancelabe: false },
);
2021-07-09 11:52:09 +01:00
}
};
/**
*
* @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) {
2025-01-03 06:14:09 -04:00
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);
Alert.alert(
loc.send.scan_error,
loc._.invalid_animated_qr_code_fragment,
[
{
text: loc._.ok,
onPress: () => {
setIsLoading(false);
},
style: 'default',
},
],
{ cancelabe: false },
);
}
};
2019-12-27 18:53:34 -06: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 11:52:09 +01:00
if (ret.data.toUpperCase().startsWith('UR:CRYPTO-PSBT')) {
return _onReadUniformResourceV2(ret.data);
}
if (ret.data.toUpperCase().startsWith('UR:CRYPTO-OUTPUT')) {
return _onReadUniformResourceV2(ret.data);
}
2021-07-09 11:52:09 +01:00
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) {
2025-01-03 06:14:09 -04:00
const merge = true;
navigation.navigate({ name: launchedBy, params: { onBarScanned: data }, merge });
} else {
onBarScanned && onBarScanned({ data });
}
return;
} catch (_) {}
2020-05-21 11:36:03 -04:00
if (!isLoading) {
2019-12-27 18:53:34 -06:00
setIsLoading(true);
try {
if (launchedBy) {
2025-01-03 06:14:09 -04:00
const merge = true;
navigation.navigate({ name: launchedBy, params: { onBarScanned: ret.data }, merge });
} else {
onBarScanned && onBarScanned(ret.data);
2019-12-27 18:53:34 -06:00
}
} catch (e) {
console.log(e);
}
}
setIsLoading(false);
};
2018-01-30 22:42:38 +00:00
2019-12-31 21:31:04 -06:00
const showFilePicker = async () => {
setIsLoading(true);
const { data } = await fs.showFilePickerAndReadFile();
if (data) onBarCodeRead({ data });
2020-10-05 22:25:14 +01:00
setIsLoading(false);
2019-12-31 21:31:04 -06:00
};
2018-01-30 22:42:38 +00:00
2025-01-06 20:05:55 -04:00
const onShowImagePickerButtonPress = () => {
if (!isLoading) {
setIsLoading(true);
2025-01-03 13:34:55 -04:00
fs.showImagePickerAndReadImage()
.then(data => {
if (data) onBarCodeRead({ data });
})
.finally(() => setIsLoading(false));
}
};
const dismiss = () => {
2025-01-06 20:24:46 -04:00
navigation.goBack();
};
2023-10-17 09:35:10 -04:00
const render = isLoading ? (
<BlueLoading />
) : (
2023-10-17 09:35:10 -04:00
<>
{!cameraStatusGranted ? (
<View style={[styles.openSettingsContainer, stylesHook.openSettingsContainer]}>
<BlueText>{loc.send.permission_camera_message}</BlueText>
<BlueSpacing40 />
2023-11-15 04:40:22 -04:00
<Button title={loc.send.open_settings} onPress={openPrivacyDesktopSettings} />
2023-10-17 09:35:10 -04:00
</View>
2023-10-17 09:51:06 -04:00
) : isFocused ? (
<CameraScreen
scanBarcode
2024-09-01 13:08:33 -04:00
torchOffImage={require('../../img/flash-off.png')}
torchOnImage={require('../../img/flash-on.png')}
cameraFlipImage={require('../../img/camera-rotate-solid.png')}
onReadCode={event => onBarCodeRead({ data: event?.nativeEvent?.codeStringValue })}
showFrame={false}
2025-01-06 20:05:55 -04:00
showFilePickerButton={showFileImportButton}
showImagePickerButton={true}
onFilePickerButtonPress={showFilePicker}
onImagePickerButtonPress={onShowImagePickerButtonPress}
onCancelButtonPress={dismiss}
/>
2023-04-30 20:31:31 +01:00
) : null}
{urTotal > 0 && (
2020-12-14 16:28:10 -05:00
<View style={[styles.progressWrapper, stylesHook.progressWrapper]} testID="UrProgressBar">
2020-12-14 18:00:35 -05:00
<BlueText>{loc.wallets.please_continue_scanning}</BlueText>
2020-11-22 03:04:04 -05:00
<BlueText>
{urHave} / {urTotal}
2020-11-22 03:04:04 -05:00
</BlueText>
</View>
)}
2020-09-28 14:37:41 +01:00
{backdoorVisible && (
<View style={styles.backdoorInputWrapper}>
2020-11-22 03:04:04 -05:00
<BlueText>Provide QR code contents manually:</BlueText>
2020-09-28 14:37:41 +01:00
<TextInput
testID="scanQrBackdoorInput"
multiline
underlineColorAndroid="transparent"
style={[styles.backdoorInput, stylesHook.backdoorInput]}
2020-09-28 14:37:41 +01:00
autoCorrect={false}
autoCapitalize="none"
spellCheck={false}
selectTextOnFocus={false}
keyboardType={Platform.OS === 'android' ? 'visible-password' : 'default'}
value={backdoorText}
onChangeText={setBackdoorText}
/>
2023-11-15 08:54:04 -04:00
<Button
2020-09-28 14:37:41 +01:00
title="OK"
testID="scanQrBackdoorOkButton"
onPress={() => {
setBackdoorVisible(false);
setBackdoorText('');
2020-09-28 14:37:41 +01:00
if (backdoorText) onBarCodeRead({ data: backdoorText });
2020-09-28 14:37:41 +01:00
}}
/>
</View>
)}
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.qr_custom_input_button}
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 17:58:06 +01:00
setBackdoorPressed(0);
2020-09-28 14:37:41 +01:00
setBackdoorVisible(true);
}}
/>
2023-10-17 09:35:10 -04:00
</>
);
2023-10-19 21:54:19 -04:00
return <View style={styles.root}>{render}</View>;
2019-12-27 18:53:34 -06:00
};
2018-03-18 02:48:23 +00:00
2024-05-10 19:57:44 -04:00
export default ScanQRCode;