REF: Use a better QR image decoder

This commit is contained in:
Marcos Rodriguez Velez 2024-10-23 00:27:34 -04:00
parent cebdea6d25
commit e0f906f594
10 changed files with 187 additions and 56 deletions

View file

@ -1,4 +1,3 @@
import LocalQRCode from '@remobile/react-native-qrcode-local-image';
import { Alert, Linking, Platform } from 'react-native';
import DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs';
@ -9,6 +8,7 @@ import presentAlert from '../components/Alert';
import loc from '../loc';
import { isDesktop } from './environment';
import { readFile } from './react-native-bw-file-access';
import RNQRGenerator from 'rn-qr-generator';
const _sanitizeFileName = (fileName: string) => {
// Remove any path delimiters and non-alphanumeric characters except for -, _, and .
@ -128,14 +128,18 @@ export const showImagePickerAndReadImage = (): Promise<string | undefined> => {
if (!response.didCancel) {
const asset = response.assets?.[0] ?? {};
if (asset.uri) {
const uri = asset.uri.toString().replace('file://', '');
LocalQRCode.decode(uri, (error: any, result: string) => {
if (!error) {
resolve(result);
} else {
RNQRGenerator.detect({
uri: decodeURI(asset.uri.toString()),
})
.then(result => {
if (result) {
resolve(result.values[0]);
}
})
.catch(error => {
console.error(error);
reject(new Error(loc.send.qr_error_no_qrcode));
}
});
});
}
}
},
@ -183,13 +187,19 @@ export const showFilePickerAndReadFile = async function (): Promise<{ data: stri
return;
}
const uri2 = res.fileCopyUri.replace('file://', '');
LocalQRCode.decode(decodeURI(uri2), (error: any, result: string) => {
if (!error) {
resolve({ data: result, uri: fileCopyUri });
} else {
RNQRGenerator.detect({
uri: decodeURI(uri2),
})
.then(result => {
if (result) {
resolve({ data: result.values[0], uri: fileCopyUri });
}
})
.catch(error => {
console.error(error);
resolve({ data: false, uri: false });
}
});
});
});
}

View file

