From c5c2ce8a614db469e6c300d7e02f2f1ff24403db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Wed, 1 Jan 2025 20:32:28 -0400 Subject: [PATCH 01/11] Update LightningSettings.tsx --- screen/settings/LightningSettings.tsx | 41 ++++++++++++++------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/screen/settings/LightningSettings.tsx b/screen/settings/LightningSettings.tsx index 44416e6d8..409f88517 100644 --- a/screen/settings/LightningSettings.tsx +++ b/screen/settings/LightningSettings.tsx @@ -106,29 +106,30 @@ const LightningSettings: React.FC = () => { setURI(typeof setLndHubUrl === 'string' ? setLndHubUrl.trim() : value.trim()); }; - const save = useCallback(async () => { - setIsLoading(true); - try { - await DefaultPreference.setName(GROUP_IO_BLUEWALLET); - if (URI) { - const normalizedURI = new URL(URI.replace(/([^:]\/)\/+/g, '$1')).toString(); +const save = useCallback(async () => { + setIsLoading(true); + try { + await DefaultPreference.setName(GROUP_IO_BLUEWALLET); + if (URI) { + const normalizedURI = new URL(URI.replace(/([^:]\/)\/+/g, '$1')).toString(); - await LightningCustodianWallet.isValidNodeAddress(normalizedURI); + // Validate the normalized URI + await LightningCustodianWallet.isValidNodeAddress(normalizedURI); - await setLNDHub(normalizedURI); - } else { - await clearLNDHub(); - } - - presentAlert({ message: loc.settings.lightning_saved, type: AlertType.Toast }); - triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); - } catch (error) { - triggerHapticFeedback(HapticFeedbackTypes.NotificationError); - presentAlert({ message: loc.settings.lightning_error_lndhub_uri }); - console.log(error); + await setLNDHub(normalizedURI); + } else { + await clearLNDHub(); } - setIsLoading(false); - }, [URI]); + + presentAlert({ message: loc.settings.lightning_saved, type: AlertType.Toast }); + triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); + } catch (error) { + triggerHapticFeedback(HapticFeedbackTypes.NotificationError); + presentAlert({ message: loc.settings.lightning_error_lndhub_uri }); + console.log(error); + } + setIsLoading(false); +}, [URI]); const importScan = () => { scanQrHelper(route.name).then(data => { From e93259e39e241f986d53eaa20fc52a32926925a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Wed, 1 Jan 2025 21:07:51 -0400 Subject: [PATCH 02/11] Update LightningSettings.tsx --- screen/settings/LightningSettings.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/screen/settings/LightningSettings.tsx b/screen/settings/LightningSettings.tsx index 409f88517..5d8454144 100644 --- a/screen/settings/LightningSettings.tsx +++ b/screen/settings/LightningSettings.tsx @@ -112,8 +112,6 @@ const save = useCallback(async () => { await DefaultPreference.setName(GROUP_IO_BLUEWALLET); if (URI) { const normalizedURI = new URL(URI.replace(/([^:]\/)\/+/g, '$1')).toString(); - - // Validate the normalized URI await LightningCustodianWallet.isValidNodeAddress(normalizedURI); await setLNDHub(normalizedURI); From 17f95d5634ab84cdf4d8dd2ce05754fcba5e3139 Mon Sep 17 00:00:00 2001 From: overtorment Date: Thu, 2 Jan 2025 14:55:37 +0000 Subject: [PATCH 03/11] FIX: import Casa multisig wallet descriptor (closes #7395) --- class/wallets/multisig-hd-wallet.ts | 6 +++++- tests/unit/multisig-hd-wallet.test.js | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/class/wallets/multisig-hd-wallet.ts b/class/wallets/multisig-hd-wallet.ts index 7f886e918..c160bbd24 100644 --- a/class/wallets/multisig-hd-wallet.ts +++ b/class/wallets/multisig-hd-wallet.ts @@ -629,7 +629,11 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet { hexFingerprint = Buffer.from(hexFingerprint, 'hex').toString('hex'); } - const path = 'm/' + m[1].split('/').slice(1).join('/').replace(/[h]/g, "'"); + let path = 'm/' + m[1].split('/').slice(1).join('/').replace(/[h]/g, "'"); + if (path === 'm/') { + // not considered valid by Bip32 lib + path = 'm/0'; + } let xpub = m[2]; if (xpub.indexOf('/') !== -1) { xpub = xpub.substr(0, xpub.indexOf('/')); diff --git a/tests/unit/multisig-hd-wallet.test.js b/tests/unit/multisig-hd-wallet.test.js index d547c3ee0..9887f9c3f 100644 --- a/tests/unit/multisig-hd-wallet.test.js +++ b/tests/unit/multisig-hd-wallet.test.js @@ -2005,6 +2005,20 @@ describe('multisig-wallet (native segwit)', () => { assert.strictEqual(w._getInternalAddressByIndex(0), w3coordinator._getInternalAddressByIndex(0)); }); + it('can import wallet descriptor from Casa multisig', () => { + const w = new MultisigHDWallet(); + w.setSecret( + 'sh(wsh(sortedmulti(3,[35282ae3]Ypub6j9gr7f7uvWfTHdwVwhYRowZH9oqkh9DZz2hdbeKJw7yjQbsfqaruLva2piTZqN7jSKT43LpsnAqHU4vufUGAyGBzD6UJpwTeKiG5JRYUnm/<0;1>/*,[1b373e31/49/0/5]Ypub6iPAfqqinG5VHf2sJ3XfRjFh8BXm4C6x4U3DaPyu4RKocnqzb1peStw8Q624jwXpMZ5G81s7uwq8qLQ8rW55aZ3LvBbr9XVLTYxG78k28PE/<0;1>/*,[0d7bb846/49/0/5]Ypub6hwdyxEzktht47xgyyFY7TFrAzW7XgGbMSqS3QzfosYekG9LibhpohY6LrsjfP55VyU8i7iWS2s2Vs6RkAeze6bSpU3rNHsmESymE8X3J3k/<0;1>/*,[faa45c74/49/0/5]Ypub6iSiDkVrTTEh6eFbkCvohn1FfuhGr1Co9MCCHi6VPTT6crnX9Avq3PnZj7JcqhDTkWoDLEyeWmaPyxDY22b7k652Sg2eMc2g5tU6GDTvSng/<0;1>/*,[0551354e/49/0/5]Ypub6ipvk7JbDUXormRsSxXRf9eTUAVFwcjDfZXWYxRdN6nV7MNnnuD3WZBYjjWnMTncHhKJsnfkUGVYMRgQthZqY2wfHfMaJoVYBBhHT7MACGs/<0;1>/*)))', + ); + + assert.strictEqual(w._getExternalAddressByIndex(0), '3HJiAohE25FFBLPLZGVDwv7ZbSXVsSiZH7'); + assert.strictEqual(w._getExternalAddressByIndex(1), '3GBvQK1iHJ9dw7H8datM4REtYdAu9iQjeh'); + assert.strictEqual(w._getExternalAddressByIndex(2), '3KrAqZwVwND5XFNEAnCuWyf4nPNAfF2JCF'); + assert.strictEqual(w._getInternalAddressByIndex(0), '36oNF12VNk4G5hPRR5zuZixnNjJikkTSWD'); + assert.strictEqual(w._getInternalAddressByIndex(1), '3Q7SnCQK9DYPpFviiEB7NQHa1FJqprCWNX'); + assert.strictEqual(w._getInternalAddressByIndex(2), '33Rkb8XURWkxZBN6dpdk64qa4raK2bRFJS'); + }); + it('can import descriptor from Sparrow', () => { const payload = 'UR:CRYPTO-OUTPUT/TAADMETAADMSOEADAOAOLSTAADDLOLAOWKAXHDCLAOCEBDFLNNTKJTIOJSFSURBNFXRPEEHKDLGYRTEMRPYTGYZOCASWENCYMKPAVWJKHYAAHDCXJEFTGSZOIMFEYNDYHYZEJTBAMSJEHLDSRDDIYLSRFYTSZTKNRNYLRNDPAMTLDPZCAHTAADEHOEADAEAOAEAMTAADDYOTADLOCSDYYKAEYKAEYKAOYKAOCYUOHFJPKOAXAAAYCYCSYASAVDTAADDLOLAOWKAXHDCLAXMSZTWZDIGERYDKFSFWTYDPFNDKLNAYSWTTMUHYZTOXHSETPEWSFXPEAYWLJSDEMTAAHDCXSPLTSTDPNTLESANSUTTLPRPFHNVSPFCNMHESOYGASTLRPYVAATNNDKFYHLQZPKLEAHTAADEHOEADAEAOAEAMTAADDYOTADLOCSDYYKAEYKAEYKAOYKAOCYWZFEPLETAXAAAYCYCPCKRENBTAADDLOLAOWKAXHDCLAOLSFWYKYLKTFHJLPYEMGLCEDPFNSNRDDSRFASEOZTGWIALFLUIYDNFXHGVESFEMMEAAHDCXHTZETLJNKPHHAYLSCXWPNDSWPSTPGTEOJKKGHDAELSKPNNBKBSYAWZJTFWNNBDKTAHTAADEHOEADAEAOAEAMTAADDYOTADLOCSDYYKAEYKAEYKAOYKAOCYSKTPJPMSAXAAAYCYCEBKWLAMTDWZGRZE\n'; From 92c8bdc202b39e1b60c28c439ebca0a8df5c5890 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Thu, 2 Jan 2025 20:58:04 -0400 Subject: [PATCH 04/11] FIX: If theres no valid file then dont attempt to scan for qr --- components/CompanionDelegates.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/components/CompanionDelegates.tsx b/components/CompanionDelegates.tsx index 03f3007f1..912ef54df 100644 --- a/components/CompanionDelegates.tsx +++ b/components/CompanionDelegates.tsx @@ -183,6 +183,9 @@ const CompanionDelegates = () => { if (fileName && /\.(jpe?g|png)$/i.test(fileName)) { try { + if (!decodedUrl) { + throw new Error(loc.send.qr_error_no_qrcode); + } const values = await RNQRGenerator.detect({ uri: decodedUrl, }); @@ -200,11 +203,12 @@ const CompanionDelegates = () => { }, ); } else { - triggerHapticFeedback(HapticFeedbackTypes.NotificationError); - presentAlert({ message: loc.send.qr_error_no_qrcode }); + throw new Error(loc.send.qr_error_no_qrcode); } } catch (error) { console.error('Error detecting QR code:', error); + triggerHapticFeedback(HapticFeedbackTypes.NotificationError); + presentAlert({ message: loc.send.qr_error_no_qrcode }); } } else { DeeplinkSchemaMatch.navigationRouteFor(event, (value: [string, any]) => navigationRef.navigate(...value), { From 61097732cb8c2e3ab3b2a1d3ed3c108d9c78bc4e Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Thu, 2 Jan 2025 22:05:07 -0400 Subject: [PATCH 05/11] FIX: Price value should autosize --- android/app/src/main/res/layout/widget_layout.xml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/res/layout/widget_layout.xml b/android/app/src/main/res/layout/widget_layout.xml index 2f0285aa4..addec03a8 100644 --- a/android/app/src/main/res/layout/widget_layout.xml +++ b/android/app/src/main/res/layout/widget_layout.xml @@ -58,13 +58,20 @@ style="@style/WidgetTextPrimary" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_gravity="end" + android:layout_marginEnd="8dp" + android:layout_marginBottom="8dp" + android:autoSizeMaxTextSize="24sp" + android:autoSizeMinTextSize="12sp" + android:autoSizeStepGranularity="2sp" + android:autoSizeTextType="uniform" + android:duplicateParentState="false" + android:editable="false" + android:lines="1" android:text="Loading..." android:textSize="24sp" android:textStyle="bold" - android:layout_marginEnd="8dp" - android:layout_marginBottom="8dp" - android:visibility="gone" - android:layout_gravity="end"/> + android:visibility="gone" /> Date: Thu, 2 Jan 2025 23:10:10 -0400 Subject: [PATCH 06/11] Update Info.plist --- ios/BlueWallet/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/BlueWallet/Info.plist b/ios/BlueWallet/Info.plist index 5fcc7826a..d7b145bf5 100644 --- a/ios/BlueWallet/Info.plist +++ b/ios/BlueWallet/Info.plist @@ -137,7 +137,7 @@ NSAppTransportSecurity NSAllowsArbitraryLoads - + NSAllowsLocalNetworking NSExceptionDomains From 8ea558345860ec6e04e640e0fa78b46d6eb668b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Fri, 3 Jan 2025 06:14:09 -0400 Subject: [PATCH 07/11] REF: ScanQRCode navigation (#7444) --- components/AddressInputScanButton.tsx | 39 ++- helpers/scan-qr.ts | 51 +-- navigation/AddWalletStack.tsx | 15 +- navigation/DetailViewScreensStack.tsx | 21 +- navigation/DetailViewStackParamList.ts | 42 +-- navigation/LNDCreateInvoiceStack.tsx | 11 + navigation/ScanLndInvoiceStack.tsx | 11 + navigation/ScanQRCodeStack.tsx | 28 -- navigation/SendDetailsStack.tsx | 11 + navigation/SendDetailsStackParamList.ts | 3 + navigation/ViewEditMultisigCosignersStack.tsx | 14 + screen/lnd/lndCreateInvoice.js | 184 ++++++----- screen/lnd/scanLndInvoice.js | 23 +- screen/send/Broadcast.tsx | 42 +-- screen/send/ScanQRCode.js | 48 ++- screen/send/SendDetails.tsx | 254 ++++++++------- screen/send/psbtMultisigQRCode.js | 50 +-- screen/send/psbtWithHardwareWallet.js | 72 +++-- screen/settings/ElectrumSettings.tsx | 23 +- screen/settings/IsItMyAddress.tsx | 11 +- screen/settings/LightningSettings.tsx | 28 +- screen/wallets/ImportWallet.tsx | 19 +- screen/wallets/ViewEditMultisigCosigners.tsx | 33 +- screen/wallets/WalletTransactions.tsx | 61 ++-- screen/wallets/WalletsList.tsx | 60 ++-- screen/wallets/addMultisigStep2.js | 301 +++++++++--------- typings/CommonToolTipActions.ts | 12 +- 27 files changed, 802 insertions(+), 665 deletions(-) delete mode 100644 navigation/ScanQRCodeStack.tsx diff --git a/components/AddressInputScanButton.tsx b/components/AddressInputScanButton.tsx index ac2001e33..5c7a0bcc6 100644 --- a/components/AddressInputScanButton.tsx +++ b/components/AddressInputScanButton.tsx @@ -1,15 +1,16 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { Image, Keyboard, Platform, StyleSheet, Text } from 'react-native'; import Clipboard from '@react-native-clipboard/clipboard'; import ToolTipMenu from './TooltipMenu'; import loc from '../loc'; -import { scanQrHelper } from '../helpers/scan-qr'; import { showFilePickerAndReadFile, showImagePickerAndReadImage } from '../blue_modules/fs'; import presentAlert from './Alert'; import { useTheme } from './themes'; import RNQRGenerator from 'rn-qr-generator'; import { CommonToolTipActions } from '../typings/CommonToolTipActions'; import { useSettings } from '../hooks/context/useSettings'; +import { useRoute } from '@react-navigation/native'; +import { useExtendedNavigation } from '../hooks/useExtendedNavigation'; interface AddressInputScanButtonProps { isLoading: boolean; @@ -19,6 +20,10 @@ interface AddressInputScanButtonProps { onChangeText: (text: string) => void; } +interface RouteParams { + onBarScanned?: any; +} + export const AddressInputScanButton = ({ isLoading, launchedBy, @@ -28,6 +33,9 @@ export const AddressInputScanButton = ({ }: AddressInputScanButtonProps) => { const { colors } = useTheme(); const { isClipboardGetContentEnabled } = useSettings(); + + const navigation = useExtendedNavigation(); + const params = useRoute().params as RouteParams; const stylesHook = StyleSheet.create({ scan: { backgroundColor: colors.scanLabel, @@ -40,8 +48,10 @@ export const AddressInputScanButton = ({ const toolTipOnPress = useCallback(async () => { await scanButtonTapped(); Keyboard.dismiss(); - if (launchedBy) scanQrHelper(launchedBy, true).then(value => onBarScanned({ data: value })); - }, [launchedBy, onBarScanned, scanButtonTapped]); + navigation.navigate('ScanQRCode', { + showFileImportButton: true, + }); + }, [navigation, scanButtonTapped]); const actions = useMemo(() => { const availableActions = [ @@ -57,20 +67,23 @@ export const AddressInputScanButton = ({ return availableActions; }, [isClipboardGetContentEnabled]); + useEffect(() => { + const data = params.onBarScanned; + if (data) { + onBarScanned({ data }); + navigation.setParams({ onBarScanned: undefined }); + } + }); + const onMenuItemPressed = useCallback( async (action: string) => { if (onBarScanned === undefined) throw new Error('onBarScanned is required'); switch (action) { case CommonToolTipActions.ScanQR.id: scanButtonTapped(); - if (launchedBy) { - scanQrHelper(launchedBy) - .then(value => onBarScanned({ data: value })) - .catch(error => { - presentAlert({ message: error.message }); - }); - } - + navigation.navigate('ScanQRCode', { + showFileImportButton: true, + }); break; case CommonToolTipActions.PasteFromClipboard.id: try { @@ -134,7 +147,7 @@ export const AddressInputScanButton = ({ } Keyboard.dismiss(); }, - [launchedBy, onBarScanned, onChangeText, scanButtonTapped], + [navigation, onBarScanned, onChangeText, scanButtonTapped], ); const buttonStyle = useMemo(() => [styles.scan, stylesHook.scan], [stylesHook.scan]); diff --git a/helpers/scan-qr.ts b/helpers/scan-qr.ts index b4b7cb67f..1224a0135 100644 --- a/helpers/scan-qr.ts +++ b/helpers/scan-qr.ts @@ -1,54 +1,5 @@ import { Platform } from 'react-native'; import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions'; -import { navigationRef } from '../NavigationService'; - -/** - * Helper function that navigates to ScanQR screen, and returns promise that will resolve with the result of a scan, - * and then navigates back. If QRCode scan was closed, promise resolves to null. - * - * @param currentScreenName {string} - * @param showFileImportButton {boolean} - * - * @param onDismiss {function} - if camera is closed via X button it gets triggered - * @param useMerge {boolean} - if true, will merge the new screen with the current screen, otherwise will replace the current screen - * @return {Promise} - */ -function scanQrHelper( - currentScreenName: string, - showFileImportButton = true, - onDismiss?: () => void, - useMerge = true, -): Promise { - return requestCameraAuthorization().then(() => { - return new Promise(resolve => { - let params = {}; - - if (useMerge) { - const onBarScanned = function (data: any) { - setTimeout(() => resolve(data.data || data), 1); - navigationRef.navigate({ name: currentScreenName, params: data, merge: true }); - }; - - params = { - showFileImportButton: Boolean(showFileImportButton), - onDismiss, - onBarScanned, - }; - } else { - params = { launchedBy: currentScreenName, showFileImportButton: Boolean(showFileImportButton) }; - } - - navigationRef.navigate({ - name: 'ScanQRCodeRoot', - params: { - screen: 'ScanQRCode', - params, - }, - merge: true, - }); - }); - }); -} const isCameraAuthorizationStatusGranted = async () => { const status = await check(Platform.OS === 'android' ? PERMISSIONS.ANDROID.CAMERA : PERMISSIONS.IOS.CAMERA); @@ -59,4 +10,4 @@ const requestCameraAuthorization = () => { return request(Platform.OS === 'android' ? PERMISSIONS.ANDROID.CAMERA : PERMISSIONS.IOS.CAMERA); }; -export { scanQrHelper, isCameraAuthorizationStatusGranted, requestCameraAuthorization }; +export { isCameraAuthorizationStatusGranted, requestCameraAuthorization }; diff --git a/navigation/AddWalletStack.tsx b/navigation/AddWalletStack.tsx index 5e24820f7..8c52c448a 100644 --- a/navigation/AddWalletStack.tsx +++ b/navigation/AddWalletStack.tsx @@ -17,13 +17,15 @@ import { WalletsAddMultisigHelpComponent, WalletsAddMultisigStep2Component, } from './LazyLoadAddWalletStack'; +import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack'; +import { ScanQRCodeParamList } from './DetailViewStackParamList'; export type AddWalletStackParamList = { AddWallet: undefined; ImportWallet?: { label?: string; triggerImport?: boolean; - scannedData?: string; + onBarScanned?: string; }; ImportWalletDiscovery: { importText: string; @@ -55,6 +57,7 @@ export type AddWalletStackParamList = { format: string; }; WalletsAddMultisigHelp: undefined; + ScanQRCode: ScanQRCodeParamList; }; const Stack = createNativeStackNavigator(); @@ -138,6 +141,16 @@ const AddWalletStack = () => { headerShadowVisible: false, })(theme)} /> + ); }; diff --git a/navigation/DetailViewScreensStack.tsx b/navigation/DetailViewScreensStack.tsx index 14425f408..4efd12b3e 100644 --- a/navigation/DetailViewScreensStack.tsx +++ b/navigation/DetailViewScreensStack.tsx @@ -33,7 +33,6 @@ import PaymentCodesListComponent from './LazyLoadPaymentCodeStack'; import LNDCreateInvoiceRoot from './LNDCreateInvoiceStack'; import ReceiveDetailsStackRoot from './ReceiveDetailsStack'; import ScanLndInvoiceRoot from './ScanLndInvoiceStack'; -import ScanQRCodeStackRoot from './ScanQRCodeStack'; import SendDetailsStack from './SendDetailsStack'; import SignVerifyStackRoot from './SignVerifyStack'; import ViewEditMultisigCosignersStackRoot from './ViewEditMultisigCosignersStack'; @@ -65,6 +64,7 @@ import SelfTest from '../screen/settings/SelfTest'; import ReleaseNotes from '../screen/settings/ReleaseNotes'; import ToolsScreen from '../screen/settings/tools'; import SettingsPrivacy from '../screen/settings/SettingsPrivacy'; +import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack'; const DetailViewStackScreensStack = () => { const theme = useTheme(); @@ -358,15 +358,6 @@ const DetailViewStackScreensStack = () => { options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions }} /> - { statusBarStyle: 'auto', })(theme)} /> + ); }; diff --git a/navigation/DetailViewStackParamList.ts b/navigation/DetailViewStackParamList.ts index 4f0aae54c..036c2d6de 100644 --- a/navigation/DetailViewStackParamList.ts +++ b/navigation/DetailViewStackParamList.ts @@ -2,10 +2,24 @@ import { LightningTransaction, Transaction, TWallet } from '../class/wallets/typ import { ElectrumServerItem } from '../screen/settings/ElectrumSettings'; import { SendDetailsParams } from './SendDetailsStackParamList'; +export type ScanQRCodeParamList = { + cameraStatusGranted?: boolean; + backdoorPressed?: boolean; + launchedBy?: string; + urTotal?: number; + urHave?: number; + backdoorText?: string; + onDismiss?: () => void; + onBarScanned?: (data: string) => void; + showFileImportButton?: boolean; + backdoorVisible?: boolean; + animatedQRCodeData?: Record; +}; + export type DetailViewStackParamList = { UnlockWithScreen: undefined; - WalletsList: { scannedData?: string }; - WalletTransactions: { isLoading?: boolean; walletID: string; walletType: string }; + WalletsList: { onBarScanned?: string }; + WalletTransactions: { isLoading?: boolean; walletID: string; walletType: string; onBarScanned?: string }; WalletDetails: { walletID: string }; TransactionDetails: { tx: Transaction; hash: string; walletID: string }; TransactionStatus: { hash: string; walletID?: string }; @@ -19,8 +33,8 @@ export type DetailViewStackParamList = { LNDViewInvoice: { invoice: LightningTransaction; walletID: string }; LNDViewAdditionalInvoiceInformation: { invoiceId: string }; LNDViewAdditionalInvoicePreImage: { invoiceId: string }; - Broadcast: { scannedData?: string }; - IsItMyAddress: { address?: string }; + Broadcast: { onBarScanned?: string }; + IsItMyAddress: { address?: string; onBarScanned?: string }; GenerateWord: undefined; LnurlPay: undefined; LnurlPaySuccess: { @@ -57,12 +71,13 @@ export type DetailViewStackParamList = { NetworkSettings: undefined; About: undefined; DefaultView: undefined; - ElectrumSettings: { server?: ElectrumServerItem }; + ElectrumSettings: { server?: ElectrumServerItem; onBarScanned?: string }; SettingsBlockExplorer: undefined; EncryptStorage: undefined; Language: undefined; LightningSettings: { url?: string; + onBarScanned?: string; }; NotificationSettings: undefined; SelfTest: undefined; @@ -85,22 +100,7 @@ export type DetailViewStackParamList = { address: string; }; }; - ScanQRCodeRoot: { - screen: string; - params: { - isLoading: false; - cameraStatusGranted?: boolean; - backdoorPressed?: boolean; - launchedBy?: string; - urTotal?: number; - urHave?: number; - backdoorText?: string; - onDismiss?: () => void; - showFileImportButton: true; - backdoorVisible?: boolean; - animatedQRCodeData?: Record; - }; - }; + ScanQRCode: ScanQRCodeParamList; PaymentCodeList: { paymentCode: string; walletID: string; diff --git a/navigation/LNDCreateInvoiceStack.tsx b/navigation/LNDCreateInvoiceStack.tsx index 45b431572..c3fa0bf17 100644 --- a/navigation/LNDCreateInvoiceStack.tsx +++ b/navigation/LNDCreateInvoiceStack.tsx @@ -10,6 +10,7 @@ import { LNDViewInvoiceComponent, SelectWalletComponent, } from './LazyLoadLNDCreateInvoiceStack'; +import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack'; const Stack = createNativeStackNavigator(); @@ -54,6 +55,16 @@ const LNDCreateInvoiceRoot = () => { component={LNDViewAdditionalInvoicePreImageComponent} options={navigationStyle({ title: loc.lndViewInvoice.additional_info })(theme)} /> + ); }; diff --git a/navigation/ScanLndInvoiceStack.tsx b/navigation/ScanLndInvoiceStack.tsx index 1b2fb4d2d..2510bb413 100644 --- a/navigation/ScanLndInvoiceStack.tsx +++ b/navigation/ScanLndInvoiceStack.tsx @@ -11,6 +11,7 @@ import { SelectWalletComponent, SuccessComponent, } from './LazyLoadScanLndInvoiceStack'; +import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack'; const Stack = createNativeStackNavigator(); @@ -50,6 +51,16 @@ const ScanLndInvoiceRoot = () => { gestureEnabled: false, })(theme)} /> + ); }; diff --git a/navigation/ScanQRCodeStack.tsx b/navigation/ScanQRCodeStack.tsx deleted file mode 100644 index 119807eae..000000000 --- a/navigation/ScanQRCodeStack.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import React from 'react'; - -import navigationStyle from '../components/navigationStyle'; -import { useTheme } from '../components/themes'; -import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack'; - -const Stack = createNativeStackNavigator(); - -const ScanQRCodeStackRoot = () => { - const theme = useTheme(); - - return ( - - - - ); -}; - -export default ScanQRCodeStackRoot; diff --git a/navigation/SendDetailsStack.tsx b/navigation/SendDetailsStack.tsx index ec5fbc0da..feba8d8ad 100644 --- a/navigation/SendDetailsStack.tsx +++ b/navigation/SendDetailsStack.tsx @@ -18,6 +18,7 @@ import { import { SendDetailsStackParamList } from './SendDetailsStackParamList'; import HeaderRightButton from '../components/HeaderRightButton'; import { BitcoinUnit } from '../models/bitcoinUnits'; +import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack'; const Stack = createNativeStackNavigator(); @@ -81,6 +82,16 @@ const SendDetailsStack = () => { component={PaymentCodesListComponent} options={navigationStyle({ title: loc.bip47.contacts })(theme)} /> + ); }; diff --git a/navigation/SendDetailsStackParamList.ts b/navigation/SendDetailsStackParamList.ts index f29c53eff..5cfc21a71 100644 --- a/navigation/SendDetailsStackParamList.ts +++ b/navigation/SendDetailsStackParamList.ts @@ -1,6 +1,7 @@ import { Psbt } from 'bitcoinjs-lib'; import { CreateTransactionTarget, CreateTransactionUtxo, TWallet } from '../class/wallets/types'; import { BitcoinUnit, Chain } from '../models/bitcoinUnits'; +import { ScanQRCodeParamList } from './DetailViewStackParamList'; export type SendDetailsParams = { transactionMemo?: string; @@ -12,6 +13,7 @@ export type SendDetailsParams = { address?: string; amount?: number; amountSats?: number; + onBarScanned?: string; unit?: BitcoinUnit; noRbf?: boolean; walletID: string; @@ -84,4 +86,5 @@ export type SendDetailsStackParamList = { PaymentCodeList: { walletID: string; }; + ScanQRCode: ScanQRCodeParamList; }; diff --git a/navigation/ViewEditMultisigCosignersStack.tsx b/navigation/ViewEditMultisigCosignersStack.tsx index fb5aeba69..6e7d42aec 100644 --- a/navigation/ViewEditMultisigCosignersStack.tsx +++ b/navigation/ViewEditMultisigCosignersStack.tsx @@ -5,11 +5,15 @@ import navigationStyle from '../components/navigationStyle'; import { useTheme } from '../components/themes'; import loc from '../loc'; import { ViewEditMultisigCosignersComponent } from './LazyLoadViewEditMultisigCosignersStack'; +import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack'; +import { ScanQRCodeParamList } from './DetailViewStackParamList'; export type ViewEditMultisigCosignersStackParamList = { ViewEditMultisigCosigners: { walletID: string; + onBarScanned?: string; }; + ScanQRCode: ScanQRCodeParamList; }; const Stack = createNativeStackNavigator(); @@ -27,6 +31,16 @@ const ViewEditMultisigCosignersStackRoot = () => { title: loc.multisig.manage_keys, })(theme)} /> + ); }; diff --git a/screen/lnd/lndCreateInvoice.js b/screen/lnd/lndCreateInvoice.js index ea0b7139a..11fe0d9f2 100644 --- a/screen/lnd/lndCreateInvoice.js +++ b/screen/lnd/lndCreateInvoice.js @@ -24,7 +24,6 @@ import AmountInput from '../../components/AmountInput'; import Button from '../../components/Button'; import { useTheme } from '../../components/themes'; import { presentWalletExportReminder } from '../../helpers/presentWalletExportReminder'; -import { scanQrHelper } from '../../helpers/scan-qr'; import loc, { formatBalance, formatBalancePlain, formatBalanceWithoutSuffix } from '../../loc'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; import * as NavigationService from '../../NavigationService'; @@ -36,7 +35,7 @@ const LNDCreateInvoice = () => { const { wallets, saveToDisk, setSelectedWalletID } = useStorage(); const { walletID, uri } = useRoute().params; const wallet = useRef(wallets.find(item => item.getID() === walletID) || wallets.find(item => item.chain === Chain.OFFCHAIN)); - const { name } = useRoute(); + const { params } = useRoute(); const { colors } = useTheme(); const { navigate, getParent, goBack, pop, setParams } = useNavigation(); const [unit, setUnit] = useState(wallet.current?.getPreferredBalanceUnit() || BitcoinUnit.BTC); @@ -75,6 +74,100 @@ const LNDCreateInvoice = () => { }, }); + const processLnurl = useCallback( + async data => { + setIsLoading(true); + if (!wallet.current) { + triggerHapticFeedback(HapticFeedbackTypes.NotificationError); + presentAlert({ message: loc.wallets.no_ln_wallet_error }); + return goBack(); + } + + // decoding the lnurl + const url = Lnurl.getUrlFromLnurl(data); + const { query } = parse(url, true); + + if (query.tag === Lnurl.TAG_LOGIN_REQUEST) { + navigate('LnurlAuth', { + lnurl: data, + walletID: walletID ?? wallet.current.getID(), + }); + return; + } + + // calling the url + try { + const resp = await fetch(url, { method: 'GET' }); + if (resp.status >= 300) { + throw new Error('Bad response from server'); + } + const reply = await resp.json(); + if (reply.status === 'ERROR') { + throw new Error('Reply from server: ' + reply.reason); + } + + if (reply.tag === Lnurl.TAG_PAY_REQUEST) { + // we are here by mistake. user wants to SEND to lnurl-pay, but he is on a screen that creates + // invoices (including through lnurl-withdraw) + navigate('ScanLndInvoiceRoot', { + screen: 'LnurlPay', + params: { + lnurl: data, + walletID: walletID ?? wallet.current.getID(), + }, + }); + return; + } + + if (reply.tag !== Lnurl.TAG_WITHDRAW_REQUEST) { + throw new Error('Unsupported lnurl'); + } + + // amount that comes from lnurl is always in sats + let newAmount = (reply.maxWithdrawable / 1000).toString(); + const sats = newAmount; + switch (unit) { + case BitcoinUnit.SATS: + // nop + break; + case BitcoinUnit.BTC: + newAmount = satoshiToBTC(newAmount); + break; + case BitcoinUnit.LOCAL_CURRENCY: + newAmount = formatBalancePlain(newAmount, BitcoinUnit.LOCAL_CURRENCY); + AmountInput.setCachedSatoshis(newAmount, sats); + break; + } + + // setting the invoice creating screen with the parameters + setLNURLParams({ + k1: reply.k1, + callback: reply.callback, + fixed: reply.minWithdrawable === reply.maxWithdrawable, + min: (reply.minWithdrawable || 0) / 1000, + max: reply.maxWithdrawable / 1000, + }); + setAmount(newAmount); + setDescription(reply.defaultDescription); + setIsLoading(false); + } catch (Err) { + Keyboard.dismiss(); + setIsLoading(false); + triggerHapticFeedback(HapticFeedbackTypes.NotificationError); + presentAlert({ message: Err.message }); + } + }, + [goBack, navigate, unit, walletID], + ); + + useEffect(() => { + const data = params.onBarScanned; + if (data) { + processLnurl(data); + setParams({ onBarScanned: undefined }); + } + }, [params.onBarScanned, processLnurl, setParams]); + useEffect(() => { const showSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', _keyboardDidShow); const hideSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', _keyboardDidHide); @@ -228,89 +321,6 @@ const LNDCreateInvoice = () => { } }; - const processLnurl = async data => { - setIsLoading(true); - if (!wallet.current) { - triggerHapticFeedback(HapticFeedbackTypes.NotificationError); - presentAlert({ message: loc.wallets.no_ln_wallet_error }); - return goBack(); - } - - // decoding the lnurl - const url = Lnurl.getUrlFromLnurl(data); - const { query } = parse(url, true); - - if (query.tag === Lnurl.TAG_LOGIN_REQUEST) { - navigate('LnurlAuth', { - lnurl: data, - walletID: walletID ?? wallet.current.getID(), - }); - return; - } - - // calling the url - try { - const resp = await fetch(url, { method: 'GET' }); - if (resp.status >= 300) { - throw new Error('Bad response from server'); - } - const reply = await resp.json(); - if (reply.status === 'ERROR') { - throw new Error('Reply from server: ' + reply.reason); - } - - if (reply.tag === Lnurl.TAG_PAY_REQUEST) { - // we are here by mistake. user wants to SEND to lnurl-pay, but he is on a screen that creates - // invoices (including through lnurl-withdraw) - navigate('ScanLndInvoiceRoot', { - screen: 'LnurlPay', - params: { - lnurl: data, - walletID: walletID ?? wallet.current.getID(), - }, - }); - return; - } - - if (reply.tag !== Lnurl.TAG_WITHDRAW_REQUEST) { - throw new Error('Unsupported lnurl'); - } - - // amount that comes from lnurl is always in sats - let newAmount = (reply.maxWithdrawable / 1000).toString(); - const sats = newAmount; - switch (unit) { - case BitcoinUnit.SATS: - // nop - break; - case BitcoinUnit.BTC: - newAmount = satoshiToBTC(newAmount); - break; - case BitcoinUnit.LOCAL_CURRENCY: - newAmount = formatBalancePlain(newAmount, BitcoinUnit.LOCAL_CURRENCY); - AmountInput.setCachedSatoshis(newAmount, sats); - break; - } - - // setting the invoice creating screen with the parameters - setLNURLParams({ - k1: reply.k1, - callback: reply.callback, - fixed: reply.minWithdrawable === reply.maxWithdrawable, - min: (reply.minWithdrawable || 0) / 1000, - max: reply.maxWithdrawable / 1000, - }); - setAmount(newAmount); - setDescription(reply.defaultDescription); - setIsLoading(false); - } catch (Err) { - Keyboard.dismiss(); - setIsLoading(false); - triggerHapticFeedback(HapticFeedbackTypes.NotificationError); - presentAlert({ message: Err.message }); - } - }; - const renderCreateButton = () => { return ( @@ -320,7 +330,9 @@ const LNDCreateInvoice = () => { }; const navigateToScanQRCode = () => { - scanQrHelper(name, true, processLnurl); + navigate('ScanQRCode', { + showFileImportButton: true, + }); Keyboard.dismiss(); }; diff --git a/screen/lnd/scanLndInvoice.js b/screen/lnd/scanLndInvoice.js index 02f2da8e5..3a9557fd7 100644 --- a/screen/lnd/scanLndInvoice.js +++ b/screen/lnd/scanLndInvoice.js @@ -18,13 +18,14 @@ import loc, { formatBalanceWithoutSuffix } from '../../loc'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; import { useStorage } from '../../hooks/context/useStorage'; import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory'; +import DeeplinkSchemaMatch from '../../class/deeplink-schema-match'; const ScanLndInvoice = () => { const { wallets, fetchAndSaveWalletTransactions } = useStorage(); const { isBiometricUseCapableAndEnabled } = useBiometrics(); const { colors } = useTheme(); + const route = useRoute(); const { walletID, uri, invoice } = useRoute().params; - const name = useRoute().name; /** @type {LightningCustodianWallet} */ const [wallet, setWallet] = useState( wallets.find(item => item.getID() === walletID) || wallets.find(item => item.chain === Chain.OFFCHAIN), @@ -281,6 +282,25 @@ const ScanLndInvoice = () => { pop(); }; + const onBarScanned = useCallback( + value => { + if (!value) return; + DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => { + triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); + navigate(...completionValue); + }); + }, + [navigate], + ); + + useEffect(() => { + const data = route.params?.onBarScanned; + if (data) { + onBarScanned(data); + setParams({ onBarScanned: undefined }); + } + }, [navigate, onBarScanned, route.params?.onBarScanned, setParams]); + if (wallet === undefined || !wallet) { return ( @@ -323,7 +343,6 @@ const ScanLndInvoice = () => { isLoading={isLoading} placeholder={loc.lnd.placeholder} inputAccessoryViewID={DismissKeyboardInputAccessoryViewID} - launchedBy={name} onBlur={onBlur} keyboardType="email-address" style={styles.addressInput} diff --git a/screen/send/Broadcast.tsx b/screen/send/Broadcast.tsx index d01fab2bd..2bda94bf2 100644 --- a/screen/send/Broadcast.tsx +++ b/screen/send/Broadcast.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { useRoute, RouteProp } from '@react-navigation/native'; import * as bitcoin from 'bitcoinjs-lib'; import { ActivityIndicator, Keyboard, Linking, StyleSheet, TextInput, View } from 'react-native'; @@ -19,11 +19,12 @@ import presentAlert from '../../components/Alert'; import Button from '../../components/Button'; import SafeArea from '../../components/SafeArea'; import { useTheme } from '../../components/themes'; -import { scanQrHelper } from '../../helpers/scan-qr'; import loc from '../../loc'; import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList'; import { useSettings } from '../../hooks/context/useSettings'; import { majorTomToGroundControl } from '../../blue_modules/notifications'; +import { navigate } from '../../NavigationService'; +import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; const BROADCAST_RESULT = Object.freeze({ none: 'Input transaction hex', @@ -35,12 +36,13 @@ const BROADCAST_RESULT = Object.freeze({ type RouteProps = RouteProp; const Broadcast: React.FC = () => { - const { name, params } = useRoute(); + const { params } = useRoute(); const [tx, setTx] = useState(); const [txHex, setTxHex] = useState(); const { colors } = useTheme(); const [broadcastResult, setBroadcastResult] = useState(BROADCAST_RESULT.none); const { selectedBlockExplorer } = useSettings(); + const { setParams } = useExtendedNavigation(); const stylesHooks = StyleSheet.create({ input: { @@ -50,13 +52,26 @@ const Broadcast: React.FC = () => { }, }); + const handleScannedData = useCallback((scannedData: string) => { + if (scannedData.indexOf('+') === -1 && scannedData.indexOf('=') === -1 && scannedData.indexOf('=') === -1) { + // this looks like NOT base64, so maybe its transaction's hex + return handleUpdateTxHex(scannedData); + } + + try { + // should be base64 encoded PSBT + const validTx = bitcoin.Psbt.fromBase64(scannedData).extractTransaction(); + return handleUpdateTxHex(validTx.toHex()); + } catch (e) {} + }, []); + useEffect(() => { - const scannedData = params?.scannedData; + const scannedData = params?.onBarScanned; if (scannedData) { handleScannedData(scannedData); + setParams({ onBarScanned: undefined }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [params?.scannedData]); + }, [handleScannedData, params?.onBarScanned, setParams]); const handleUpdateTxHex = (nextValue: string) => setTxHex(nextValue.trim()); @@ -88,21 +103,8 @@ const Broadcast: React.FC = () => { } }; - const handleScannedData = (scannedData: string) => { - if (scannedData.indexOf('+') === -1 && scannedData.indexOf('=') === -1 && scannedData.indexOf('=') === -1) { - // this looks like NOT base64, so maybe its transaction's hex - return handleUpdateTxHex(scannedData); - } - - try { - // should be base64 encoded PSBT - const validTx = bitcoin.Psbt.fromBase64(scannedData).extractTransaction(); - return handleUpdateTxHex(validTx.toHex()); - } catch (e) {} - }; - const handleQRScan = () => { - scanQrHelper(name, true, undefined, false); + navigate('ScanQRCode'); }; let status; diff --git a/screen/send/ScanQRCode.js b/screen/send/ScanQRCode.js index be76dd7e0..239c11cee 100644 --- a/screen/send/ScanQRCode.js +++ b/screen/send/ScanQRCode.js @@ -89,7 +89,11 @@ const ScanQRCode = () => { const { setIsDrawerShouldHide } = useSettings(); const navigation = useNavigation(); const route = useRoute(); - const { launchedBy, onBarScanned, onDismiss, showFileImportButton } = route.params; + const navigationState = navigation.getState(); + const previousRoute = navigationState.routes[navigationState.routes.length - 2]; + const defaultLaunchedBy = previousRoute ? previousRoute.name : undefined; + + const { launchedBy = defaultLaunchedBy, onBarScanned, onDismiss, showFileImportButton } = route.params || {}; const scannedCache = {}; const { colors } = useTheme(); const isFocused = useIsFocused(); @@ -139,13 +143,11 @@ const ScanQRCode = () => { const data = decoder.toString(); decoder = false; // nullify for future use (?) if (launchedBy) { - let merge = true; - if (typeof onBarScanned !== 'function') { - merge = false; - } - navigation.navigate({ name: launchedBy, params: { scannedData: data }, merge }); + const merge = true; + navigation.navigate({ name: launchedBy, params: { onBarScanned: data }, merge }); + } else { + onBarScanned && onBarScanned({ data }); } - onBarScanned && onBarScanned({ data }); } else { setUrTotal(100); setUrHave(Math.floor(decoder.estimatedPercentComplete() * 100)); @@ -192,13 +194,11 @@ const ScanQRCode = () => { data = Buffer.from(payload, 'hex').toString(); } if (launchedBy) { - let merge = true; - if (typeof onBarScanned !== 'function') { - merge = false; - } - navigation.navigate({ name: launchedBy, params: { scannedData: data }, merge }); + const merge = true; + navigation.navigate({ name: launchedBy, params: { onBarScanned: data }, merge }); + } else { + onBarScanned && onBarScanned({ data }); } - onBarScanned && onBarScanned({ data }); } else { setAnimatedQRCodeData(animatedQRCodeData); } @@ -259,13 +259,12 @@ const ScanQRCode = () => { bitcoin.Psbt.fromHex(hex); // if it doesnt throw - all good const data = Buffer.from(hex, 'hex').toString('base64'); if (launchedBy) { - let merge = true; - if (typeof onBarScanned !== 'function') { - merge = false; - } - navigation.navigate({ name: launchedBy, params: { scannedData: data }, merge }); + const merge = true; + + navigation.navigate({ name: launchedBy, params: { onBarScanned: data }, merge }); + } else { + onBarScanned && onBarScanned({ data }); } - onBarScanned && onBarScanned({ data }); return; } catch (_) {} @@ -273,13 +272,12 @@ const ScanQRCode = () => { setIsLoading(true); try { if (launchedBy) { - let merge = true; - if (typeof onBarScanned !== 'function') { - merge = false; - } - navigation.navigate({ name: launchedBy, params: { scannedData: ret.data }, merge }); + const merge = true; + + navigation.navigate({ name: launchedBy, params: { onBarScanned: ret.data }, merge }); + } else { + onBarScanned && onBarScanned(ret.data); } - onBarScanned && onBarScanned(ret.data); } catch (e) { console.log(e); } diff --git a/screen/send/SendDetails.tsx b/screen/send/SendDetails.tsx index fea1ba0e7..eeba03657 100644 --- a/screen/send/SendDetails.tsx +++ b/screen/send/SendDetails.tsx @@ -1,5 +1,5 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; -import { RouteProp, StackActions, useFocusEffect, useRoute } from '@react-navigation/native'; +import { RouteProp, useFocusEffect, useRoute } from '@react-navigation/native'; import BigNumber from 'bignumber.js'; import * as bitcoin from 'bitcoinjs-lib'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -39,7 +39,6 @@ import Button from '../../components/Button'; import CoinsSelected from '../../components/CoinsSelected'; import InputAccessoryAllFunds, { InputAccessoryAllFundsAccessoryViewID } from '../../components/InputAccessoryAllFunds'; import { useTheme } from '../../components/themes'; -import { scanQrHelper } from '../../helpers/scan-qr'; import loc, { formatBalance, formatBalanceWithoutSuffix } from '../../loc'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; import NetworkTransactionFees, { NetworkTransactionFee } from '../../models/networkTransactionFees'; @@ -56,7 +55,7 @@ import { useKeyboard } from '../../hooks/useKeyboard'; import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory'; import ActionSheet from '../ActionSheet'; import HeaderMenuButton from '../../components/HeaderMenuButton'; -import { CommonToolTipActions } from '../../typings/CommonToolTipActions'; +import { CommonToolTipActions, ToolTipAction } from '../../typings/CommonToolTipActions'; import { Action } from '../../components/types'; interface IPaymentDestinations { @@ -79,6 +78,7 @@ type RouteProps = RouteProp; const SendDetails = () => { const { wallets, setSelectedWalletID, sleep, txMetadata, saveToDisk } = useStorage(); const navigation = useExtendedNavigation(); + const selectedDataProcessor = useRef(); const setParams = navigation.setParams; const route = useRoute(); const name = route.name; @@ -93,7 +93,6 @@ const SendDetails = () => { const scrollView = useRef>(null); const scrollIndex = useRef(0); const { colors } = useTheme(); - const popAction = StackActions.pop(1); // state const [width, setWidth] = useState(Dimensions.get('window').width); @@ -665,34 +664,35 @@ const SendDetails = () => { return presentAlert({ title: loc.errors.error, message: 'Importing transaction in non-watchonly wallet (this should never happen)' }); } - const data = await scanQrHelper(route.name, true); - importQrTransactionOnBarScanned(data); + navigateToQRCodeScanner(); }; - const importQrTransactionOnBarScanned = (ret: any) => { - navigation.getParent()?.getParent()?.dispatch(popAction); - if (!wallet) return; - if (!ret.data) ret = { data: ret }; - if (ret.data.toUpperCase().startsWith('UR')) { - presentAlert({ title: loc.errors.error, message: 'BC-UR not decoded. This should never happen' }); - } else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) { - // this looks like NOT base64, so maybe its transaction's hex - // we dont support it in this flow - } else { - // psbt base64? + const importQrTransactionOnBarScanned = useCallback( + (ret: any) => { + if (!wallet) return; + if (!ret.data) ret = { data: ret }; + if (ret.data.toUpperCase().startsWith('UR')) { + presentAlert({ title: loc.errors.error, message: 'BC-UR not decoded. This should never happen' }); + } else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) { + // this looks like NOT base64, so maybe its transaction's hex + // we dont support it in this flow + } else { + // psbt base64? - // we construct PSBT object and pass to next screen - // so user can do smth with it: - const psbt = bitcoin.Psbt.fromBase64(ret.data); + // we construct PSBT object and pass to next screen + // so user can do smth with it: + const psbt = bitcoin.Psbt.fromBase64(ret.data); - navigation.navigate('PsbtWithHardwareWallet', { - memo: transactionMemo, - walletID: wallet.getID(), - psbt, - }); - setIsLoading(false); - } - }; + navigation.navigate('PsbtWithHardwareWallet', { + memo: transactionMemo, + walletID: wallet.getID(), + psbt, + }); + setIsLoading(false); + } + }, + [navigation, transactionMemo, wallet], + ); /** * watch-only wallets with enabled HW wallet support have different flow. we have to show PSBT to user as QR code @@ -776,53 +776,126 @@ const SendDetails = () => { }); }; - const _importTransactionMultisig = async (base64arg: string | false) => { - try { - const base64 = base64arg || (await fs.openSignedTransaction()); - if (!base64) return; - const psbt = bitcoin.Psbt.fromBase64(base64); // if it doesnt throw - all good, its valid + const _importTransactionMultisig = useCallback( + async (base64arg: string | false) => { + try { + const base64 = base64arg || (await fs.openSignedTransaction()); + if (!base64) return; + const psbt = bitcoin.Psbt.fromBase64(base64); // if it doesnt throw - all good, its valid - if ((wallet as MultisigHDWallet)?.howManySignaturesCanWeMake() > 0 && (await askCosignThisTransaction())) { - setIsLoading(true); - await sleep(100); - (wallet as MultisigHDWallet).cosignPsbt(psbt); - setIsLoading(false); - await sleep(100); - } + if ((wallet as MultisigHDWallet)?.howManySignaturesCanWeMake() > 0 && (await askCosignThisTransaction())) { + setIsLoading(true); + await sleep(100); + (wallet as MultisigHDWallet).cosignPsbt(psbt); + setIsLoading(false); + await sleep(100); + } - if (wallet) { - navigation.navigate('PsbtMultisig', { - memo: transactionMemo, - psbtBase64: psbt.toBase64(), - walletID: wallet.getID(), - }); + if (wallet) { + navigation.navigate('PsbtMultisig', { + memo: transactionMemo, + psbtBase64: psbt.toBase64(), + walletID: wallet.getID(), + }); + } + } catch (error: any) { + presentAlert({ title: loc.send.problem_with_psbt, message: error.message }); } - } catch (error: any) { - presentAlert({ title: loc.send.problem_with_psbt, message: error.message }); - } - setIsLoading(false); - }; + setIsLoading(false); + }, + [navigation, sleep, transactionMemo, wallet], + ); const importTransactionMultisig = () => { return _importTransactionMultisig(false); }; - const onBarScanned = (ret: any) => { - if (!ret.data) ret = { data: ret }; - if (ret.data.toUpperCase().startsWith('UR')) { - presentAlert({ title: loc.errors.error, message: 'BC-UR not decoded. This should never happen' }); - } else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) { - // this looks like NOT base64, so maybe its transaction's hex - // we dont support it in this flow - } else { - // psbt base64? - return _importTransactionMultisig(ret.data); - } - }; + const onBarScanned = useCallback( + (ret: any) => { + if (!ret.data) ret = { data: ret }; + if (ret.data.toUpperCase().startsWith('UR')) { + presentAlert({ title: loc.errors.error, message: 'BC-UR not decoded. This should never happen' }); + } else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) { + // this looks like NOT base64, so maybe its transaction's hex + // we dont support it in this flow + } else { + // psbt base64? + return _importTransactionMultisig(ret.data); + } + }, + [_importTransactionMultisig], + ); - const importTransactionMultisigScanQr = async () => { - const data = await scanQrHelper(route.name, true); - onBarScanned(data); + const handlePsbtSign = useCallback( + async (psbtBase64: string) => { + let tx; + let psbt; + try { + psbt = bitcoin.Psbt.fromBase64(psbtBase64); + tx = (wallet as MultisigHDWallet).cosignPsbt(psbt).tx; + } catch (e: any) { + presentAlert({ title: loc.errors.error, message: e.message }); + return; + } finally { + setIsLoading(false); + } + + if (!tx || !wallet) return setIsLoading(false); + + // we need to remove change address from recipients, so that Confirm screen show more accurate info + const changeAddresses: string[] = []; + // @ts-ignore hacky + for (let c = 0; c < wallet.next_free_change_address_index + wallet.gap_limit; c++) { + // @ts-ignore hacky + changeAddresses.push(wallet._getInternalAddressByIndex(c)); + } + const recipients = psbt.txOutputs.filter(({ address }) => !changeAddresses.includes(String(address))); + + navigation.navigate('CreateTransaction', { + fee: new BigNumber(psbt.getFee()).dividedBy(100000000).toNumber(), + feeSatoshi: psbt.getFee(), + wallet, + tx: tx.toHex(), + recipients, + satoshiPerByte: psbt.getFeeRate(), + showAnimatedQr: true, + psbt, + }); + }, + [navigation, wallet], + ); + + useEffect(() => { + const data = routeParams.onBarScanned; + if (data) { + if (selectedDataProcessor.current) { + if ( + selectedDataProcessor.current === CommonToolTipActions.ImportTransactionQR || + selectedDataProcessor.current === CommonToolTipActions.CoSignTransaction || + selectedDataProcessor.current === CommonToolTipActions.SignPSBT + ) { + if (selectedDataProcessor.current === CommonToolTipActions.ImportTransactionQR) { + importQrTransactionOnBarScanned(data); + } else if ( + selectedDataProcessor.current === CommonToolTipActions.CoSignTransaction || + selectedDataProcessor.current === CommonToolTipActions.SignPSBT + ) { + handlePsbtSign(data); + } else { + onBarScanned(data); + } + } else { + console.log('Unknown selectedDataProcessor:', selectedDataProcessor.current); + } + } + setParams({ onBarScanned: undefined }); + } + }, [handlePsbtSign, importQrTransactionOnBarScanned, onBarScanned, routeParams.onBarScanned, setParams]); + + const navigateToQRCodeScanner = () => { + navigation.navigate('ScanQRCode', { + showFileImportButton: true, + }); }; const handleAddRecipient = () => { @@ -891,48 +964,6 @@ const SendDetails = () => { navigation.navigate('PaymentCodeList', { walletID: wallet.getID() }); }; - const handlePsbtSign = async () => { - setIsLoading(true); - await new Promise(resolve => setTimeout(resolve, 100)); // sleep for animations - - const scannedData = await scanQrHelper(name, true, undefined); - if (!scannedData) return setIsLoading(false); - - let tx; - let psbt; - try { - psbt = bitcoin.Psbt.fromBase64(scannedData); - tx = (wallet as MultisigHDWallet).cosignPsbt(psbt).tx; - } catch (e: any) { - presentAlert({ title: loc.errors.error, message: e.message }); - return; - } finally { - setIsLoading(false); - } - - if (!tx || !wallet) return setIsLoading(false); - - // we need to remove change address from recipients, so that Confirm screen show more accurate info - const changeAddresses: string[] = []; - // @ts-ignore hacky - for (let c = 0; c < wallet.next_free_change_address_index + wallet.gap_limit; c++) { - // @ts-ignore hacky - changeAddresses.push(wallet._getInternalAddressByIndex(c)); - } - const recipients = psbt.txOutputs.filter(({ address }) => !changeAddresses.includes(String(address))); - - navigation.navigate('CreateTransaction', { - fee: new BigNumber(psbt.getFee()).dividedBy(100000000).toNumber(), - feeSatoshi: psbt.getFee(), - wallet, - tx: tx.toHex(), - recipients, - satoshiPerByte: psbt.getFeeRate(), - showAnimatedQr: true, - psbt, - }); - }; - // Header Right Button const headerRightOnPress = (id: string) => { @@ -941,7 +972,8 @@ const SendDetails = () => { } else if (id === CommonToolTipActions.RemoveRecipient.id) { handleRemoveRecipient(); } else if (id === CommonToolTipActions.SignPSBT.id) { - handlePsbtSign(); + selectedDataProcessor.current = CommonToolTipActions.SignPSBT; + navigateToQRCodeScanner(); } else if (id === CommonToolTipActions.SendMax.id) { onUseAllPressed(); } else if (id === CommonToolTipActions.AllowRBF.id) { @@ -949,11 +981,13 @@ const SendDetails = () => { } else if (id === CommonToolTipActions.ImportTransaction.id) { importTransaction(); } else if (id === CommonToolTipActions.ImportTransactionQR.id) { + selectedDataProcessor.current = CommonToolTipActions.ImportTransactionQR; importQrTransaction(); } else if (id === CommonToolTipActions.ImportTransactionMultsig.id) { importTransactionMultisig(); } else if (id === CommonToolTipActions.CoSignTransaction.id) { - importTransactionMultisigScanQr(); + selectedDataProcessor.current = CommonToolTipActions.CoSignTransaction; + navigateToQRCodeScanner(); } else if (id === CommonToolTipActions.CoinControl.id) { handleCoinControl(); } else if (id === CommonToolTipActions.InsertContact.id) { diff --git a/screen/send/psbtMultisigQRCode.js b/screen/send/psbtMultisigQRCode.js index 3377b49e2..5f7b6af79 100644 --- a/screen/send/psbtMultisigQRCode.js +++ b/screen/send/psbtMultisigQRCode.js @@ -1,6 +1,6 @@ import { useIsFocused, useNavigation, useRoute } from '@react-navigation/native'; import * as bitcoin from 'bitcoinjs-lib'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { ActivityIndicator, ScrollView, StyleSheet, View } from 'react-native'; import { BlueSpacing20 } from '../../BlueComponents'; @@ -10,15 +10,14 @@ import SafeArea from '../../components/SafeArea'; import SaveFileButton from '../../components/SaveFileButton'; import { SquareButton } from '../../components/SquareButton'; import { useTheme } from '../../components/themes'; -import { scanQrHelper } from '../../helpers/scan-qr'; import loc from '../../loc'; const PsbtMultisigQRCode = () => { const { navigate } = useNavigation(); const { colors } = useTheme(); const openScannerButton = useRef(); - const { psbtBase64, isShowOpenScanner } = useRoute().params; - const { name } = useRoute(); + const { params } = useRoute(); + const { psbtBase64, isShowOpenScanner } = params; const [isLoading, setIsLoading] = useState(false); const dynamicQRCode = useRef(); const isFocused = useIsFocused(); @@ -45,23 +44,34 @@ const PsbtMultisigQRCode = () => { } }, [isFocused]); - const onBarScanned = ret => { - if (!ret.data) ret = { data: ret }; - if (ret.data.toUpperCase().startsWith('UR')) { - presentAlert({ message: 'BC-UR not decoded. This should never happen' }); - } else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) { - // this looks like NOT base64, so maybe its transaction's hex - // we dont support it in this flow - presentAlert({ message: loc.wallets.import_error }); - } else { - // psbt base64? - navigate({ name: 'PsbtMultisig', params: { receivedPSBTBase64: ret.data }, merge: true }); - } - }; + const onBarScanned = useCallback( + ret => { + if (!ret.data) ret = { data: ret }; + if (ret.data.toUpperCase().startsWith('UR')) { + presentAlert({ message: 'BC-UR not decoded. This should never happen' }); + } else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) { + // this looks like NOT base64, so maybe its transaction's hex + // we dont support it in this flow + presentAlert({ message: loc.wallets.import_error }); + } else { + // psbt base64? + navigate({ name: 'PsbtMultisig', params: { receivedPSBTBase64: ret.data }, merge: true }); + } + }, + [navigate], + ); - const openScanner = async () => { - const scanned = await scanQrHelper(name, true); - onBarScanned({ data: scanned }); + useEffect(() => { + const data = params.onBarScanned; + if (data) { + onBarScanned({ data }); + } + }, [onBarScanned, params.onBarScanned]); + + const openScanner = () => { + navigate('ScanQRCode', { + showFileImportButton: true, + }); }; const saveFileButtonBeforeOnPress = () => { diff --git a/screen/send/psbtWithHardwareWallet.js b/screen/send/psbtWithHardwareWallet.js index 88b080ceb..27782ae68 100644 --- a/screen/send/psbtWithHardwareWallet.js +++ b/screen/send/psbtWithHardwareWallet.js @@ -1,7 +1,7 @@ import Clipboard from '@react-native-clipboard/clipboard'; import { useIsFocused, useRoute } from '@react-navigation/native'; import * as bitcoin from 'bitcoinjs-lib'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { ActivityIndicator, Linking, Platform, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'; import DocumentPicker from 'react-native-document-picker'; import RNFS from 'react-native-fs'; @@ -14,7 +14,6 @@ import { DynamicQRCode } from '../../components/DynamicQRCode'; import SaveFileButton from '../../components/SaveFileButton'; import { SecondButton } from '../../components/SecondButton'; import { useTheme } from '../../components/themes'; -import { scanQrHelper } from '../../helpers/scan-qr'; import { useBiometrics, unlockWithBiometrics } from '../../hooks/useBiometrics'; import loc from '../../loc'; import { useStorage } from '../../hooks/context/useStorage'; @@ -62,34 +61,40 @@ const PsbtWithHardwareWallet = () => { }, }); - const _combinePSBT = receivedPSBT => { - return wallet.combinePsbt(psbt, receivedPSBT); - }; + const _combinePSBT = useCallback( + receivedPSBT => { + return wallet.combinePsbt(psbt, receivedPSBT); + }, + [psbt, wallet], + ); - const onBarScanned = ret => { - if (ret && !ret.data) ret = { data: ret }; - if (ret.data.toUpperCase().startsWith('UR')) { - presentAlert({ message: 'BC-UR not decoded. This should never happen' }); - } - if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) { - // this looks like NOT base64, so maybe its transaction's hex - setTxHex(ret.data); - return; - } - try { - const Tx = _combinePSBT(ret.data); - setTxHex(Tx.toHex()); - if (launchedBy) { - // we must navigate back to the screen who requested psbt (instead of broadcasting it ourselves) - // most likely for LN channel opening - navigation.navigate({ name: launchedBy, params: { psbt }, merge: true }); - // ^^^ we just use `psbt` variable sinse it was finalized in the above _combinePSBT() - // (passed by reference) + const onBarScanned = useCallback( + ret => { + if (ret && !ret.data) ret = { data: ret }; + if (ret.data.toUpperCase().startsWith('UR')) { + presentAlert({ message: 'BC-UR not decoded. This should never happen' }); } - } catch (Err) { - presentAlert({ message: Err.message }); - } - }; + if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) { + // this looks like NOT base64, so maybe its transaction's hex + setTxHex(ret.data); + return; + } + try { + const Tx = _combinePSBT(ret.data); + setTxHex(Tx.toHex()); + if (launchedBy) { + // we must navigate back to the screen who requested psbt (instead of broadcasting it ourselves) + // most likely for LN channel opening + navigation.navigate({ name: launchedBy, params: { psbt }, merge: true }); + // ^^^ we just use `psbt` variable sinse it was finalized in the above _combinePSBT() + // (passed by reference) + } + } catch (Err) { + presentAlert({ message: Err.message }); + } + }, + [_combinePSBT, launchedBy, navigation, psbt], + ); useEffect(() => { if (isFocused) { @@ -217,11 +222,18 @@ const PsbtWithHardwareWallet = () => { } }; - const openScanner = async () => { - const data = await scanQrHelper(route.name, true); + useEffect(() => { + const data = route.params.onBarScanned; if (data) { onBarScanned(data); + navigation.setParams({ onBarScanned: undefined }); } + }, [navigation, onBarScanned, route.params.onBarScanned]); + + const openScanner = async () => { + navigation.navigate('ScanQRCode', { + showFileImportButton: true, + }); }; if (txHex) return _renderBroadcastHex(); diff --git a/screen/settings/ElectrumSettings.tsx b/screen/settings/ElectrumSettings.tsx index 9314fc34c..b4a99ac47 100644 --- a/screen/settings/ElectrumSettings.tsx +++ b/screen/settings/ElectrumSettings.tsx @@ -6,7 +6,6 @@ import { BlueCard, BlueSpacing10, BlueSpacing20, BlueText } from '../../BlueComp import DeeplinkSchemaMatch from '../../class/deeplink-schema-match'; import presentAlert from '../../components/Alert'; import Button from '../../components/Button'; -import { scanQrHelper } from '../../helpers/scan-qr'; import loc from '../../loc'; import { DoneAndDismissKeyboardInputAccessory, @@ -43,8 +42,9 @@ const PREFERRED_SERVER_ROW = 'preferredserverrow'; const ElectrumSettings: React.FC = () => { const { colors } = useTheme(); - const { server } = useRoute().params; - const { setOptions } = useExtendedNavigation(); + const params = useRoute().params; + const { server } = params; + const navigation = useExtendedNavigation(); const [isLoading, setIsLoading] = useState(true); const [serverHistory, setServerHistory] = useState([]); const [config, setConfig] = useState<{ connected?: number; host?: string; port?: string }>({}); @@ -378,10 +378,10 @@ const ElectrumSettings: React.FC = () => { ); useEffect(() => { - setOptions({ + navigation.setOptions({ headerRight: isElectrumDisabled ? null : () => HeaderRight, }); - }, [HeaderRight, isElectrumDisabled, setOptions]); + }, [HeaderRight, isElectrumDisabled, navigation]); const checkServer = async () => { setIsLoading(true); @@ -413,12 +413,17 @@ const ElectrumSettings: React.FC = () => { }; const importScan = async () => { - const scanned = await scanQrHelper('ElectrumSettings', true); - if (scanned) { - onBarScanned(scanned); - } + navigation.navigate('ScanQRCode'); }; + useEffect(() => { + const data = params.onBarScanned; + if (data) { + onBarScanned(data); + navigation.setParams({ onBarScanned: undefined }); + } + }, [navigation, params.onBarScanned]); + const onSSLPortChange = (value: boolean) => { Keyboard.dismiss(); if (value) { diff --git a/screen/settings/IsItMyAddress.tsx b/screen/settings/IsItMyAddress.tsx index f49c56a22..748c2e5dc 100644 --- a/screen/settings/IsItMyAddress.tsx +++ b/screen/settings/IsItMyAddress.tsx @@ -4,7 +4,6 @@ import { Keyboard, StyleSheet, TextInput, View, ScrollView, TouchableOpacity, Te import { BlueButtonLink, BlueCard, BlueSpacing10, BlueSpacing20, BlueSpacing40, BlueText } from '../../BlueComponents'; import Button from '../../components/Button'; import { useTheme } from '../../components/themes'; -import { scanQrHelper } from '../../helpers/scan-qr'; import loc from '../../loc'; import { useStorage } from '../../hooks/context/useStorage'; import { TWallet } from '../../class/wallets/types'; @@ -15,6 +14,7 @@ import Icon from 'react-native-vector-icons/MaterialIcons'; import { Divider } from '@rneui/themed'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import presentAlert from '../../components/Alert'; +import { navigate } from '../../NavigationService'; type RouteProps = RouteProp; type NavigationProp = NativeStackNavigationProp; @@ -109,11 +109,16 @@ const IsItMyAddress: React.FC = () => { }; const importScan = async () => { - const data = await scanQrHelper(route.name, true, undefined, true); + navigate('ScanQRCode'); + }; + + useEffect(() => { + const data = route.params?.onBarScanned; if (data) { onBarScanned(data); + navigation.setParams({ onBarScanned: undefined }); } - }; + }, [navigation, route.name, route.params?.onBarScanned]); const viewQRCode = () => { if (!resultCleanAddress) return; diff --git a/screen/settings/LightningSettings.tsx b/screen/settings/LightningSettings.tsx index 44416e6d8..aeb30a8cc 100644 --- a/screen/settings/LightningSettings.tsx +++ b/screen/settings/LightningSettings.tsx @@ -9,11 +9,12 @@ import { LightningCustodianWallet } from '../../class/wallets/lightning-custodia import presentAlert, { AlertType } from '../../components/Alert'; import { Button } from '../../components/Button'; import { useTheme } from '../../components/themes'; -import { scanQrHelper } from '../../helpers/scan-qr'; import loc from '../../loc'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import { GROUP_IO_BLUEWALLET } from '../../blue_modules/currency'; import { clearLNDHub, getLNDHub, setLNDHub } from '../../helpers/lndHub'; +import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList'; +import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; const styles = StyleSheet.create({ uri: { @@ -38,21 +39,14 @@ const styles = StyleSheet.create({ }, }); -type LightingSettingsRouteProps = RouteProp< - { - params?: { - url?: string; - }; - }, - 'params' ->; +type LightingSettingsRouteProps = RouteProp; const LightningSettings: React.FC = () => { const params = useRoute().params; const [isLoading, setIsLoading] = useState(true); const [URI, setURI] = useState(); const { colors } = useTheme(); - const route = useRoute(); + const { navigate, setParams } = useExtendedNavigation(); const styleHook = StyleSheet.create({ uri: { borderColor: colors.formBorder, @@ -131,13 +125,19 @@ const LightningSettings: React.FC = () => { }, [URI]); const importScan = () => { - scanQrHelper(route.name).then(data => { - if (data) { - setLndhubURI(data); - } + navigate('ScanQRCode', { + showFileImportButton: true, }); }; + useEffect(() => { + const data = params?.onBarScanned; + if (data) { + setLndhubURI(data); + setParams({ onBarScanned: undefined }); + } + }, [params?.onBarScanned, setParams]); + return ( diff --git a/screen/wallets/ImportWallet.tsx b/screen/wallets/ImportWallet.tsx index 3102a09d1..2a5ea1ddf 100644 --- a/screen/wallets/ImportWallet.tsx +++ b/screen/wallets/ImportWallet.tsx @@ -11,7 +11,6 @@ import { } from '../../components/DoneAndDismissKeyboardInputAccessory'; import HeaderMenuButton from '../../components/HeaderMenuButton'; import { useTheme } from '../../components/themes'; -import { scanQrHelper } from '../../helpers/scan-qr'; import { useSettings } from '../../hooks/context/useSettings'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import { useKeyboard } from '../../hooks/useKeyboard'; @@ -30,7 +29,6 @@ const ImportWallet = () => { const route = useRoute(); const label = route?.params?.label ?? ''; const triggerImport = route?.params?.triggerImport ?? false; - const scannedData = route?.params?.scannedData ?? ''; const [importText, setImportText] = useState(label); const [isToolbarVisibleForAndroid, setIsToolbarVisibleForAndroid] = useState(false); const [, setSpeedBackdoor] = useState(0); @@ -108,11 +106,18 @@ const ImportWallet = () => { ); const importScan = useCallback(async () => { - const data = await scanQrHelper(route.name, true); + navigation.navigate('ScanQRCode', { + showFileImportButton: true, + }); + }, [navigation]); + + useEffect(() => { + const data = route.params?.onBarScanned; if (data) { onBarScanned(data); + navigation.setParams({ onBarScanned: undefined }); } - }, [route.name, onBarScanned]); + }, [route.name, onBarScanned, route.params?.onBarScanned, navigation]); const speedBackdoorTap = () => { setSpeedBackdoor(v => { @@ -162,12 +167,6 @@ const ImportWallet = () => { if (triggerImport) handleImport(); }, [triggerImport, handleImport]); - useEffect(() => { - if (scannedData) { - onBarScanned(scannedData); - } - }, [scannedData, onBarScanned]); - // Adding the ToolTipMenu to the header useEffect(() => { navigation.setOptions({ diff --git a/screen/wallets/ViewEditMultisigCosigners.tsx b/screen/wallets/ViewEditMultisigCosigners.tsx index 8fdc1f401..bbe221877 100644 --- a/screen/wallets/ViewEditMultisigCosigners.tsx +++ b/screen/wallets/ViewEditMultisigCosigners.tsx @@ -1,5 +1,5 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { useFocusEffect, useRoute } from '@react-navigation/native'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { CommonActions, RouteProp, useFocusEffect, useRoute } from '@react-navigation/native'; import { ActivityIndicator, Alert, @@ -38,7 +38,6 @@ import QRCodeComponent from '../../components/QRCodeComponent'; import SquareEnumeratedWords, { SquareEnumeratedWordsContentAlign } from '../../components/SquareEnumeratedWords'; import { useTheme } from '../../components/themes'; import prompt from '../../helpers/prompt'; -import { scanQrHelper } from '../../helpers/scan-qr'; import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import { disallowScreenshot } from 'react-native-screen-capture'; @@ -48,6 +47,12 @@ import { useStorage } from '../../hooks/context/useStorage'; import ToolTipMenu from '../../components/TooltipMenu'; import { CommonToolTipActions } from '../../typings/CommonToolTipActions'; import { useSettings } from '../../hooks/context/useSettings'; +import { ViewEditMultisigCosignersStackParamList } from '../../navigation/ViewEditMultisigCosignersStack'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { navigationRef } from '../../NavigationService'; + +type RouteParams = RouteProp; +type NavigationProp = NativeStackNavigationProp; const ViewEditMultisigCosigners: React.FC = () => { const hasLoaded = useRef(false); @@ -55,10 +60,10 @@ const ViewEditMultisigCosigners: React.FC = () => { const { wallets, setWalletsWithNewOrder } = useStorage(); const { isBiometricUseCapableAndEnabled } = useBiometrics(); const { isElectrumDisabled, isPrivacyBlurEnabled } = useSettings(); - const { navigate, dispatch, addListener } = useExtendedNavigation(); + const { navigate, dispatch, addListener, setParams } = useExtendedNavigation(); const openScannerButtonRef = useRef(); - const route = useRoute(); - const { walletID } = route.params as { walletID: string }; + const route = useRoute(); + const { walletID } = route.params; const w = useRef(wallets.find(wallet => wallet.getID() === walletID)); const tempWallet = useRef(new MultisigHDWallet()); const [wallet, setWallet] = useState(); @@ -183,7 +188,8 @@ const ViewEditMultisigCosigners: React.FC = () => { setIsSaveButtonDisabled(true); setTimeout(() => { setWalletsWithNewOrder(newWallets); - navigate('WalletsList'); + // dismiss this modal + navigationRef.dispatch(CommonActions.navigate({ name: 'WalletsList' })); }, 500); }; useFocusEffect( @@ -496,11 +502,18 @@ const ViewEditMultisigCosigners: React.FC = () => { const scanOrOpenFile = async () => { await provideMnemonicsModalRef.current?.dismiss(); - const scanned = await scanQrHelper(route.name, true, undefined); - setImportText(String(scanned)); - provideMnemonicsModalRef.current?.present(); + navigate('ScanQRCode', { showFileImportButton: true }); }; + useEffect(() => { + const scannedData = route.params.onBarScanned; + if (scannedData) { + setImportText(String(scannedData)); + setParams({ onBarScanned: undefined }); + provideMnemonicsModalRef.current?.present(); + } + }, [route.params.onBarScanned, setParams]); + const hideProvideMnemonicsModal = () => { Keyboard.dismiss(); provideMnemonicsModalRef.current?.dismiss(); diff --git a/screen/wallets/WalletTransactions.tsx b/screen/wallets/WalletTransactions.tsx index 1ee221b5a..f76aaff8f 100644 --- a/screen/wallets/WalletTransactions.tsx +++ b/screen/wallets/WalletTransactions.tsx @@ -1,4 +1,4 @@ -import { useFocusEffect, useRoute } from '@react-navigation/native'; +import { RouteProp, useFocusEffect, useRoute } from '@react-navigation/native'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, @@ -26,7 +26,6 @@ import { FButton, FContainer } from '../../components/FloatButtons'; import { useTheme } from '../../components/themes'; import { TransactionListItem } from '../../components/TransactionListItem'; import TransactionsNavigationHeader, { actionKeys } from '../../components/TransactionsNavigationHeader'; -import { scanQrHelper } from '../../helpers/scan-qr'; import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import loc from '../../loc'; @@ -53,14 +52,15 @@ const buttonFontSize = : PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26); type WalletTransactionsProps = NativeStackScreenProps; +type RouteProps = RouteProp; const WalletTransactions: React.FC = ({ route }) => { const { wallets, saveToDisk, setSelectedWalletID } = useStorage(); const { setReloadTransactionsMenuActionFunction } = useMenuElements(); const { isBiometricUseCapableAndEnabled } = useBiometrics(); const [isLoading, setIsLoading] = useState(false); - const { walletID } = route.params; - const { name } = useRoute(); + const { params, name } = useRoute(); + const { walletID } = params; const wallet = useMemo(() => wallets.find(w => w.getID() === walletID), [walletID, wallets]); const [limit, setLimit] = useState(15); const [pageSize] = useState(20); @@ -85,6 +85,33 @@ const WalletTransactions: React.FC = ({ route }) => { }, [route, setOptions]), ); + const onBarCodeRead = useCallback( + (ret?: { data?: any }) => { + if (!isLoading) { + setIsLoading(true); + const parameters = { + walletID, + uri: ret?.data ? ret.data : ret, + }; + if (wallet?.chain === Chain.ONCHAIN) { + navigate('SendDetailsRoot', { screen: 'SendDetails', params: parameters }); + } else { + navigate('ScanLndInvoiceRoot', { screen: 'ScanLndInvoice', params: parameters }); + } + setIsLoading(false); + } + }, + [isLoading, walletID, wallet?.chain, navigate], + ); + + useEffect(() => { + const data = route.params?.onBarScanned; + if (data) { + onBarCodeRead({ data }); + navigation.setParams({ onBarScanned: undefined }); + } + }, [navigation, onBarCodeRead, route.params]); + const getTransactions = useCallback( (lmt = Infinity): Transaction[] => { if (!wallet) return []; @@ -259,25 +286,6 @@ const WalletTransactions: React.FC = ({ route }) => { ); - const onBarCodeRead = useCallback( - (ret?: { data?: any }) => { - if (!isLoading) { - setIsLoading(true); - const params = { - walletID, - uri: ret?.data ? ret.data : ret, - }; - if (wallet?.chain === Chain.ONCHAIN) { - navigate('SendDetailsRoot', { screen: 'SendDetails', params }); - } else { - navigate('ScanLndInvoiceRoot', { screen: 'ScanLndInvoice', params }); - } - setIsLoading(false); - } - }, - [isLoading, walletID, wallet?.chain, navigate], - ); - const choosePhoto = () => { fs.showImagePickerAndReadImage() .then(data => { @@ -351,10 +359,9 @@ const WalletTransactions: React.FC = ({ route }) => { break; } case 2: { - const data = await scanQrHelper(name, true); - if (data) { - onBarCodeRead({ data }); - } + navigate('ScanQRCode', { + showImportFileButton: true, + }); break; } case 3: diff --git a/screen/wallets/WalletsList.tsx b/screen/wallets/WalletsList.tsx index 1750516e2..3e7c0832d 100644 --- a/screen/wallets/WalletsList.tsx +++ b/screen/wallets/WalletsList.tsx @@ -13,7 +13,6 @@ import { FButton, FContainer } from '../../components/FloatButtons'; import { useTheme } from '../../components/themes'; import { TransactionListItem } from '../../components/TransactionListItem'; import WalletsCarousel from '../../components/WalletsCarousel'; -import { scanQrHelper } from '../../helpers/scan-qr'; import { useIsLargeScreen } from '../../hooks/useIsLargeScreen'; import loc from '../../loc'; import ActionSheet from '../ActionSheet'; @@ -104,10 +103,9 @@ const WalletsList: React.FC = () => { const { isTotalBalanceEnabled, isElectrumDisabled } = useSettings(); const { width } = useWindowDimensions(); const { colors, scanImage } = useTheme(); - const { navigate } = useExtendedNavigation(); + const navigation = useExtendedNavigation(); const isFocused = useIsFocused(); const route = useRoute(); - const routeName = route.name; const dataSource = getTransactions(undefined, 10); const walletsCount = useRef(wallets.length); const walletActionButtonsRef = useRef(); @@ -179,13 +177,25 @@ const WalletsList: React.FC = () => { walletsCount.current = wallets.length; }, [wallets]); + const onBarScanned = useCallback( + (value: any) => { + if (!value) return; + DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => { + triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); + // @ts-ignore: for now + navigation.navigate(...completionValue); + }); + }, + [navigation], + ); + useEffect(() => { - const scannedData = route.params?.scannedData; - if (scannedData) { - onBarScanned(scannedData); + const data = route.params?.onBarScanned; + if (data) { + onBarScanned(data); + navigation.setParams({ onBarScanned: undefined }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [route.params?.scannedData]); + }, [navigation, onBarScanned, route.params?.onBarScanned]); useEffect(() => { refreshTransactions(false, true); @@ -196,15 +206,15 @@ const WalletsList: React.FC = () => { (item?: TWallet) => { if (item?.getID) { const walletID = item.getID(); - navigate('WalletTransactions', { + navigation.navigate('WalletTransactions', { walletID, walletType: item.type, }); } else { - navigate('AddWalletRoot'); + navigation.navigate('AddWalletRoot'); } }, - [navigate], + [navigation], ); const setIsLoading = useCallback((value: boolean) => { @@ -240,8 +250,8 @@ const WalletsList: React.FC = () => { }, [stylesHook.listHeaderBack, stylesHook.listHeaderText]); const handleLongPress = useCallback(() => { - navigate('ManageWallets'); - }, [navigate]); + navigation.navigate('ManageWallets'); + }, [navigation]); const renderTransactionListsRow = useCallback( (item: ExtendedTransaction) => ( @@ -349,20 +359,10 @@ const WalletsList: React.FC = () => { }; const onScanButtonPressed = useCallback(() => { - scanQrHelper(routeName, true, undefined, false); - }, [routeName]); - - const onBarScanned = useCallback( - (value: any) => { - if (!value) return; - DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => { - triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); - // @ts-ignore: for now - navigate(...completionValue); - }); - }, - [navigate], - ); + navigation.navigate('ScanQRCode', { + showFileImportButton: true, + }); + }, [navigation]); const pasteFromClipboard = useCallback(async () => { onBarScanned(await getClipboardContent()); @@ -397,7 +397,9 @@ const WalletsList: React.FC = () => { }); break; case 2: - scanQrHelper(routeName, true, undefined, false); + navigation.navigate('ScanQRCode', { + showFileImportButton: true, + }); break; case 3: if (!isClipboardEmpty) { @@ -406,7 +408,7 @@ const WalletsList: React.FC = () => { break; } }); - }, [pasteFromClipboard, onBarScanned, routeName]); + }, [onBarScanned, navigation, pasteFromClipboard]); const refreshProps = isDesktop || isElectrumDisabled ? {} : { refreshing: isLoading, onRefresh }; diff --git a/screen/wallets/addMultisigStep2.js b/screen/wallets/addMultisigStep2.js index 13d66e3e5..f38a4f420 100644 --- a/screen/wallets/addMultisigStep2.js +++ b/screen/wallets/addMultisigStep2.js @@ -32,7 +32,6 @@ import prompt from '../../helpers/prompt'; import { disallowScreenshot } from 'react-native-screen-capture'; import loc from '../../loc'; import { useStorage } from '../../hooks/context/useStorage'; -import { scanQrHelper } from '../../helpers/scan-qr'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import ToolTipMenu from '../../components/TooltipMenu'; import { CommonToolTipActions } from '../../typings/CommonToolTipActions'; @@ -46,10 +45,9 @@ const WalletsAddMultisigStep2 = () => { const { addWallet, saveToDisk, isElectrumDisabled, sleep, currentSharedCosigner, setSharedCosigner } = useStorage(); const { colors } = useTheme(); - const { navigate, navigateToWalletsList } = useExtendedNavigation(); - const { m, n, format, walletLabel } = useRoute().params; - const { name } = useRoute(); - + const { navigate, navigateToWalletsList, setParams } = useExtendedNavigation(); + const params = useRoute().params; + const { m, n, format, walletLabel } = params; const [cosigners, setCosigners] = useState([]); // array of cosigners user provided. if format [cosigner, fp, path] const [isLoading, setIsLoading] = useState(false); const mnemonicsModalRef = useRef(null); @@ -203,7 +201,7 @@ const WalletsAddMultisigStep2 = () => { }); }; - const getPath = () => { + const getPath = useCallback(() => { let path = ''; switch (format) { case MultisigHDWallet.FORMAT_P2WSH: @@ -221,7 +219,7 @@ const WalletsAddMultisigStep2 = () => { throw new Error('This should never happen'); } return path; - }; + }, [format]); const viewKey = cosigner => { if (MultisigHDWallet.isXpubValid(cosigner[0])) { @@ -267,52 +265,55 @@ const WalletsAddMultisigStep2 = () => { provideMnemonicsModalRef.current.present(); }; - const tryUsingXpub = async (xpub, fp, path) => { - if (!MultisigHDWallet.isXpubForMultisig(xpub)) { + const tryUsingXpub = useCallback( + async (xpub, fp, path) => { + if (!MultisigHDWallet.isXpubForMultisig(xpub)) { + provideMnemonicsModalRef.current.dismiss(); + setIsLoading(false); + setImportText(''); + setAskPassphrase(false); + presentAlert({ message: loc.multisig.not_a_multisignature_xpub }); + return; + } + if (fp) { + // do nothing, it's already set + } else { + try { + fp = await prompt(loc.multisig.input_fp, loc.multisig.input_fp_explain, true, 'plain-text'); + fp = (fp + '').toUpperCase(); + if (!MultisigHDWallet.isFpValid(fp)) fp = '00000000'; + } catch (e) { + return setIsLoading(false); + } + } + if (path) { + // do nothing, it's already set + } else { + try { + path = await prompt( + loc.multisig.input_path, + loc.formatString(loc.multisig.input_path_explain, { default: getPath() }), + true, + 'plain-text', + ); + if (!MultisigHDWallet.isPathValid(path)) path = getPath(); + } catch { + return setIsLoading(false); + } + } + provideMnemonicsModalRef.current.dismiss(); setIsLoading(false); setImportText(''); setAskPassphrase(false); - presentAlert({ message: loc.multisig.not_a_multisignature_xpub }); - return; - } - if (fp) { - // do nothing, it's already set - } else { - try { - fp = await prompt(loc.multisig.input_fp, loc.multisig.input_fp_explain, true, 'plain-text'); - fp = (fp + '').toUpperCase(); - if (!MultisigHDWallet.isFpValid(fp)) fp = '00000000'; - } catch (e) { - return setIsLoading(false); - } - } - if (path) { - // do nothing, it's already set - } else { - try { - path = await prompt( - loc.multisig.input_path, - loc.formatString(loc.multisig.input_path_explain, { default: getPath() }), - true, - 'plain-text', - ); - if (!MultisigHDWallet.isPathValid(path)) path = getPath(); - } catch { - return setIsLoading(false); - } - } - provideMnemonicsModalRef.current.dismiss(); - setIsLoading(false); - setImportText(''); - setAskPassphrase(false); - - const cosignersCopy = [...cosigners]; - cosignersCopy.push([xpub, fp, path]); - if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setCosigners(cosignersCopy); - }; + const cosignersCopy = [...cosigners]; + cosignersCopy.push([xpub, fp, path]); + if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + setCosigners(cosignersCopy); + }, + [cosigners, getPath], + ); const useMnemonicPhrase = async () => { setIsLoading(true); @@ -371,114 +372,122 @@ const WalletsAddMultisigStep2 = () => { return hd.validateMnemonic(); }; - const onBarScanned = ret => { - if (!ret.data) ret = { data: ret }; + const onBarScanned = useCallback( + ret => { + if (!ret.data) ret = { data: ret }; - try { - let retData = JSON.parse(ret.data); - if (Array.isArray(retData) && retData.length === 1) { - // UR:CRYPTO-ACCOUNT now parses as an array of accounts, even if it is just one, - // so in case of cosigner data its gona be an array of 1 cosigner account. lets pop it for - // the code that expects it - retData = retData.pop(); - ret.data = JSON.stringify(retData); - } - } catch (_) {} + try { + let retData = JSON.parse(ret.data); + if (Array.isArray(retData) && retData.length === 1) { + // UR:CRYPTO-ACCOUNT now parses as an array of accounts, even if it is just one, + // so in case of cosigner data its gona be an array of 1 cosigner account. lets pop it for + // the code that expects it + retData = retData.pop(); + ret.data = JSON.stringify(retData); + } + } catch (_) {} - if (ret.data.toUpperCase().startsWith('UR')) { - presentAlert({ message: 'BC-UR not decoded. This should never happen' }); - } else if (isValidMnemonicSeed(ret.data)) { - setImportText(ret.data); - setTimeout(() => { - provideMnemonicsModalRef.current.present().then(() => {}); - }, 100); - } else { - if (MultisigHDWallet.isXpubValid(ret.data) && !MultisigHDWallet.isXpubForMultisig(ret.data)) { - return presentAlert({ message: loc.multisig.not_a_multisignature_xpub }); - } - if (MultisigHDWallet.isXpubValid(ret.data)) { - return tryUsingXpub(ret.data); - } - let cosigner = new MultisigCosigner(ret.data); - if (!cosigner.isValid()) return presentAlert({ message: loc.multisig.invalid_cosigner }); - provideMnemonicsModalRef.current.dismiss(); - if (cosigner.howManyCosignersWeHave() > 1) { - // lets look for the correct cosigner. thats probably gona be the one with specific corresponding path, - // for example m/48'/0'/0'/2' if user chose to setup native segwit in BW - for (const cc of cosigner.getAllCosigners()) { - switch (format) { - case MultisigHDWallet.FORMAT_P2WSH: - if (cc.getPath().startsWith('m/48') && cc.getPath().endsWith("/2'")) { - // found it - cosigner = cc; - } - break; - case MultisigHDWallet.FORMAT_P2SH_P2WSH: - case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT: - if (cc.getPath().startsWith('m/48') && cc.getPath().endsWith("/1'")) { - // found it - cosigner = cc; - } - break; - case MultisigHDWallet.FORMAT_P2SH: - if (cc.getPath().startsWith('m/45')) { - // found it - cosigner = cc; - } - break; - default: - console.error('Unexpected format:', format); - throw new Error('This should never happen'); + if (ret.data.toUpperCase().startsWith('UR')) { + presentAlert({ message: 'BC-UR not decoded. This should never happen' }); + } else if (isValidMnemonicSeed(ret.data)) { + setImportText(ret.data); + setTimeout(() => { + provideMnemonicsModalRef.current.present().then(() => {}); + }, 100); + } else { + if (MultisigHDWallet.isXpubValid(ret.data) && !MultisigHDWallet.isXpubForMultisig(ret.data)) { + return presentAlert({ message: loc.multisig.not_a_multisignature_xpub }); + } + if (MultisigHDWallet.isXpubValid(ret.data)) { + return tryUsingXpub(ret.data); + } + let cosigner = new MultisigCosigner(ret.data); + if (!cosigner.isValid()) return presentAlert({ message: loc.multisig.invalid_cosigner }); + provideMnemonicsModalRef.current.dismiss(); + if (cosigner.howManyCosignersWeHave() > 1) { + // lets look for the correct cosigner. thats probably gona be the one with specific corresponding path, + // for example m/48'/0'/0'/2' if user chose to setup native segwit in BW + for (const cc of cosigner.getAllCosigners()) { + switch (format) { + case MultisigHDWallet.FORMAT_P2WSH: + if (cc.getPath().startsWith('m/48') && cc.getPath().endsWith("/2'")) { + // found it + cosigner = cc; + } + break; + case MultisigHDWallet.FORMAT_P2SH_P2WSH: + case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT: + if (cc.getPath().startsWith('m/48') && cc.getPath().endsWith("/1'")) { + // found it + cosigner = cc; + } + break; + case MultisigHDWallet.FORMAT_P2SH: + if (cc.getPath().startsWith('m/45')) { + // found it + cosigner = cc; + } + break; + default: + console.error('Unexpected format:', format); + throw new Error('This should never happen'); + } } } + + for (const existingCosigner of cosigners) { + if (existingCosigner[0] === cosigner.getXpub()) return presentAlert({ message: loc.multisig.this_cosigner_is_already_imported }); + } + + // now, validating that cosigner is in correct format: + + let correctFormat = false; + switch (format) { + case MultisigHDWallet.FORMAT_P2WSH: + if (cosigner.getPath().startsWith('m/48') && cosigner.getPath().endsWith("/2'")) { + correctFormat = true; + } + break; + case MultisigHDWallet.FORMAT_P2SH_P2WSH: + case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT: + if (cosigner.getPath().startsWith('m/48') && cosigner.getPath().endsWith("/1'")) { + correctFormat = true; + } + break; + case MultisigHDWallet.FORMAT_P2SH: + if (cosigner.getPath().startsWith('m/45')) { + correctFormat = true; + } + break; + default: + console.error('Unexpected format:', format); + throw new Error('This should never happen'); + } + + if (!correctFormat) return presentAlert({ message: loc.formatString(loc.multisig.invalid_cosigner_format, { format }) }); + + const cosignersCopy = [...cosigners]; + cosignersCopy.push([cosigner.getXpub(), cosigner.getFp(), cosigner.getPath()]); + if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + setCosigners(cosignersCopy); } - - for (const existingCosigner of cosigners) { - if (existingCosigner[0] === cosigner.getXpub()) return presentAlert({ message: loc.multisig.this_cosigner_is_already_imported }); - } - - // now, validating that cosigner is in correct format: - - let correctFormat = false; - switch (format) { - case MultisigHDWallet.FORMAT_P2WSH: - if (cosigner.getPath().startsWith('m/48') && cosigner.getPath().endsWith("/2'")) { - correctFormat = true; - } - break; - case MultisigHDWallet.FORMAT_P2SH_P2WSH: - case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT: - if (cosigner.getPath().startsWith('m/48') && cosigner.getPath().endsWith("/1'")) { - correctFormat = true; - } - break; - case MultisigHDWallet.FORMAT_P2SH: - if (cosigner.getPath().startsWith('m/45')) { - correctFormat = true; - } - break; - default: - console.error('Unexpected format:', format); - throw new Error('This should never happen'); - } - - if (!correctFormat) return presentAlert({ message: loc.formatString(loc.multisig.invalid_cosigner_format, { format }) }); - - const cosignersCopy = [...cosigners]; - cosignersCopy.push([cosigner.getXpub(), cosigner.getFp(), cosigner.getPath()]); - if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setCosigners(cosignersCopy); - } - }; + }, + [cosigners, format, tryUsingXpub], + ); const scanOrOpenFile = async () => { await provideMnemonicsModalRef.current.dismiss(); - const scanned = await scanQrHelper(name, true); - if (scanned) { - onBarScanned(scanned); - } + navigate('ScanQRCode'); }; + useEffect(() => { + const scannedData = params.onBarScanned; + if (scannedData) { + onBarScanned(scannedData); + setParams({ onBarScanned: undefined }); + } + }, [onBarScanned, params.onBarScanned, setParams]); + const dashType = ({ index, lastIndex, isChecked, isFocus }) => { if (isChecked) { if (index === lastIndex) { diff --git a/typings/CommonToolTipActions.ts b/typings/CommonToolTipActions.ts index 0ef572e0f..50fb741e1 100644 --- a/typings/CommonToolTipActions.ts +++ b/typings/CommonToolTipActions.ts @@ -100,7 +100,17 @@ const icons = { Delete: { iconValue: 'trash' }, } as const; -export const CommonToolTipActions = { +export type ToolTipAction = { + id: string; + text: string; + subtitle?: string; + icon?: { iconValue: string }; + hidden?: boolean; + menuState?: boolean; + destructive?: boolean; +}; + +export const CommonToolTipActions: Record = { CopyTXID: { id: keys.CopyTXID, text: loc.transactions.details_copy_txid, From 297e8d8e429a7c1934121ebfe90619d3463587e3 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Fri, 3 Jan 2025 13:34:55 -0400 Subject: [PATCH 08/11] REF: Reuse fs class --- blue_modules/fs.ts | 16 ++++++++++---- loc/en.json | 2 +- screen/send/ScanQRCode.js | 45 +++++---------------------------------- 3 files changed, 18 insertions(+), 45 deletions(-) diff --git a/blue_modules/fs.ts b/blue_modules/fs.ts index 9e7630d5d..3380fa142 100644 --- a/blue_modules/fs.ts +++ b/blue_modules/fs.ts @@ -132,13 +132,19 @@ export const showImagePickerAndReadImage = async (): Promise return undefined; } else if (response.errorCode) { throw new Error(response.errorMessage); - } else if (response.assets?.[0]?.uri) { + } else if (response.assets) { try { - const result = await RNQRGenerator.detect({ uri: decodeURI(response.assets[0].uri.toString()) }); - return result?.values[0]; + const uri = response.assets[0].uri; + if (uri) { + const result = await RNQRGenerator.detect({ uri: decodeURI(uri.toString()) }); + if (result?.values.length > 0) { + return result?.values[0]; + } + } + throw new Error(loc.send.qr_error_no_qrcode); } catch (error) { console.error(error); - throw new Error(loc.send.qr_error_no_qrcode); + presentAlert({ message: loc.send.qr_error_no_qrcode }); } } @@ -187,9 +193,11 @@ export const showFilePickerAndReadFile = async function (): Promise<{ data: stri if (result) { return { data: result.values[0], uri: fileCopyUri }; } + presentAlert({ message: loc.send.qr_error_no_qrcode }); return { data: false, uri: false }; } catch (error) { console.error(error); + presentAlert({ message: loc.send.qr_error_no_qrcode }); return { data: false, uri: false }; } } diff --git a/loc/en.json b/loc/en.json index d8fa2fef3..31fd92a98 100644 --- a/loc/en.json +++ b/loc/en.json @@ -192,7 +192,7 @@ "outdated_rate": "Rate was last updated: {date}", "psbt_tx_open": "Open Signed Transaction", "psbt_tx_scan": "Scan Signed Transaction", - "qr_error_no_qrcode": "We were unable to find a QR Code in the selected image. Make sure the image contains only a QR Code and no additional content such as text or buttons.", + "qr_error_no_qrcode": "We were unable to find a valid QR Code in the selected image. Make sure the image contains only a QR Code and no additional content such as text or buttons.", "reset_amount": "Reset Amount", "reset_amount_confirm": "Would you like to reset the amount?", "success_done": "Done", diff --git a/screen/send/ScanQRCode.js b/screen/send/ScanQRCode.js index be76dd7e0..7706e9620 100644 --- a/screen/send/ScanQRCode.js +++ b/screen/send/ScanQRCode.js @@ -5,20 +5,16 @@ import React, { useCallback, useEffect, useState } from 'react'; import { Alert, Image, Platform, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native'; import { CameraScreen } from 'react-native-camera-kit'; import { Icon } from '@rneui/themed'; -import { launchImageLibrary } from 'react-native-image-picker'; - 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 presentAlert from '../../components/Alert'; 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 RNQRGenerator from 'rn-qr-generator'; let decoder = false; @@ -297,42 +293,11 @@ const ScanQRCode = () => { 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) { - 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); - } - } - }, - ); + fs.showImagePickerAndReadImage() + .then(data => { + if (data) onBarCodeRead({ data }); + }) + .finally(() => setIsLoading(false)); } }; From b9714332456a9614795d8b0a58b761b0fc3f5682 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Sat, 4 Jan 2025 19:31:08 -0400 Subject: [PATCH 09/11] FIX: Ask for camera auth --- hooks/useExtendedNavigation.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hooks/useExtendedNavigation.ts b/hooks/useExtendedNavigation.ts index bfa280351..be5b42cdd 100644 --- a/hooks/useExtendedNavigation.ts +++ b/hooks/useExtendedNavigation.ts @@ -3,6 +3,7 @@ import { navigationRef } from '../NavigationService'; import { presentWalletExportReminder } from '../helpers/presentWalletExportReminder'; import { unlockWithBiometrics, useBiometrics } from './useBiometrics'; import { useStorage } from './context/useStorage'; +import { requestCameraAuthorization } from '../helpers/scan-qr'; // List of screens that require biometrics const requiresBiometrics = ['WalletExportRoot', 'WalletXpubRoot', 'ViewEditMultisigCosignersRoot', 'ExportMultisigCoordinationSetupRoot']; @@ -90,6 +91,10 @@ export const useExtendedNavigation = >() return; // Prevent proceeding with the original navigation if the reminder is shown } } + + if (screenName === 'ScanQRCode') { + await requestCameraAuthorization(); + } proceedWithNavigation(); })(); }; From 8b45f11441b446a34fff4e2f2b6caf2988ab2b41 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Sat, 4 Jan 2025 19:59:46 -0400 Subject: [PATCH 10/11] REF: Add loading indicator to Edit Vault row --- components/MultipleStepsListItem.js | 18 ++- screen/wallets/ViewEditMultisigCosigners.tsx | 142 +++++++++++-------- 2 files changed, 100 insertions(+), 60 deletions(-) diff --git a/components/MultipleStepsListItem.js b/components/MultipleStepsListItem.js index 409b46d51..a6f5e4a23 100644 --- a/components/MultipleStepsListItem.js +++ b/components/MultipleStepsListItem.js @@ -155,9 +155,13 @@ const MultipleStepsListItem = props => { style={[styles.rowPartialRightButton, stylesHook.provideKeyButton, rightButtonOpacity]} onPress={props.button.onPress} > - - {props.button.text} - + {props.button.showActivityIndicator ? ( + + ) : ( + + {props.button.text} + + )} )} @@ -171,7 +175,11 @@ const MultipleStepsListItem = props => { style={styles.rightButton} onPress={props.rightButton.onPress} > - {props.rightButton.text} + {props.rightButton.showActivityIndicator ? ( + + ) : ( + {props.rightButton.text} + )} )} @@ -194,11 +202,13 @@ MultipleStepsListItem.propTypes = { disabled: PropTypes.bool, buttonType: PropTypes.number, leftText: PropTypes.string, + showActivityIndicator: PropTypes.bool, }), rightButton: PropTypes.shape({ text: PropTypes.string, onPress: PropTypes.func, disabled: PropTypes.bool, + showActivityIndicator: PropTypes.bool, }), }; diff --git a/screen/wallets/ViewEditMultisigCosigners.tsx b/screen/wallets/ViewEditMultisigCosigners.tsx index bbe221877..520d139cd 100644 --- a/screen/wallets/ViewEditMultisigCosigners.tsx +++ b/screen/wallets/ViewEditMultisigCosigners.tsx @@ -50,6 +50,7 @@ import { useSettings } from '../../hooks/context/useSettings'; import { ViewEditMultisigCosignersStackParamList } from '../../navigation/ViewEditMultisigCosignersStack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { navigationRef } from '../../NavigationService'; +import SafeArea from '../../components/SafeArea'; type RouteParams = RouteProp; type NavigationProp = NativeStackNavigationProp; @@ -78,6 +79,7 @@ const ViewEditMultisigCosigners: React.FC = () => { const [exportStringURv2, setExportStringURv2] = useState(''); // used in QR const [exportFilename, setExportFilename] = useState('bw-cosigner.json'); const [vaultKeyData, setVaultKeyData] = useState({ keyIndex: 1, xpub: '', seed: '', passphrase: '', path: '', fp: '', isLoading: false }); // string rendered in modal + const [isVaultKeyIndexDataLoading, setIsVaultKeyIndexDataLoading] = useState(undefined); const [askPassphrase, setAskPassphrase] = useState(false); const data = useRef(); /* discardChangesRef is only so the action sheet can be shown on mac catalyst when a @@ -275,6 +277,24 @@ const ViewEditMultisigCosigners: React.FC = () => { ); }; + const resetModalData = () => { + setVaultKeyData({ + keyIndex: 1, + xpub: '', + seed: '', + passphrase: '', + path: '', + fp: '', + isLoading: false, + }); + setImportText(''); + setExportString('{}'); + setExportStringURv2(''); + setExportFilename(''); + setIsSaveButtonDisabled(false); + setAskPassphrase(false); + }; + const _renderKeyItem = (el: ListRenderItemInfo) => { if (!wallet) { // failsafe @@ -312,29 +332,34 @@ const ViewEditMultisigCosigners: React.FC = () => { buttonType: MultipleStepsListItemButtohType.partial, leftText, text: loc.multisig.view, + showActivityIndicator: isVaultKeyIndexDataLoading === el.index + 1, disabled: vaultKeyData.isLoading, onPress: () => { - const keyIndex = el.index + 1; - const xpub = wallet.getCosigner(keyIndex); - const fp = wallet.getFingerprint(keyIndex); - const path = wallet.getCustomDerivationPathForCosigner(keyIndex); - if (!path) { - presentAlert({ message: 'Cannot find derivation path for this cosigner' }); - return; - } - setVaultKeyData({ - keyIndex, - seed: '', - passphrase: '', - xpub, - fp, - path, - isLoading: false, - }); - setExportString(MultisigCosigner.exportToJson(fp, xpub, path)); - setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]); - setExportFilename('bw-cosigner-' + fp + '.json'); - mnemonicsModalRef.current?.present(); + setIsVaultKeyIndexDataLoading(el.index + 1); + setTimeout(() => { + const keyIndex = el.index + 1; + const xpub = wallet.getCosigner(keyIndex); + const fp = wallet.getFingerprint(keyIndex); + const path = wallet.getCustomDerivationPathForCosigner(keyIndex); + if (!path) { + presentAlert({ message: 'Cannot find derivation path for this cosigner' }); + return; + } + setVaultKeyData({ + keyIndex, + seed: '', + passphrase: '', + xpub, + fp, + path, + isLoading: false, + }); + setExportString(MultisigCosigner.exportToJson(fp, xpub, path)); + setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]); + setExportFilename('bw-cosigner-' + fp + '.json'); + mnemonicsModalRef.current?.present(); + setIsVaultKeyIndexDataLoading(undefined); + }, 100); }, }} dashes={MultipleStepsListItemDashType.topAndBottom} @@ -363,31 +388,36 @@ const ViewEditMultisigCosigners: React.FC = () => { leftText, text: loc.multisig.view, disabled: vaultKeyData.isLoading, + showActivityIndicator: isVaultKeyIndexDataLoading === el.index + 1, buttonType: MultipleStepsListItemButtohType.partial, onPress: () => { - const keyIndex = el.index + 1; - const seed = wallet.getCosigner(keyIndex); - const passphrase = wallet.getCosignerPassphrase(keyIndex); - setVaultKeyData({ - keyIndex, - seed, - xpub: '', - fp: '', - path: '', - passphrase: passphrase ?? '', - isLoading: false, - }); - mnemonicsModalRef.current?.present(); - const fp = wallet.getFingerprint(keyIndex); - const path = wallet.getCustomDerivationPathForCosigner(keyIndex); - if (!path) { - presentAlert({ message: 'Cannot find derivation path for this cosigner' }); - return; - } - const xpub = wallet.convertXpubToMultisignatureXpub(MultisigHDWallet.seedToXpub(seed, path, passphrase)); - setExportString(MultisigCosigner.exportToJson(fp, xpub, path)); - setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]); - setExportFilename('bw-cosigner-' + fp + '.json'); + setIsVaultKeyIndexDataLoading(el.index + 1); + setTimeout(() => { + const keyIndex = el.index + 1; + const seed = wallet.getCosigner(keyIndex); + const passphrase = wallet.getCosignerPassphrase(keyIndex); + setVaultKeyData({ + keyIndex, + seed, + xpub: '', + fp: '', + path: '', + passphrase: passphrase ?? '', + isLoading: false, + }); + const fp = wallet.getFingerprint(keyIndex); + const path = wallet.getCustomDerivationPathForCosigner(keyIndex); + if (!path) { + presentAlert({ message: 'Cannot find derivation path for this cosigner' }); + return; + } + const xpub = wallet.convertXpubToMultisignatureXpub(MultisigHDWallet.seedToXpub(seed, path, passphrase)); + setExportString(MultisigCosigner.exportToJson(fp, xpub, path)); + setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]); + setExportFilename('bw-cosigner-' + fp + '.json'); + mnemonicsModalRef.current?.present(); + setIsVaultKeyIndexDataLoading(undefined); + }, 100); }, }} dashes={MultipleStepsListItemDashType.topAndBottom} @@ -442,6 +472,7 @@ const ViewEditMultisigCosigners: React.FC = () => { await provideMnemonicsModalRef.current?.dismiss(); await shareModalRef.current?.dismiss(); await mnemonicsModalRef.current?.dismiss(); + resetModalData(); }; const handleUseMnemonicPhrase = async () => { let passphrase; @@ -478,9 +509,7 @@ const ViewEditMultisigCosigners: React.FC = () => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setWallet(wallet); provideMnemonicsModalRef.current?.dismiss(); - setIsSaveButtonDisabled(false); - setImportText(''); - setAskPassphrase(false); + resetModalData(); }; const xpubInsteadOfSeed = (index: number): Promise => { @@ -517,8 +546,7 @@ const ViewEditMultisigCosigners: React.FC = () => { const hideProvideMnemonicsModal = () => { Keyboard.dismiss(); provideMnemonicsModalRef.current?.dismiss(); - setImportText(''); - setAskPassphrase(false); + resetModalData(); }; const hideShareModal = () => {}; @@ -583,13 +611,15 @@ const ViewEditMultisigCosigners: React.FC = () => { backgroundColor={colors.elevated} shareContent={{ fileName: exportFilename, fileContent: exportString }} > - - - {loc.multisig.this_is_cosigners_xpub} {Platform.OS === 'ios' ? loc.multisig.this_is_cosigners_xpub_airdrop : ''} - - - - + + + + {loc.multisig.this_is_cosigners_xpub} {Platform.OS === 'ios' ? loc.multisig.this_is_cosigners_xpub_airdrop : ''} + + + + + ); }; From e68b6936e6224b8e2b01e8353af349be885023ca Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Sat, 4 Jan 2025 22:32:26 -0400 Subject: [PATCH 11/11] ADD: HKD fiat --- android/app/src/main/assets/fiatUnits.json | 7 +++++++ models/fiatUnits.json | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/android/app/src/main/assets/fiatUnits.json b/android/app/src/main/assets/fiatUnits.json index c00a8ddaa..91611a3ff 100644 --- a/android/app/src/main/assets/fiatUnits.json +++ b/android/app/src/main/assets/fiatUnits.json @@ -125,6 +125,13 @@ "symbol": "£", "country": "United Kingdom (British Pound)" }, + "HKD": { + "endPointKey": "HKD", + "locale": "zh-HK", + "source": "CoinGecko", + "symbol": "HK$", + "country": "Hong Kong (Hong Kong Dollar)" + }, "HRK": { "endPointKey": "HRK", "locale": "hr-HR", diff --git a/models/fiatUnits.json b/models/fiatUnits.json index c00a8ddaa..91611a3ff 100644 --- a/models/fiatUnits.json +++ b/models/fiatUnits.json @@ -125,6 +125,13 @@ "symbol": "£", "country": "United Kingdom (British Pound)" }, + "HKD": { + "endPointKey": "HKD", + "locale": "zh-HK", + "source": "CoinGecko", + "symbol": "HK$", + "country": "Hong Kong (Hong Kong Dollar)" + }, "HRK": { "endPointKey": "HRK", "locale": "hr-HR",