@ -5,6 +5,11 @@ import { scanQrHelper } from '../helpers/scan-qr';
import loc from '../loc';
import presentAlert from './Alert';
import ToolTipMenu from './TooltipMenu';
import { CommonToolTipActions } from '../typings/CommonToolTipActions';
import Clipboard from '@react-native-clipboard/clipboard';
import RNQRGenerator from 'rn-qr-generator';
import { useTheme } from './themes';
import { showFilePickerAndReadFile, showImagePickerAndReadImage } from '../blue_modules/fs';
interface AddressInputProps {
isLoading?: boolean;
@ -73,7 +78,7 @@ const AddressInput = ({
}, [launchedBy, onBarScanned, scanButtonTapped]);
const onMenuItemPressed = useCallback(
(action: string) => {
async (action: string) => {
if (onBarScanned === undefined) throw new Error('onBarScanned is required');
switch (action) {
case actionKeys.ScanQR:
@ -87,12 +92,42 @@ const AddressInput = ({
}
break;
case actionKeys.CopyFromClipboard:
Clipboard.getString()
.then(onChangeText)
.catch(error => {
presentAlert({ message: error.message });
});
case CommonToolTipActions.CopyFromClipboard.id:
try {
let getImage: string | null = null;
if (Platform.OS === 'android') {
getImage = await Clipboard.getImage();
} else {
const hasImage = await Clipboard.hasImage();
if (hasImage) {
getImage = await Clipboard.getImageJPG();
}
}
if (getImage) {
try {
const base64Data = getImage.replace(/^data:image\/jpeg;base64,/, '');
const values = await RNQRGenerator.detect({
base64: base64Data,
});
if (values && values.values.length > 0) {
onChangeText(values.values[0]);
} else {
presentAlert({ message: loc.send.qr_error_no_qrcode });
}
} catch (error) {
presentAlert({ message: (error as Error).message });
}
} else {
const clipboardText = await Clipboard.getString();
onChangeText(clipboardText);
}
} catch (error) {
presentAlert({ message: (error as Error).message });
}
break;
case actionKeys.ChoosePhoto:
showImagePickerAndReadImage()

View file

@ -16,6 +16,7 @@ import { Chain } from '../models/bitcoinUnits';
import { navigationRef } from '../NavigationService';
import ActionSheet from '../screen/ActionSheet';
import { useStorage } from '../hooks/context/useStorage';
import RNQRGenerator from 'rn-qr-generator';
const MenuElements = lazy(() => import('../components/MenuElements'));
const DeviceQuickActions = lazy(() => import('../components/DeviceQuickActions'));
@ -104,15 +105,56 @@ const CompanionDelegates = () => {
}, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets]);
const handleOpenURL = useCallback(
(event: { url: string }) => {
DeeplinkSchemaMatch.navigationRouteFor(event, value => navigationRef.navigate(...value), {
wallets,
addWallet,
saveToDisk,
setSharedCosigner,
});
async (event: { url: string }): Promise<void> => {
const { url } = event;
if (url && (url.endsWith('.jpg') || url.endsWith('.png') || url.endsWith('.jpeg'))) {
try {
const response = await fetch(url);
const imageData = await response.blob();
const reader = new FileReader();
reader.onloadend = async () => {
const base64Image = reader.result as string;
const base64Data = base64Image.replace(/^data:image\/jpeg;base64,/, ''); //
try {
const values = await RNQRGenerator.detect({
base64: base64Data,
});
if (values && values.values.length > 0) {
DeeplinkSchemaMatch.navigationRouteFor(
{ url: values.values[0] },
(value: [string, any]) => navigationRef.navigate(...value),
{
wallets,
addWallet,
saveToDisk,
setSharedCosigner,
},
);
} else {
console.log('No QR code detected in the image.');
}
} catch (error) {
console.error('Error detecting QR code:', error);
}
};
reader.readAsDataURL(imageData);
} catch (error) {
console.error('Error fetching image:', error);
}
} else {
// Handle other deeplinks if it's not an image
DeeplinkSchemaMatch.navigationRouteFor(event, value => navigationRef.navigate(...value), {
wallets,
addWallet,
saveToDisk,
setSharedCosigner,
});
}
},
[addWallet, saveToDisk, setSharedCosigner, wallets],
[wallets, addWallet, saveToDisk, setSharedCosigner],
);
const showClipboardAlert = useCallback(

View file

@ -31,6 +31,22 @@
<string>io.bluewallet.psbt</string>
</array>
</dict>
<!-- Image file types -->
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>Image</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>public.jpeg</string>
<string>public.image</string>
</array>
</dict>
<!-- TXN file type -->
<dict>
<key>CFBundleTypeIconFiles</key>
@ -158,7 +174,7 @@
</dict>
</dict>
<key>NSCameraUsageDescription</key>
<string>In order to quickly scan the recipient&apos;s address, we need your permission to use the camera to scan their QR Code.</string>
<string>In order to quickly scan the recipient's address, we need your permission to use the camera to scan their QR Code.</string>
<key>NSFaceIDUsageDescription</key>
<string>In order to use FaceID please confirm your permission.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
@ -209,7 +225,7 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<!-- Define exported types (UTIs) for file types -->
<key>UTExportedTypeDeclarations</key>
<array>
@ -306,6 +322,8 @@
<string>application/json</string>
</array>
</dict>
<key>LSHandlerRank</key>
<string>Alternate</string>
</dict>
</array>
@ -320,4 +338,4 @@
<key>FIREBASE_MESSAGING_AUTO_INIT_ENABLED</key>
<false/>
</dict>
</plist>
</plist>

View file

@ -1317,8 +1317,6 @@ PODS:
- React-Core
- react-native-menu (1.1.3):
- React
- react-native-qrcode-local-image (1.0.4):
- React
- react-native-randombytes (3.6.1):
- React-Core
- react-native-safe-area-context (4.11.1):
@ -1635,6 +1633,9 @@ PODS:
- React-Core
- RNPermissions (4.1.5):
- React-Core
- RNQrGenerator (1.4.2):
- React
- ZXingObjC
- RNQuickAction (0.3.13):
- React
- RNRate (1.2.12):
@ -1819,6 +1820,9 @@ PODS:
- ReactCommon/turbomodule/core
- Yoga
- Yoga (0.0.0)
- ZXingObjC (3.6.9):
- ZXingObjC/All (= 3.6.9)
- ZXingObjC/All (3.6.9)
DEPENDENCIES:
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
@ -1868,7 +1872,6 @@ DEPENDENCIES:
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
- react-native-ios-context-menu (from `../node_modules/react-native-ios-context-menu`)
- "react-native-menu (from `../node_modules/@react-native-menu/menu`)"
- "react-native-qrcode-local-image (from `../node_modules/@remobile/react-native-qrcode-local-image`)"
- react-native-randombytes (from `../node_modules/react-native-randombytes`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-screen-capture (from `../node_modules/react-native-screen-capture`)
@ -1913,6 +1916,7 @@ DEPENDENCIES:
- RNKeychain (from `../node_modules/react-native-keychain`)
- RNLocalize (from `../node_modules/react-native-localize`)
- RNPermissions (from `../node_modules/react-native-permissions`)
- RNQrGenerator (from `../node_modules/rn-qr-generator`)
- RNQuickAction (from `../node_modules/react-native-quick-actions`)
- RNRate (from `../node_modules/react-native-rate`)
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
@ -1930,6 +1934,7 @@ SPEC REPOS:
- CocoaAsyncSocket
- lottie-ios
- SocketRocket
- ZXingObjC
EXTERNAL SOURCES:
boost:
@ -2023,8 +2028,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-ios-context-menu"
react-native-menu:
:path: "../node_modules/@react-native-menu/menu"
react-native-qrcode-local-image:
:path: "../node_modules/@remobile/react-native-qrcode-local-image"
react-native-randombytes:
:path: "../node_modules/react-native-randombytes"
react-native-safe-area-context:
@ -2113,6 +2116,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-localize"
RNPermissions:
:path: "../node_modules/react-native-permissions"
RNQrGenerator:
:path: "../node_modules/rn-qr-generator"
RNQuickAction:
:path: "../node_modules/react-native-quick-actions"
RNRate:
@ -2184,7 +2189,6 @@ SPEC CHECKSUMS:
react-native-image-picker: 2fbbafdae7a7c6db9d25df2f2b1db4442d2ca2ad
react-native-ios-context-menu: e529171ba760a1af7f2ef0729f5a7f4d226171c5
react-native-menu: c30eb7a85d7b04d51945f61ea8a8986ed366ac5c
react-native-qrcode-local-image: 35ccb306e4265bc5545f813e54cc830b5d75bcfc
react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846
react-native-safe-area-context: 5141f11858b033636f1788b14f32eaba92cee810
react-native-screen-capture: 75db9b051c41fea47fa68665506e9257d4b1dadc
@ -2229,6 +2233,7 @@ SPEC CHECKSUMS:
RNKeychain: bfe3d12bf4620fe488771c414530bf16e88f3678
RNLocalize: 4f22418187ecd5ca693231093ff1d912d1b3c9bc
RNPermissions: 9fa74223844f437bc309e112994859dc47194829
RNQrGenerator: 5c12ab86443a07e923735800679da7b6fcaaeb31
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
RNRate: ef3bcff84f39bb1d1e41c5593d3eea4aab2bd73a
RNReactNativeHapticFeedback: 0d591ea1e150f36cb96d868d4e8d77272243d78a
@ -2241,6 +2246,7 @@ SPEC CHECKSUMS:
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
TrueSheet: 49bf7af5d5a29f018f02879c26a1afe595c85829
Yoga: 055f92ad73f8c8600a93f0e25ac0b2344c3b07e6
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
PODFILE CHECKSUM: 95cd28e70ae47c36e1e0b30f60bba0bd6ffa77d8

17
package-lock.json generated
View file

@ -26,7 +26,6 @@
"@react-navigation/drawer": "6.7.2",
"@react-navigation/native": "6.1.18",
"@react-navigation/native-stack": "6.11.0",
"@remobile/react-native-qrcode-local-image": "github:BlueWallet/react-native-qrcode-local-image#31b0113110fbafcf5a5f3ca4183a563550f5c352",
"@rneui/base": "4.0.0-rc.8",
"@rneui/themed": "4.0.0-rc.8",
"@spsina/bip47": "github:BlueWallet/bip47#df82345",
@ -99,6 +98,7 @@
"readable-stream": "3.6.2",
"realm": "12.13.1",
"rn-nodeify": "10.3.0",
"rn-qr-generator": "https://github.com/BlueWallet/rn-qr-generator.git#eacee6d6546eb5b43bfd257255d0a7aa3bd90165",
"scryptsy": "2.1.0",
"silent-payments": "github:BlueWallet/SilentPayments#7ac4d17",
"slip39": "https://github.com/BlueWallet/slip39-js#d316ee6",
@ -6233,12 +6233,6 @@
"node": ">=18"
}
},
"node_modules/@remobile/react-native-qrcode-local-image": {
"version": "1.0.4",
"resolved": "git+ssh://git@github.com/BlueWallet/react-native-qrcode-local-image.git#31b0113110fbafcf5a5f3ca4183a563550f5c352",
"integrity": "sha512-ooA3KFI5wUUx4++U5llEIUs2ADGop7oAOeHsZgisu/4A8Hsw27wJj26d74OonVGYgeZkWXwx6KQDe1dxRa/2WQ==",
"license": "MIT"
},
"node_modules/@rneui/base": {
"version": "4.0.0-rc.8",
"resolved": "https://registry.npmjs.org/@rneui/base/-/base-4.0.0-rc.8.tgz",
@ -21654,6 +21648,15 @@
"semver": "bin/semver"
}
},
"node_modules/rn-qr-generator": {
"version": "1.4.2",
"resolved": "git+ssh://git@github.com/BlueWallet/rn-qr-generator.git#eacee6d6546eb5b43bfd257255d0a7aa3bd90165",
"integrity": "sha512-dTGlzCUuPfaSsBeeVCGDEvzFhw3rWSz+4Fxc98ImJO+Ld/7J7gg4u1LFWhmsfrqsL34Ylm8Lr9cffVzxx3WzwA==",
"license": "MIT",
"peerDependencies": {
"react-native": ">=0.55"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",

View file

@ -90,7 +90,6 @@
"@react-navigation/drawer": "6.7.2",
"@react-navigation/native": "6.1.18",
"@react-navigation/native-stack": "6.11.0",
"@remobile/react-native-qrcode-local-image": "github:BlueWallet/react-native-qrcode-local-image#31b0113110fbafcf5a5f3ca4183a563550f5c352",
"@rneui/base": "4.0.0-rc.8",
"@rneui/themed": "4.0.0-rc.8",
"@spsina/bip47": "github:BlueWallet/bip47#df82345",
@ -163,6 +162,7 @@
"readable-stream": "3.6.2",
"realm": "12.13.1",
"rn-nodeify": "10.3.0",
"rn-qr-generator": "https://github.com/BlueWallet/rn-qr-generator.git#eacee6d6546eb5b43bfd257255d0a7aa3bd90165",
"scryptsy": "2.1.0",
"silent-payments": "github:BlueWallet/SilentPayments#7ac4d17",
"slip39": "https://github.com/BlueWallet/slip39-js#d316ee6",

View file

@ -340,7 +340,11 @@ const Confirm: React.FC = () => {
{state.isLoading ? (
<ActivityIndicator />
) : (
<Button disabled={isElectrumDisabled || state.isButtonDisabled} onPress={handleSendTransaction} title={loc.send.confirm_sendNow} />
<Button
disabled={isElectrumDisabled || state.isButtonDisabled}
onPress={handleSendTransaction}
title={loc.send.confirm_sendNow}
/>
)}
</BlueCard>
</View>
@ -444,4 +448,4 @@ const styles = StyleSheet.create({
fontSize: 15,
fontWeight: 'bold',
},
});
});

View file

@ -1,5 +1,4 @@
import { useFocusEffect, useIsFocused, useNavigation, useRoute } from '@react-navigation/native';
import LocalQRCode from '@remobile/react-native-qrcode-local-image';
import * as bitcoin from 'bitcoinjs-lib';
import createHash from 'create-hash';
import React, { useCallback, useEffect, useState } from 'react';
@ -19,6 +18,7 @@ import { useTheme } from '../../components/themes';
import { isCameraAuthorizationStatusGranted } from '../../helpers/scan-qr';
import loc from '../../loc';
import { useSettings } from '../../hooks/context/useSettings';
import RNQRGenerator from 'rn-qr-generator';
let decoder = false;
@ -312,15 +312,21 @@ const ScanQRCode = () => {
} 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 {
RNQRGenerator.detect({
uri: decodeURI(asset.uri.toString()),
})
.then(result => {
if (result) {
onBarCodeRead({ data: result.values[0] });
}
})
.catch(error => {
console.error(error);
presentAlert({ message: loc.send.qr_error_no_qrcode });
})
.finally(() => {
setIsLoading(false);
}
});
});
} else {
setIsLoading(false);
}

View file

@ -22,6 +22,7 @@ const keys = {
RemoveAllRecipients: 'RemoveAllRecipients',
AddRecipient: 'AddRecipient',
RemoveRecipient: 'RemoveRecipient',
CopyFromClipboard: 'copyFromClipboard',
};
const icons = {
@ -73,6 +74,7 @@ const icons = {
RemoveAllRecipients: { iconValue: 'person.2.slash' },
AddRecipient: { iconValue: 'person.badge.plus' },
RemoveRecipient: { iconValue: 'person.badge.minus' },
CopyFromClipboard: { iconValue: 'doc.on.clipboard' },
};
export const CommonToolTipActions = {
@ -185,4 +187,9 @@ export const CommonToolTipActions = {
icon: icons.PaymentsCode,
menuState: false,
},
CopyFromClipboard: {
id: keys.CopyFromClipboard,
text: loc.transactions.details_copy_amount,
icon: icons.CopyFromClipboard,
},
};