Compare commits

...

40 commits

Author SHA1 Message Date
GLaDOS
c7909049dc
Merge pull request #7694 from BlueWallet/handled
FIX: Dleeting wallet would not popToTop after deleting
2025-03-13 13:44:07 +00:00
Marcos Rodriguez Velez
b5270d0a07 FIX: Dleeting wallet would not popToTop after deleting 2025-03-13 08:10:06 -04:00
GLaDOS
4f3b828990
Merge pull request #7689 from BlueWallet/sle
ADD: Allow quick tap to copy
2025-03-13 11:04:17 +00:00
GLaDOS
26720e8284
Merge pull request #7692 from BlueWallet/f
FIX: Issue - Function to broadcast transaction from *.final PSBT not …
2025-03-13 10:46:24 +00:00
Marcos Rodriguez Velez
a80bacc0f4 FIX: Issue - Function to broadcast transaction from *.final PSBT not available 7.1.4 #7688 2025-03-12 22:13:09 -04:00
Marcos Rodriguez Velez
5f18540ca7 FIX: UX in ManageWallets 2025-03-12 20:42:29 -04:00
Marcos Rodriguez Velez
c14cb3508c ADD: Allow quick tap to copy 2025-03-12 19:00:08 -04:00
GLaDOS
751c7d6f45
Merge pull request #7682 from BlueWallet/menud
REF: MenuItem memory
2025-03-10 18:39:40 +00:00
GLaDOS
0b1c3dd9f7
Merge pull request #7684 from BlueWallet/man
FIX: DIscard changes alert was not working
2025-03-09 19:00:23 +00:00
Marcos Rodriguez Velez
ae89a59794 FIX: DIscard changes alert was not working 2025-03-09 10:23:56 -04:00
Marcos Rodriguez Velez
10b3432e0e OPS: Version bump 2025-03-09 10:17:04 -04:00
Marcos Rodriguez Velez
c67eea8155 REF: Use bottom tabs 2025-03-09 07:45:19 -04:00
Marcos Rodriguez Velez
9421511f74 Update useMenuElements.ts 2025-03-08 19:11:51 -04:00
Marcos Rodriguez Velez
9ec0ef51e4 Update useMenuElements.ios.ts 2025-03-08 19:09:41 -04:00
Marcos Rodriguez Velez
1cada11c50 REF: MenuItem memory 2025-03-08 11:06:55 -04:00
Marcos Rodriguez Velez
d2cebde6ad OPS: Version bump 2025-03-08 09:38:46 -04:00
GLaDOS
1a940971bc
Merge pull request #7677 from BlueWallet/scanln
REF: ScanLNDInvoice to TSX
2025-03-08 09:22:24 +00:00
GLaDOS
28316b4d73
Merge pull request #7654 from BlueWallet/renovate/androidx.constraintlayout-constraintlayout-2.x
Update dependency androidx.constraintlayout:constraintlayout to v2.2.1
2025-03-08 08:35:37 +00:00
GLaDOS
4670eea38a
Merge pull request #7681 from BlueWallet/ele
FIX: Can not turn Electrum Server on. #7680
2025-03-08 08:15:05 +00:00
Marcos Rodriguez VĂ©lez
b2552bdc71
Merge branch 'master' into ele 2025-03-07 22:29:36 -04:00
Marcos Rodriguez VĂ©lez
dbd4066f7e
Merge branch 'master' into scanln 2025-03-07 22:29:25 -04:00
Marcos Rodriguez VĂ©lez
4cdd952f90
FIX: Update use of CoinDesk API (#7678) 2025-03-07 22:29:12 -04:00
Marcos Rodriguez Velez
ddee4cdaaf FIX: Can not turn Electrum Server on. #7680 2025-03-07 21:58:33 -04:00
Marcos Rodriguez Velez
0aa6b96e4b w 2025-03-07 19:07:15 -04:00
Marcos Rodriguez Velez
8d49aff279 Merge branch 'master' into scanln 2025-03-07 19:03:43 -04:00
GLaDOS
18a187b120
Merge pull request #7663 from BlueWallet/virw
REF: View Edit Multisig navigation
2025-03-06 10:17:44 +00:00
Marcos Rodriguez VĂ©lez
1f77a852a8
Rename LazyLoadScanLndInvoiceStack.tsx to LazyLoadScanLNDInvoiceStack.tsx 2025-03-05 23:12:45 -04:00
Marcos Rodriguez VĂ©lez
9d899d672d
Rename ScanLndInvoiceStack.tsx to ScanLNDInvoiceStack.tsx 2025-03-05 23:09:28 -04:00
Marcos Rodriguez Velez
e7b81e5517 wi 2025-03-05 21:50:50 -04:00
Marcos Rodriguez Velez
f8af06e2ae REF: ScanLNDInvoice to TSX 2025-03-05 21:43:02 -04:00
Marcos Rodriguez Velez
8b81472fa4 OPS: Master was broken for catalyst 2025-03-05 20:54:50 -04:00
Marcos Rodriguez Velez
4ad2b15070 OPS: Lock file 2025-03-05 20:34:29 -04:00
Marcos Rodriguez Velez
dd118af993 Update project.pbxproj 2025-03-05 20:31:25 -04:00
GLaDOS
d375bd9780
Merge pull request #7672 from BlueWallet/cc
REF: CompanionDelegate to hook
2025-03-05 22:54:21 +00:00
Marcos Rodriguez Velez
040f91028a Update ScanQRCode.tsx 2025-03-03 14:33:49 -04:00
Marcos Rodriguez Velez
1c8aa08de8 Merge branch 'master' into cc 2025-03-03 14:22:21 -04:00
Marcos Rodriguez Velez
d7743a740f Merge branch 'master' into virw 2025-03-03 14:00:14 -04:00
renovate[bot]
88be0332e4
Update dependency androidx.constraintlayout:constraintlayout to v2.2.1 2025-03-03 13:12:51 +00:00
Marcos Rodriguez Velez
ef5887f28b REF: CompanionDelegate to hook 2025-03-02 22:12:50 -04:00
Marcos Rodriguez Velez
136dd20f9e REF: View Edit Multisig navigation
Easier to popTo since its just 1  screen
2025-03-01 10:10:00 -04:00
42 changed files with 847 additions and 584 deletions

View file

@ -87,7 +87,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1 versionCode 1
versionName "7.1.3" versionName "7.1.5"
testBuildType System.getProperty('testBuildType', 'debug') testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
} }
@ -137,7 +137,7 @@ dependencies {
androidTestImplementation('com.wix:detox:0.1.1') androidTestImplementation('com.wix:detox:0.1.1')
implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.appcompat:appcompat:1.7.0'
implementation fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.constraintlayout:constraintlayout:2.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
} }
apply plugin: 'com.google.gms.google-services' // Google Services plugin apply plugin: 'com.google.gms.google-services' // Google Services plugin
apply plugin: "com.bugsnag.android.gradle" apply plugin: "com.bugsnag.android.gradle"

View file

@ -56,7 +56,8 @@ object MarketAPI {
"CoinGecko" -> "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${endPointKey.lowercase()}" "CoinGecko" -> "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${endPointKey.lowercase()}"
"BNR" -> "https://www.bnr.ro/nbrfxrates.xml" "BNR" -> "https://www.bnr.ro/nbrfxrates.xml"
"Kraken" -> "https://api.kraken.com/0/public/Ticker?pair=XXBTZ${endPointKey.uppercase()}" "Kraken" -> "https://api.kraken.com/0/public/Ticker?pair=XXBTZ${endPointKey.uppercase()}"
else -> "https://api.coindesk.com/v1/bpi/currentprice/$endPointKey.json" "CoinDesk" -> "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=${endPointKey.uppercase()}"
else -> "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=${endPointKey.uppercase()}"
} }
} }
} }
@ -73,6 +74,10 @@ object MarketAPI {
"coinpaprika" -> json.getJSONObject("quotes").getJSONObject("INR").getString("price") "coinpaprika" -> json.getJSONObject("quotes").getJSONObject("INR").getString("price")
"Coinbase" -> json.getJSONObject("data").getString("amount") "Coinbase" -> json.getJSONObject("data").getString("amount")
"Kraken" -> json.getJSONObject("result").getJSONObject("XXBTZ${endPointKey.uppercase()}").getJSONArray("c").getString(0) "Kraken" -> json.getJSONObject("result").getJSONObject("XXBTZ${endPointKey.uppercase()}").getJSONArray("c").getString(0)
"CoinDesk" -> {
val rate = json.optDouble(endPointKey.uppercase(), -1.0)
if (rate < 0) null else rate.toString()
}
else -> null else -> null
} }
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -56,7 +56,7 @@
<TextView <TextView
android:id="@+id/price_value" android:id="@+id/price_value"
style="@style/WidgetTextPrimary" style="@style/WidgetTextPrimary"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_gravity="end"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
@ -67,6 +67,7 @@
android:autoSizeTextType="uniform" android:autoSizeTextType="uniform"
android:duplicateParentState="false" android:duplicateParentState="false"
android:editable="false" android:editable="false"
android:gravity="end"
android:lines="1" android:lines="1"
android:text="Loading..." android:text="Loading..."
android:textSize="24sp" android:textSize="24sp"

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_layout" android:initialLayout="@layout/widget_layout"
android:minWidth="160dp" android:minWidth="170dp"
android:minHeight="100dp" android:minHeight="100dp"
android:updatePeriodMillis="0" android:updatePeriodMillis="0"
android:widgetCategory="home_screen" android:widgetCategory="home_screen"

View file

@ -86,9 +86,9 @@ class DeeplinkSchemaMatch {
} else if (wallet.chain === Chain.OFFCHAIN) { } else if (wallet.chain === Chain.OFFCHAIN) {
if (action === 'openSend') { if (action === 'openSend') {
completionHandler([ completionHandler([
'ScanLndInvoiceRoot', 'ScanLNDInvoiceRoot',
{ {
screen: 'ScanLndInvoice', screen: 'ScanLNDInvoice',
params: { params: {
walletID: wallet.getID(), walletID: wallet.getID(),
}, },
@ -156,9 +156,9 @@ class DeeplinkSchemaMatch {
]); ]);
} else if (DeeplinkSchemaMatch.isLightningInvoice(event.url)) { } else if (DeeplinkSchemaMatch.isLightningInvoice(event.url)) {
completionHandler([ completionHandler([
'ScanLndInvoiceRoot', 'ScanLNDInvoiceRoot',
{ {
screen: 'ScanLndInvoice', screen: 'ScanLNDInvoice',
params: { params: {
uri: event.url.replace('://', ':'), uri: event.url.replace('://', ':'),
}, },
@ -181,9 +181,9 @@ class DeeplinkSchemaMatch {
// this might be not just an email but a lightning address // this might be not just an email but a lightning address
// @see https://lightningaddress.com // @see https://lightningaddress.com
completionHandler([ completionHandler([
'ScanLndInvoiceRoot', 'ScanLNDInvoiceRoot',
{ {
screen: 'ScanLndInvoice', screen: 'ScanLNDInvoice',
params: { params: {
uri: event.url, uri: event.url,
}, },
@ -305,9 +305,9 @@ class DeeplinkSchemaMatch {
]; ];
} else { } else {
return [ return [
'ScanLndInvoiceRoot', 'ScanLNDInvoiceRoot',
{ {
screen: 'ScanLndInvoice', screen: 'ScanLNDInvoice',
params: { params: {
uri: uri.lndInvoice, uri: uri.lndInvoice,
walletID: wallet.getID(), walletID: wallet.getID(),

View file

@ -79,6 +79,20 @@ export type TransactionOutput = {
}; };
}; };
export interface DecodedInvoice {
destination: string;
payment_hash: string;
num_satoshis: number;
timestamp: number;
expiry: number;
description: string;
description_hash: string;
fallback_addr: string;
cltv_expiry: string;
route_hints: any[];
[key: string]: any;
}
export type LightningTransaction = { export type LightningTransaction = {
memo?: string; memo?: string;
type?: 'user_invoice' | 'payment_request' | 'bitcoind_tx' | 'paid_invoice'; type?: 'user_invoice' | 'payment_request' | 'bitcoind_tx' | 'paid_invoice';

View file

@ -1,10 +1,8 @@
import React, { useCallback } from 'react'; import React from 'react';
import { Keyboard, StyleProp, StyleSheet, TextInput, View, ViewStyle } from 'react-native'; import { StyleProp, StyleSheet, TextInput, View, ViewStyle } from 'react-native';
import loc from '../loc'; import loc from '../loc';
import { AddressInputScanButton } from './AddressInputScanButton'; import { AddressInputScanButton } from './AddressInputScanButton';
import { useTheme } from './themes'; import { useTheme } from './themes';
import DeeplinkSchemaMatch from '../class/deeplink-schema-match';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
interface AddressInputProps { interface AddressInputProps {
isLoading?: boolean; isLoading?: boolean;
@ -31,7 +29,6 @@ interface AddressInputProps {
| 'twitter' | 'twitter'
| 'web-search' | 'web-search'
| 'visible-password'; | 'visible-password';
skipValidation?: boolean;
} }
const AddressInput = ({ const AddressInput = ({
@ -46,7 +43,6 @@ const AddressInput = ({
onBlur = () => {}, onBlur = () => {},
keyboardType = 'default', keyboardType = 'default',
style, style,
skipValidation = false,
}: AddressInputProps) => { }: AddressInputProps) => {
const { colors } = useTheme(); const { colors } = useTheme();
const stylesHook = StyleSheet.create({ const stylesHook = StyleSheet.create({
@ -60,29 +56,6 @@ const AddressInput = ({
}, },
}); });
const validateAddressWithFeedback = useCallback(
(value: string) => {
if (skipValidation) return;
const isBitcoinAddress = DeeplinkSchemaMatch.isBitcoinAddress(value);
const isLightningInvoice = DeeplinkSchemaMatch.isLightningInvoice(value);
const isValid = isBitcoinAddress || isLightningInvoice;
triggerHapticFeedback(isValid ? HapticFeedbackTypes.NotificationSuccess : HapticFeedbackTypes.NotificationError);
return {
isValid,
type: isBitcoinAddress ? 'bitcoin' : isLightningInvoice ? 'lightning' : 'invalid',
};
},
[skipValidation],
);
const onBlurEditing = () => {
if (!skipValidation) {
validateAddressWithFeedback(address);
}
Keyboard.dismiss();
};
return ( return (
<View style={[styles.root, stylesHook.root, style]}> <View style={[styles.root, stylesHook.root, style]}>
<TextInput <TextInput
@ -100,7 +73,7 @@ const AddressInput = ({
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
keyboardType={keyboardType} keyboardType={keyboardType}
{...(skipValidation ? { onBlur } : { onBlur: onBlurEditing })} onBlur={onBlur}
/> />
{editable ? <AddressInputScanButton isLoading={isLoading} onChangeText={onChangeText} /> : null} {editable ? <AddressInputScanButton isLoading={isLoading} onChangeText={onChangeText} /> : null}
</View> </View>

View file

@ -89,6 +89,8 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
}) => { }) => {
const { colors } = useTheme(); const { colors } = useTheme();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isSwipeActive, setIsSwipeActive] = useState(false);
const resetFunctionRef = useRef<(() => void) | null>(null);
const CARD_SORT_ACTIVE = 1.06; const CARD_SORT_ACTIVE = 1.06;
const INACTIVE_SCALE_WHEN_ACTIVE = 0.9; const INACTIVE_SCALE_WHEN_ACTIVE = 0.9;
@ -125,24 +127,39 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
reset(); reset();
}; };
const leftContent = (reset: () => void) => ( const leftContent = (reset: () => void) => {
<LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} /> resetFunctionRef.current = reset;
); return <LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} />;
const handleRightPress = (reset: () => void) => {
handleDeleteWallet(item.data as TWallet);
reset();
}; };
const rightContent = (reset: () => void) => <RightSwipeContent onPress={() => handleRightPress(reset)} />; const handleRightPress = (reset: () => void) => {
reset();
setTimeout(() => {
handleDeleteWallet(item.data as TWallet);
}, 100); // short delay to allow swipe reset animation to complete
};
const rightContent = (reset: () => void) => {
resetFunctionRef.current = reset;
return <RightSwipeContent onPress={() => handleRightPress(reset)} />;
};
const startDrag = useCallback(() => { const startDrag = useCallback(() => {
if (isSwipeActive) {
return;
}
if (resetFunctionRef.current) {
resetFunctionRef.current();
}
scaleValue.setValue(CARD_SORT_ACTIVE); scaleValue.setValue(CARD_SORT_ACTIVE);
triggerHapticFeedback(HapticFeedbackTypes.ImpactMedium); triggerHapticFeedback(HapticFeedbackTypes.ImpactMedium);
if (drag) { if (drag) {
drag(); drag();
} }
}, [CARD_SORT_ACTIVE, drag, scaleValue]); }, [CARD_SORT_ACTIVE, drag, scaleValue, isSwipeActive]);
if (isLoading) { if (isLoading) {
return <ActivityIndicator size="large" color={colors.brandingColor} />; return <ActivityIndicator size="large" color={colors.brandingColor} />;
@ -155,23 +172,38 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
}; };
const backgroundColor = isActive || globalDragActive ? colors.brandingColor : colors.background; const backgroundColor = isActive || globalDragActive ? colors.brandingColor : colors.background;
const swipeDisabled = isActive || globalDragActive;
return ( return (
<Animated.View style={animatedStyle}> <Animated.View style={animatedStyle}>
<ListItem.Swipeable <ListItem.Swipeable
leftWidth={80} leftWidth={swipeDisabled ? 0 : 80}
rightWidth={90} rightWidth={swipeDisabled ? 0 : 90}
containerStyle={[style, { backgroundColor }, isActive || globalDragActive ? styles.transparentBackground : {}]} containerStyle={[style, { backgroundColor }, swipeDisabled ? styles.transparentBackground : {}]}
leftContent={globalDragActive ? null : isActive ? null : leftContent} leftContent={swipeDisabled ? null : leftContent}
rightContent={globalDragActive ? null : isActive ? null : rightContent} rightContent={swipeDisabled ? null : rightContent}
onPressOut={onPressOut} onPressOut={onPressOut}
minSlideWidth={80} minSlideWidth={swipeDisabled ? 0 : 80}
onPressIn={onPressIn} onPressIn={onPressIn}
style={isActive || globalDragActive ? styles.transparentBackground : {}} style={swipeDisabled ? styles.transparentBackground : {}}
onSwipeBegin={direction => {
if (!swipeDisabled) {
console.debug(`Swipe began: ${direction}`);
setIsSwipeActive(true);
}
}}
onSwipeEnd={() => {
if (!swipeDisabled) {
console.debug('Swipe ended');
setIsSwipeActive(false);
}
}}
> >
<ListItem.Content> <ListItem.Content>
<WalletCarouselItem <WalletCarouselItem
item={item.data} item={item.data}
handleLongPress={isDraggingDisabled ? undefined : startDrag} handleLongPress={isDraggingDisabled || isSwipeActive ? undefined : startDrag}
onPress={onPress} onPress={onPress}
onPressIn={onPressIn} onPressIn={onPressIn}
onPressOut={onPressOut} onPressOut={onPressOut}

View file

@ -220,7 +220,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = memo(
} }
const loaded = await LN.loadSuccessfulPayment(paymentHash); const loaded = await LN.loadSuccessfulPayment(paymentHash);
if (loaded) { if (loaded) {
navigate('ScanLndInvoiceRoot', { navigate('ScanLNDInvoiceRoot', {
screen: 'LnurlPaySuccess', screen: 'LnurlPaySuccess',
params: { params: {
paymentHash, paymentHash,

View file

@ -15,7 +15,8 @@ const scanQrHelper = async (): Promise<string> => {
await requestCameraAuthorization(); await requestCameraAuthorization();
return new Promise(resolve => { return new Promise(resolve => {
if (navigationRef.isReady()) { if (navigationRef.isReady()) {
navigationRef.current?.navigate('ScanQRCode', { navigationRef.navigate('ScanQRCode', {
showFileImportButton: true,
onBarScanned: (data: string) => { onBarScanned: (data: string) => {
resolve(data); resolve(data);
}, },

View file

@ -19,25 +19,42 @@ import loc from '../loc';
import { Chain } from '../models/bitcoinUnits'; import { Chain } from '../models/bitcoinUnits';
import { navigationRef } from '../NavigationService'; import { navigationRef } from '../NavigationService';
import ActionSheet from '../screen/ActionSheet'; import ActionSheet from '../screen/ActionSheet';
import { useStorage } from '../hooks/context/useStorage'; import { useStorage } from './context/useStorage';
import RNQRGenerator from 'rn-qr-generator'; import RNQRGenerator from 'rn-qr-generator';
import presentAlert from './Alert'; import presentAlert from '../components/Alert';
import useMenuElements from '../hooks/useMenuElements'; import useWidgetCommunication from './useWidgetCommunication';
import useWidgetCommunication from '../hooks/useWidgetCommunication'; import useWatchConnectivity from './useWatchConnectivity';
import useWatchConnectivity from '../hooks/useWatchConnectivity'; import useDeviceQuickActions from './useDeviceQuickActions';
import useDeviceQuickActions from '../hooks/useDeviceQuickActions'; import useHandoffListener from './useHandoffListener';
import useHandoffListener from '../hooks/useHandoffListener'; import useMenuElements from './useMenuElements';
const ClipboardContentType = Object.freeze({ const ClipboardContentType = Object.freeze({
BITCOIN: 'BITCOIN', BITCOIN: 'BITCOIN',
LIGHTNING: 'LIGHTNING', LIGHTNING: 'LIGHTNING',
}); });
const CompanionDelegates = () => { /**
const { wallets, addWallet, saveToDisk, fetchAndSaveWalletTransactions, refreshAllWalletTransactions, setSharedCosigner } = useStorage(); * Hook that initializes all companion listeners and functionality without rendering a component
*/
const useCompanionListeners = (skipIfNotInitialized = true) => {
const {
wallets,
addWallet,
saveToDisk,
fetchAndSaveWalletTransactions,
refreshAllWalletTransactions,
setSharedCosigner,
walletsInitialized,
} = useStorage();
const appState = useRef<AppStateStatus>(AppState.currentState); const appState = useRef<AppStateStatus>(AppState.currentState);
const clipboardContent = useRef<undefined | string>(); const clipboardContent = useRef<undefined | string>();
// We need to call hooks unconditionally before any conditional logic
// We'll use this check inside the effects to conditionally run logic
const shouldActivateListeners = !skipIfNotInitialized || walletsInitialized;
// Initialize other hooks regardless of activation status
// They'll handle their own conditional logic internally
useWatchConnectivity(); useWatchConnectivity();
useWidgetCommunication(); useWidgetCommunication();
useMenuElements(); useMenuElements();
@ -45,6 +62,8 @@ const CompanionDelegates = () => {
useHandoffListener(); useHandoffListener();
const processPushNotifications = useCallback(async () => { const processPushNotifications = useCallback(async () => {
if (!shouldActivateListeners) return false;
await new Promise(resolve => setTimeout(resolve, 200)); await new Promise(resolve => setTimeout(resolve, 200));
try { try {
const notifications2process = await getStoredNotifications(); const notifications2process = await getStoredNotifications();
@ -164,15 +183,19 @@ const CompanionDelegates = () => {
console.error('Failed to process push notifications:', error); console.error('Failed to process push notifications:', error);
} }
return false; return false;
}, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets]); }, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets, shouldActivateListeners]);
useEffect(() => { useEffect(() => {
if (!shouldActivateListeners) return;
initializeNotifications(processPushNotifications); initializeNotifications(processPushNotifications);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [shouldActivateListeners]);
const handleOpenURL = useCallback( const handleOpenURL = useCallback(
async (event: { url: string }): Promise<void> => { async (event: { url: string }): Promise<void> => {
if (!shouldActivateListeners) return;
try { try {
if (!event.url) return; if (!event.url) return;
let decodedUrl: string; let decodedUrl: string;
@ -227,11 +250,13 @@ const CompanionDelegates = () => {
presentAlert({ message: err.message || loc.send.qr_error_no_qrcode }); presentAlert({ message: err.message || loc.send.qr_error_no_qrcode });
} }
}, },
[wallets, addWallet, saveToDisk, setSharedCosigner], [wallets, addWallet, saveToDisk, setSharedCosigner, shouldActivateListeners],
); );
const showClipboardAlert = useCallback( const showClipboardAlert = useCallback(
({ contentType }: { contentType: undefined | string }) => { ({ contentType }: { contentType: undefined | string }) => {
if (!shouldActivateListeners) return;
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight); triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
getClipboardContent().then(clipboard => { getClipboardContent().then(clipboard => {
if (!clipboard) return; if (!clipboard) return;
@ -254,12 +279,13 @@ const CompanionDelegates = () => {
); );
}); });
}, },
[handleOpenURL], [handleOpenURL, shouldActivateListeners],
); );
const handleAppStateChange = useCallback( const handleAppStateChange = useCallback(
async (nextAppState: AppStateStatus | undefined) => { async (nextAppState: AppStateStatus | undefined) => {
if (wallets.length === 0) return; if (!shouldActivateListeners || wallets.length === 0) return;
if ((appState.current.match(/background/) && nextAppState === 'active') || nextAppState === undefined) { if ((appState.current.match(/background/) && nextAppState === 'active') || nextAppState === undefined) {
setTimeout(() => A(A.ENUM.APP_UNSUSPENDED), 2000); setTimeout(() => A(A.ENUM.APP_UNSUSPENDED), 2000);
updateExchangeRate(); updateExchangeRate();
@ -299,10 +325,12 @@ const CompanionDelegates = () => {
appState.current = nextAppState; appState.current = nextAppState;
} }
}, },
[processPushNotifications, showClipboardAlert, wallets], [processPushNotifications, showClipboardAlert, wallets, shouldActivateListeners],
); );
const addListeners = useCallback(() => { const addListeners = useCallback(() => {
if (!shouldActivateListeners) return { urlSubscription: null, appStateSubscription: null };
const urlSubscription = Linking.addEventListener('url', handleOpenURL); const urlSubscription = Linking.addEventListener('url', handleOpenURL);
const appStateSubscription = AppState.addEventListener('change', handleAppStateChange); const appStateSubscription = AppState.addEventListener('change', handleAppStateChange);
@ -310,18 +338,16 @@ const CompanionDelegates = () => {
urlSubscription, urlSubscription,
appStateSubscription, appStateSubscription,
}; };
}, [handleOpenURL, handleAppStateChange]); }, [handleOpenURL, handleAppStateChange, shouldActivateListeners]);
useEffect(() => { useEffect(() => {
const subscriptions = addListeners(); const subscriptions = addListeners();
return () => { return () => {
subscriptions.urlSubscription?.remove(); subscriptions.urlSubscription?.remove?.();
subscriptions.appStateSubscription?.remove(); subscriptions.appStateSubscription?.remove?.();
}; };
}, [addListeners]); }, [addListeners]);
return null;
}; };
export default CompanionDelegates; export default useCompanionListeners;

View file

@ -10,7 +10,7 @@ import { useCallback, useMemo } from 'react';
const requiresBiometrics = [ const requiresBiometrics = [
'WalletExportRoot', 'WalletExportRoot',
'WalletXpubRoot', 'WalletXpubRoot',
'ViewEditMultisigCosignersRoot', 'ViewEditMultisigCosigners',
'ExportMultisigCoordinationSetupRoot', 'ExportMultisigCoordinationSetupRoot',
]; ];

View file

@ -1,140 +1,167 @@
import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useEffect, useCallback } from 'react';
import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
import { navigationRef } from '../NavigationService';
import { CommonActions } from '@react-navigation/native'; import { CommonActions } from '@react-navigation/native';
import * as NavigationService from '../NavigationService';
import { useStorage } from './context/useStorage';
/* /*
Hook for managing iPadOS and macOS menu actions with keyboard shortcuts. Hook for managing iPadOS and macOS menu actions with keyboard shortcuts.
Uses MenuElementsEmitter for event handling. Uses MenuElementsEmitter for event handling and navigation state.
*/ */
type MenuEventHandler = () => void; type MenuActionHandler = () => void;
// Singleton setup - initialize once at module level
const { MenuElementsEmitter } = NativeModules; const { MenuElementsEmitter } = NativeModules;
let eventEmitter: NativeEventEmitter | null = null; let eventEmitter: NativeEventEmitter | null = null;
let listenersInitialized = false;
let globalReloadTransactionsFunction: MenuEventHandler | null = null; // Registry for transaction handlers by screen ID
const handlerRegistry = new Map<string, MenuActionHandler>();
// Only create the emitter if the module exists and we're on iOS/macOS // Store subscription references for proper cleanup
let subscriptions: { remove: () => void }[] = [];
// Create a more robust emitter with error handling
try { try {
if ((Platform.OS === 'ios' || Platform.OS === 'macos') && MenuElementsEmitter) { if (Platform.OS === 'ios' && MenuElementsEmitter) {
eventEmitter = new NativeEventEmitter(MenuElementsEmitter); eventEmitter = new NativeEventEmitter(MenuElementsEmitter);
} }
} catch (error) { } catch (error) {
console.warn('[MenuElements] Failed to initialize event emitter: ', error);
eventEmitter = null; eventEmitter = null;
} }
// Empty function that does nothing - used as default /**
const noop = () => {}; * Safely navigate using multiple fallback approaches
*/
const useMenuElements = () => { function safeNavigate(routeName: string, params?: Record<string, any>): void {
const { walletsInitialized } = useStorage(); try {
const reloadTransactionsMenuActionRef = useRef<MenuEventHandler>(noop); if (navigationRef.current?.isReady()) {
// Track if listeners have been set up navigationRef.current.navigate(routeName as never, params as never);
const listenersInitialized = useRef<boolean>(false);
const listenersRef = useRef<any[]>([]);
const setReloadTransactionsMenuActionFunction = useCallback((handler: MenuEventHandler) => {
if (typeof handler !== 'function') {
return; return;
} }
reloadTransactionsMenuActionRef.current = handler; if (navigationRef.isReady()) {
globalReloadTransactionsFunction = handler; navigationRef.dispatch(
}, []); CommonActions.navigate({
name: routeName,
const clearReloadTransactionsMenuAction = useCallback(() => { params,
reloadTransactionsMenuActionRef.current = noop; }),
}, []); );
const dispatchNavigate = useCallback((routeName: string, screen?: string) => {
try {
NavigationService.dispatch(CommonActions.navigate({ name: routeName, params: screen ? { screen } : undefined }));
} catch (error) {
// Navigation failed silently
} }
}, []); } catch (error) {
console.error(`[MenuElements] Navigation error:`, error);
}
}
const eventActions = useMemo( // Cleanup event listeners to prevent memory leaks
() => ({ function cleanupListeners(): void {
openSettings: () => { if (subscriptions.length > 0) {
dispatchNavigate('Settings'); subscriptions.forEach(subscription => {
}, try {
addWallet: () => { subscription.remove();
dispatchNavigate('AddWalletRoot'); } catch (e) {
}, console.warn('[MenuElements] Error removing subscription:', e);
importWallet: () => {
dispatchNavigate('AddWalletRoot', 'ImportWallet');
},
reloadTransactions: () => {
try {
const handler = reloadTransactionsMenuActionRef.current || globalReloadTransactionsFunction || noop;
handler();
} catch (error) {
// Execution failed silently
}
},
}),
[dispatchNavigate],
);
useEffect(() => {
// Skip if emitter doesn't exist or wallets aren't initialized yet
if (!eventEmitter || !walletsInitialized) {
return;
}
if (listenersInitialized.current) {
return;
}
try {
if (listenersRef.current.length > 0) {
listenersRef.current.forEach(listener => listener?.remove?.());
listenersRef.current = [];
} }
});
subscriptions = [];
listenersInitialized = false;
}
}
eventEmitter.removeAllListeners('openSettings'); function initializeListeners(): void {
eventEmitter.removeAllListeners('addWalletMenuAction'); if (!eventEmitter || listenersInitialized) return;
eventEmitter.removeAllListeners('importWalletMenuAction');
eventEmitter.removeAllListeners('reloadTransactionsMenuAction');
} catch (error) {
// Error cleanup silently ignored
}
try { cleanupListeners();
const listeners = [
eventEmitter.addListener('openSettings', eventActions.openSettings),
eventEmitter.addListener('addWalletMenuAction', eventActions.addWallet),
eventEmitter.addListener('importWalletMenuAction', eventActions.importWallet),
eventEmitter.addListener('reloadTransactionsMenuAction', eventActions.reloadTransactions),
];
listenersRef.current = listeners; // Navigation actions
listenersInitialized.current = true; const globalActions = {
} catch (error) { navigateToSettings: (): void => {
// Listener setup failed silently safeNavigate('Settings');
} },
navigateToAddWallet: (): void => {
safeNavigate('AddWalletRoot');
},
navigateToImportWallet: (): void => {
safeNavigate('AddWalletRoot', { screen: 'ImportWallet' });
},
executeReloadTransactions: (): void => {
const currentRoute = navigationRef.current?.getCurrentRoute();
if (!currentRoute) return;
const screenName = currentRoute.name;
const params = (currentRoute.params as { walletID?: string }) || {};
const walletID = params.walletID;
const specificKey = walletID ? `${screenName}-${walletID}` : null;
const specificHandler = specificKey ? handlerRegistry.get(specificKey) : undefined;
const genericHandler = handlerRegistry.get(screenName);
const handler = specificHandler || genericHandler;
if (typeof handler === 'function') {
handler();
}
},
};
try {
subscriptions.push(eventEmitter.addListener('openSettings', globalActions.navigateToSettings));
subscriptions.push(eventEmitter.addListener('addWalletMenuAction', globalActions.navigateToAddWallet));
subscriptions.push(eventEmitter.addListener('importWalletMenuAction', globalActions.navigateToImportWallet));
subscriptions.push(eventEmitter.addListener('reloadTransactionsMenuAction', globalActions.executeReloadTransactions));
} catch (error) {
console.error('[MenuElements] Error setting up event listeners:', error);
}
listenersInitialized = true;
}
interface MenuElementsHook {
registerTransactionsHandler: (handler: MenuActionHandler, screenKey?: string) => boolean;
unregisterTransactionsHandler: (screenKey: string) => void;
isMenuElementsSupported: boolean;
}
const mountedComponents = new Set<string>();
const useMenuElements = (): MenuElementsHook => {
useEffect(() => {
initializeListeners();
const unsubscribe = navigationRef.addListener('state', () => {});
return () => { return () => {
try { unsubscribe();
listenersRef.current.forEach(listener => {
if (listener && typeof listener.remove === 'function') {
listener.remove();
}
});
listenersRef.current = [];
listenersInitialized.current = false;
} catch (error) {
// Cleanup error silently ignored
}
}; };
}, [walletsInitialized, eventActions]); }, []);
const registerTransactionsHandler = useCallback((handler: MenuActionHandler, screenKey?: string): boolean => {
if (typeof handler !== 'function') return false;
const key = screenKey || navigationRef.current?.getCurrentRoute()?.name;
if (!key) return false;
mountedComponents.add(key);
handlerRegistry.set(key, handler);
return true;
}, []);
const unregisterTransactionsHandler = useCallback((screenKey: string): void => {
if (!screenKey) return;
handlerRegistry.delete(screenKey);
mountedComponents.delete(screenKey);
}, []);
return { return {
setReloadTransactionsMenuActionFunction, registerTransactionsHandler,
clearReloadTransactionsMenuAction, unregisterTransactionsHandler,
isMenuElementsSupported: !!eventEmitter, isMenuElementsSupported: !!eventEmitter,
}; };
}; };

View file

@ -1,8 +1,28 @@
const useMenuElements = () => { import { useCallback } from 'react';
type MenuActionHandler = () => void;
interface MenuElementsHook {
registerTransactionsHandler: (handler: MenuActionHandler, screenKey?: string) => boolean;
unregisterTransactionsHandler: (screenKey: string) => void;
isMenuElementsSupported: boolean;
}
// Default implementation for platforms other than iOS
const useMenuElements = (): MenuElementsHook => {
const registerTransactionsHandler = useCallback((_handler: MenuActionHandler, _screenKey?: string): boolean => {
// Non-functional stub for non-iOS platforms
return false;
}, []);
const unregisterTransactionsHandler = useCallback((_screenKey: string): void => {
// No-op for non-supported platforms
}, []);
return { return {
setReloadTransactionsMenuActionFunction: (_func: any) => {}, registerTransactionsHandler,
clearReloadTransactionsMenuAction: () => {}, unregisterTransactionsHandler,
isMenuElementsSupported: true, isMenuElementsSupported: false, // Not supported on platforms other than iOS
}; };
}; };

View file

@ -1455,7 +1455,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703157999; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
@ -1483,7 +1483,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.1.3; MARKETING_VERSION = 7.1.5;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
@ -1518,7 +1518,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703157999; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
@ -1541,7 +1541,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.1.3; MARKETING_VERSION = 7.1.5;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
@ -1577,7 +1577,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703157999; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
@ -1590,7 +1590,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.1.3; MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -1620,7 +1620,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1703157999; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
@ -1633,7 +1633,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.1.3; MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.Stickers; PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.Stickers;
@ -1664,7 +1664,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703157999; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
@ -1683,7 +1683,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.1.3; MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -1720,7 +1720,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1703157999; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
@ -1739,7 +1739,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.1.3; MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.MarketWidget; PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.MarketWidget;
@ -1907,7 +1907,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703157999; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
@ -1927,7 +1927,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.1.3; MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -1960,7 +1960,7 @@
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1703157999; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
@ -1980,7 +1980,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.1.3; MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch.extension; PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch.extension;
@ -2012,7 +2012,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703157999; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
@ -2026,7 +2026,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.1.3; MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -2061,7 +2061,7 @@
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1703157999; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
@ -2075,7 +2075,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.1.3; MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch; PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch;

View file

@ -257,11 +257,13 @@
// Safely access the MenuElementsEmitter // Safely access the MenuElementsEmitter
MenuElementsEmitter *emitter = [MenuElementsEmitter shared]; MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
if (emitter) { if (emitter) {
NSLog(@"[MenuElements] AppDelegate: openSettings called, calling emitter");
// Force on main thread for consistency
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[emitter openSettings]; [emitter openSettings];
}); });
} else { } else {
NSLog(@"MenuElementsEmitter not available"); NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for openSettings");
} }
} }
@ -269,11 +271,13 @@
// Safely access the MenuElementsEmitter // Safely access the MenuElementsEmitter
MenuElementsEmitter *emitter = [MenuElementsEmitter shared]; MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
if (emitter) { if (emitter) {
NSLog(@"[MenuElements] AppDelegate: addWalletAction called, calling emitter");
// Force on main thread for consistency
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[emitter addWalletMenuAction]; [emitter addWalletMenuAction];
}); });
} else { } else {
NSLog(@"MenuElementsEmitter not available"); NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for addWalletAction");
} }
} }
@ -281,11 +285,13 @@
// Safely access the MenuElementsEmitter // Safely access the MenuElementsEmitter
MenuElementsEmitter *emitter = [MenuElementsEmitter shared]; MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
if (emitter) { if (emitter) {
NSLog(@"[MenuElements] AppDelegate: importWalletAction called, calling emitter");
// Force on main thread for consistency
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[emitter importWalletMenuAction]; [emitter importWalletMenuAction];
}); });
} else { } else {
NSLog(@"MenuElementsEmitter not available"); NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for importWalletAction");
} }
} }
@ -293,11 +299,13 @@
// Safely access the MenuElementsEmitter // Safely access the MenuElementsEmitter
MenuElementsEmitter *emitter = [MenuElementsEmitter shared]; MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
if (emitter) { if (emitter) {
NSLog(@"[MenuElements] AppDelegate: reloadTransactionsAction called, calling emitter");
// Force on main thread for consistency
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[emitter reloadTransactionsMenuAction]; [emitter reloadTransactionsMenuAction];
}); });
} else { } else {
NSLog(@"MenuElementsEmitter not available"); NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for reloadTransactionsAction");
} }
} }

View file

@ -3,14 +3,27 @@ import React
@objc(MenuElementsEmitter) @objc(MenuElementsEmitter)
class MenuElementsEmitter: RCTEventEmitter { class MenuElementsEmitter: RCTEventEmitter {
private static var _sharedInstance: MenuElementsEmitter? // Use a weak reference for the singleton to prevent retain cycles
private static weak var sharedInstance: MenuElementsEmitter?
// Use LRU cache with a max size to prevent unbounded growth
private var lastEventTime: [String: TimeInterval] = [:]
private let throttleInterval: TimeInterval = 0.3 // 300ms throttle
private let maxCacheSize = 10 // Limit the cache size
// Track listener state without needing constant bridge access
private var hasListeners = false private var hasListeners = false
override init() { override init() {
super.init() super.init()
MenuElementsEmitter._sharedInstance = self MenuElementsEmitter.sharedInstance = self
NSLog("[MenuElements] Swift: Initialized MenuElementsEmitter instance") NSLog("[MenuElements] MenuElementsEmitter initialized")
}
deinit {
NSLog("[MenuElements] MenuElementsEmitter deallocated")
// Ensure all event listeners are removed in deinit
self.removeAllListeners()
} }
override class func requiresMainQueueSetup() -> Bool { override class func requiresMainQueueSetup() -> Bool {
@ -22,54 +35,100 @@ class MenuElementsEmitter: RCTEventEmitter {
} }
@objc static func shared() -> MenuElementsEmitter? { @objc static func shared() -> MenuElementsEmitter? {
return _sharedInstance if sharedInstance == nil {
NSLog("[MenuElements] Warning: Attempting to use sharedInstance when it's nil")
}
return sharedInstance
} }
override func startObserving() { override func startObserving() {
hasListeners = true hasListeners = true
NSLog("[MenuElements] Swift: Started observing events") NSLog("[MenuElements] Started observing events, bridge: \(self.bridge != nil ? "available" : "unavailable")")
} }
override func stopObserving() { override func stopObserving() {
hasListeners = false hasListeners = false
NSLog("[MenuElements] Swift: Stopped observing events") NSLog("[MenuElements] Stopped observing events")
// Clear cache when stopping observation
lastEventTime.removeAll()
} }
private func safelyEmitEvent(withName name: String) { private func limitCacheSize() {
if hasListeners && self.bridge != nil { if lastEventTime.count > maxCacheSize {
NSLog("[MenuElements] Swift: Emitting event: %@", name) // Remove oldest entries if cache is too large
DispatchQueue.main.async { [weak self] in let sortedKeys = lastEventTime.sorted(by: { $0.value < $1.value })
guard let self = self else { return } for i in 0..<(lastEventTime.count - maxCacheSize) {
self.sendEvent(withName: name, body: nil) lastEventTime.removeValue(forKey: sortedKeys[i].key)
} }
} else {
NSLog("[MenuElements] Swift: Cannot emit %@ event. %@", name, !hasListeners ? "No listeners" : "Bridge not ready")
} }
} }
private func canEmitEvent(named eventName: String) -> Bool {
let now = Date().timeIntervalSince1970
if let lastTime = lastEventTime[eventName], now - lastTime < throttleInterval {
NSLog("[MenuElements] Throttling event: \(eventName)")
return false
}
lastEventTime[eventName] = now
limitCacheSize() // Keep cache size in check
let canEmit = hasListeners && bridge != nil
if (!canEmit) {
NSLog("[MenuElements] Cannot emit event: \(eventName), hasListeners: \(hasListeners), bridge: \(bridge != nil ? "available" : "unavailable")")
}
return canEmit
}
private func safelyEmitEvent(withName name: String) {
guard canEmitEvent(named: name) else { return }
NSLog("[MenuElements] Emitting event: \(name)")
// Use weak self to avoid retain cycles
DispatchQueue.main.async { [weak self] in
guard let self = self, self.bridge != nil, self.hasListeners else {
NSLog("[MenuElements] Failed to emit event: \(name) - bridge or listeners not available")
return
}
self.sendEvent(withName: name, body: nil)
NSLog("[MenuElements] Event sent: \(name)")
}
}
func removeAllListeners() {
NSLog("[MenuElements] Removing all listeners")
// Clean up resources
lastEventTime.removeAll()
}
@objc func openSettings() { @objc func openSettings() {
NSLog("[MenuElements] Swift: openSettings called") NSLog("[MenuElements] openSettings method called")
safelyEmitEvent(withName: "openSettings") safelyEmitEvent(withName: "openSettings")
} }
@objc func addWalletMenuAction() { @objc func addWalletMenuAction() {
NSLog("[MenuElements] Swift: addWalletMenuAction called") NSLog("[MenuElements] addWalletMenuAction method called")
safelyEmitEvent(withName: "addWalletMenuAction") safelyEmitEvent(withName: "addWalletMenuAction")
} }
@objc func importWalletMenuAction() { @objc func importWalletMenuAction() {
NSLog("[MenuElements] Swift: importWalletMenuAction called") NSLog("[MenuElements] importWalletMenuAction method called")
safelyEmitEvent(withName: "importWalletMenuAction") safelyEmitEvent(withName: "importWalletMenuAction")
} }
@objc func reloadTransactionsMenuAction() { @objc func reloadTransactionsMenuAction() {
NSLog("[MenuElements] Swift: reloadTransactionsMenuAction called")
safelyEmitEvent(withName: "reloadTransactionsMenuAction") safelyEmitEvent(withName: "reloadTransactionsMenuAction")
} }
override func invalidate() { override func invalidate() {
NSLog("[MenuElements] Swift: Module invalidated") NSLog("[MenuElements] Module invalidated")
MenuElementsEmitter._sharedInstance = nil if MenuElementsEmitter.sharedInstance === self {
MenuElementsEmitter.sharedInstance = nil
}
removeAllListeners()
super.invalidate() super.invalidate()
} }
} }

View file

@ -30,8 +30,8 @@ class MarketAPI {
return "https://www.bnr.ro/nbrfxrates.xml" return "https://www.bnr.ro/nbrfxrates.xml"
case "Kraken": case "Kraken":
return "https://api.kraken.com/0/public/Ticker?pair=XXBTZ\(endPointKey.uppercased())" return "https://api.kraken.com/0/public/Ticker?pair=XXBTZ\(endPointKey.uppercased())"
default: default: // CoinDesk
return "https://api.coindesk.com/v1/bpi/currentprice/\(endPointKey).json" return "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=\(endPointKey)"
} }
} }
@ -131,8 +131,14 @@ class MarketAPI {
throw CurrencyError(errorDescription: "Data formatting error for source: \(source)") throw CurrencyError(errorDescription: "Data formatting error for source: \(source)")
} }
} }
default: default: // CoinDesk
throw CurrencyError(errorDescription: "Unsupported data source \(source)") if let rateDouble = json[endPointKey] as? Double {
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
latestRateDataStore = WidgetDataStore(rate: String(rateDouble), lastUpdate: lastUpdatedString, rateDouble: rateDouble)
return latestRateDataStore
} else {
throw CurrencyError(errorDescription: "Data formatting error for source: \(source)")
}
} }
} }

View file

@ -1,3 +1,4 @@
import { fetch } from '../util/fetch';
import untypedFiatUnit from './fiatUnits.json'; import untypedFiatUnit from './fiatUnits.json';
export const FiatUnitSource = { export const FiatUnitSource = {
@ -15,7 +16,8 @@ export const FiatUnitSource = {
const handleError = (source: string, ticker: string, error: Error) => { const handleError = (source: string, ticker: string, error: Error) => {
throw new Error( throw new Error(
`Could not update rate for ${ticker} from ${source}: ${error.message}. ` + `Make sure the network you're on has access to ${source}.`, `Could not update rate for ${ticker} from ${source}\n: ${error.message}. ` +
`\nMake sure the network you're on has access to ${source}.`,
); );
}; };
@ -34,11 +36,7 @@ interface CoinbaseResponse {
} }
interface CoinDeskResponse { interface CoinDeskResponse {
bpi: { [ticker: string]: number;
[ticker: string]: {
rate_float: number;
};
};
} }
interface CoinGeckoResponse { interface CoinGeckoResponse {
@ -96,8 +94,10 @@ const RateExtractors = {
CoinDesk: async (ticker: string): Promise<number> => { CoinDesk: async (ticker: string): Promise<number> => {
try { try {
const json = (await fetchRate(`https://api.coindesk.com/v1/bpi/currentprice/${ticker}.json`)) as CoinDeskResponse; const json = (await fetchRate(
const rate = Number(json?.bpi?.[ticker]?.rate_float); `https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=${ticker.toUpperCase()}`,
)) as CoinDeskResponse;
const rate = json?.[ticker.toUpperCase()];
if (!(rate >= 0)) throw new Error('Invalid data received'); if (!(rate >= 0)) throw new Error('Invalid data received');
return rate; return rate;
} catch (error: any) { } catch (error: any) {

View file

@ -32,10 +32,8 @@ import AztecoRedeemStackRoot from './AztecoRedeemStack';
import PaymentCodesListComponent from './LazyLoadPaymentCodeStack'; import PaymentCodesListComponent from './LazyLoadPaymentCodeStack';
import LNDCreateInvoiceRoot from './LNDCreateInvoiceStack'; import LNDCreateInvoiceRoot from './LNDCreateInvoiceStack';
import ReceiveDetailsStackRoot from './ReceiveDetailsStack'; import ReceiveDetailsStackRoot from './ReceiveDetailsStack';
import ScanLndInvoiceRoot from './ScanLndInvoiceStack';
import SendDetailsStack from './SendDetailsStack'; import SendDetailsStack from './SendDetailsStack';
import SignVerifyStackRoot from './SignVerifyStack'; import SignVerifyStackRoot from './SignVerifyStack';
import ViewEditMultisigCosignersStackRoot from './ViewEditMultisigCosignersStack';
import WalletExportStack from './WalletExportStack'; import WalletExportStack from './WalletExportStack';
import WalletXpubStackRoot from './WalletXpubStack'; import WalletXpubStackRoot from './WalletXpubStack';
import SettingsButton from '../components/icons/SettingsButton'; import SettingsButton from '../components/icons/SettingsButton';
@ -66,6 +64,8 @@ import ToolsScreen from '../screen/settings/tools';
import SettingsPrivacy from '../screen/settings/SettingsPrivacy'; import SettingsPrivacy from '../screen/settings/SettingsPrivacy';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack'; import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
import { useIsLargeScreen } from '../hooks/useIsLargeScreen'; import { useIsLargeScreen } from '../hooks/useIsLargeScreen';
import ScanLNDInvoiceRoot from './ScanLNDInvoiceStack';
import { ViewEditMultisigCosignersComponent } from './LazyLoadViewEditMultisigCosignersStack';
const DetailViewStackScreensStack = () => { const DetailViewStackScreensStack = () => {
const theme = useTheme(); const theme = useTheme();
@ -327,7 +327,7 @@ const DetailViewStackScreensStack = () => {
<DetailViewStack.Screen name="AddWalletRoot" component={AddWalletStack} options={NavigationDefaultOptions} /> <DetailViewStack.Screen name="AddWalletRoot" component={AddWalletStack} options={NavigationDefaultOptions} />
<DetailViewStack.Screen name="SendDetailsRoot" component={SendDetailsStack} options={NavigationDefaultOptions} /> <DetailViewStack.Screen name="SendDetailsRoot" component={SendDetailsStack} options={NavigationDefaultOptions} />
<DetailViewStack.Screen name="LNDCreateInvoiceRoot" component={LNDCreateInvoiceRoot} options={NavigationDefaultOptions} /> <DetailViewStack.Screen name="LNDCreateInvoiceRoot" component={LNDCreateInvoiceRoot} options={NavigationDefaultOptions} />
<DetailViewStack.Screen name="ScanLndInvoiceRoot" component={ScanLndInvoiceRoot} options={NavigationDefaultOptions} /> <DetailViewStack.Screen name="ScanLNDInvoiceRoot" component={ScanLNDInvoiceRoot} options={NavigationDefaultOptions} />
<DetailViewStack.Screen name="AztecoRedeemRoot" component={AztecoRedeemStackRoot} options={NavigationDefaultOptions} /> <DetailViewStack.Screen name="AztecoRedeemRoot" component={AztecoRedeemStackRoot} options={NavigationDefaultOptions} />
{/* screens */} {/* screens */}
<DetailViewStack.Screen <DetailViewStack.Screen
@ -342,8 +342,8 @@ const DetailViewStackScreensStack = () => {
/> />
<DetailViewStack.Screen <DetailViewStack.Screen
name="ViewEditMultisigCosignersRoot" name="ViewEditMultisigCosigners"
component={ViewEditMultisigCosignersStackRoot} component={ViewEditMultisigCosignersComponent}
options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions, gestureEnabled: false, fullScreenGestureEnabled: false }} options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions, gestureEnabled: false, fullScreenGestureEnabled: false }}
initialParams={{ walletID: undefined, cosigners: undefined }} initialParams={{ walletID: undefined, cosigners: undefined }}
/> />
@ -364,7 +364,7 @@ const DetailViewStackScreensStack = () => {
options={navigationStyle({ options={navigationStyle({
headerBackVisible: false, headerBackVisible: false,
gestureEnabled: false, gestureEnabled: false,
presentation: 'containedModal', presentation: 'fullScreenModal',
title: loc.wallets.manage_title, title: loc.wallets.manage_title,
statusBarStyle: 'auto', statusBarStyle: 'auto',
})(theme)} })(theme)}

View file

@ -47,7 +47,7 @@ export type DetailViewStackParamList = {
AddWalletRoot: undefined; AddWalletRoot: undefined;
SendDetailsRoot: SendDetailsParams; SendDetailsRoot: SendDetailsParams;
LNDCreateInvoiceRoot: undefined; LNDCreateInvoiceRoot: undefined;
ScanLndInvoiceRoot: { ScanLNDInvoiceRoot: {
screen: string; screen: string;
params: { params: {
paymentHash: string; paymentHash: string;
@ -79,7 +79,7 @@ export type DetailViewStackParamList = {
ReleaseNotes: undefined; ReleaseNotes: undefined;
ToolsScreen: undefined; ToolsScreen: undefined;
SettingsPrivacy: undefined; SettingsPrivacy: undefined;
ViewEditMultisigCosignersRoot: { walletID: string; cosigners: string[] }; ViewEditMultisigCosigners: { walletID: string; cosigners: string[]; onBarScanned?: string };
WalletXpubRoot: undefined; WalletXpubRoot: undefined;
SignVerifyRoot: { SignVerifyRoot: {
screen: 'SignVerify'; screen: 'SignVerify';

View file

@ -0,0 +1,33 @@
import { TWallet } from '../class/wallets/types';
import { BitcoinUnit, Chain } from '../models/bitcoinUnits';
import { ScanQRCodeParamList } from './DetailViewStackParamList';
import { TNavigationWrapper } from './SendDetailsStackParamList';
export type LNDStackParamsList = {
ScanLNDInvoice: {
walletID: string | undefined;
uri: string | undefined;
invoice: string | undefined;
onBarScanned: string | undefined;
};
LnurlPay: {
lnurl: string;
walletID: string;
};
LnurlPaySuccess: undefined;
ScanQRCode: ScanQRCodeParamList;
SelectWallet: {
chainType?: Chain;
onWalletSelect?: (wallet: TWallet, navigationWrapper: TNavigationWrapper) => void;
availableWallets?: TWallet[];
noWalletExplanationText?: string;
onChainRequireSend?: boolean;
};
Success: {
amount?: number;
fee?: number;
invoiceDescription?: string;
amountUnit: BitcoinUnit;
txid?: string;
};
};

View file

@ -3,15 +3,15 @@ import React, { lazy, Suspense } from 'react';
import { LazyLoadingIndicator } from './LazyLoadingIndicator'; import { LazyLoadingIndicator } from './LazyLoadingIndicator';
// Lazy loading components for the navigation stack // Lazy loading components for the navigation stack
const ScanLndInvoice = lazy(() => import('../screen/lnd/scanLndInvoice')); const ScanLNDInvoice = lazy(() => import('../screen/lnd/ScanLNDInvoice'));
const SelectWallet = lazy(() => import('../screen/wallets/SelectWallet')); const SelectWallet = lazy(() => import('../screen/wallets/SelectWallet'));
const Success = lazy(() => import('../screen/send/success')); const Success = lazy(() => import('../screen/send/success'));
const LnurlPay = lazy(() => import('../screen/lnd/lnurlPay')); const LnurlPay = lazy(() => import('../screen/lnd/lnurlPay'));
const LnurlPaySuccess = lazy(() => import('../screen/lnd/lnurlPaySuccess')); const LnurlPaySuccess = lazy(() => import('../screen/lnd/lnurlPaySuccess'));
export const ScanLndInvoiceComponent = () => ( export const ScanLNDInvoiceComponent = () => (
<Suspense fallback={<LazyLoadingIndicator />}> <Suspense fallback={<LazyLoadingIndicator />}>
<ScanLndInvoice /> <ScanLNDInvoice />
</Suspense> </Suspense>
); );

View file

@ -1,20 +1,16 @@
import React, { lazy, Suspense } from 'react'; import React from 'react';
import { useStorage } from '../hooks/context/useStorage';
import DevMenu from '../components/DevMenu'; import DevMenu from '../components/DevMenu';
import MainRoot from './index'; import MainRoot from './index';
const CompanionDelegates = lazy(() => import('../components/CompanionDelegates')); import useCompanionListeners from '../hooks/useCompanionListeners';
const MasterView = () => { const MasterView = () => {
const { walletsInitialized } = useStorage(); // Initialize companion listeners only when wallets are initialized
// The hook checks walletsInitialized internally, so it won't run until ready
useCompanionListeners();
return ( return (
<> <>
<MainRoot /> <MainRoot />
{walletsInitialized && (
<Suspense>
<CompanionDelegates />
</Suspense>
)}
{__DEV__ && <DevMenu />} {__DEV__ && <DevMenu />}
</> </>
); );

View file

@ -4,24 +4,19 @@ import React from 'react';
import navigationStyle from '../components/navigationStyle'; import navigationStyle from '../components/navigationStyle';
import { useTheme } from '../components/themes'; import { useTheme } from '../components/themes';
import loc from '../loc'; import loc from '../loc';
import {
LnurlPayComponent,
LnurlPaySuccessComponent,
ScanLndInvoiceComponent,
SelectWalletComponent,
SuccessComponent,
} from './LazyLoadScanLndInvoiceStack';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack'; import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
import { LnurlPayComponent, LnurlPaySuccessComponent, ScanLNDInvoiceComponent, SuccessComponent } from './LazyLoadScanLNDInvoiceStack';
import { SelectWalletComponent } from './LazyLoadLNDCreateInvoiceStack';
const Stack = createNativeStackNavigator(); const Stack = createNativeStackNavigator();
const ScanLndInvoiceRoot = () => { const ScanLNDInvoiceRoot = () => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}> <Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
<Stack.Screen <Stack.Screen
name="ScanLndInvoice" name="ScanLNDInvoice"
component={ScanLndInvoiceComponent} component={ScanLNDInvoiceComponent}
options={navigationStyle({ headerBackVisible: false, title: loc.send.header, statusBarStyle: 'light' })(theme)} options={navigationStyle({ headerBackVisible: false, title: loc.send.header, statusBarStyle: 'light' })(theme)}
initialParams={{ uri: undefined, walletID: undefined, invoice: undefined }} initialParams={{ uri: undefined, walletID: undefined, invoice: undefined }}
/> />
@ -65,4 +60,4 @@ const ScanLndInvoiceRoot = () => {
); );
}; };
export default ScanLndInvoiceRoot; export default ScanLNDInvoiceRoot;

View file

@ -82,9 +82,11 @@ export type SendDetailsStackParamList = {
launchedBy?: string; launchedBy?: string;
}; };
Success: { Success: {
fee: number; fee?: number;
amount: number; amount: number;
amountUnit?: BitcoinUnit;
txid?: string; txid?: string;
invoiceDescription?: string;
}; };
SelectWallet: { SelectWallet: {
chainType?: Chain; chainType?: Chain;

View file

@ -1,48 +0,0 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import React from 'react';
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<ViewEditMultisigCosignersStackParamList>();
const ViewEditMultisigCosignersStackRoot = () => {
const theme = useTheme();
return (
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
<Stack.Screen
name="ViewEditMultisigCosigners"
component={ViewEditMultisigCosignersComponent}
options={navigationStyle({
headerBackVisible: false,
title: loc.multisig.manage_keys,
})(theme)}
/>
<Stack.Screen
name="ScanQRCode"
component={ScanQRCodeComponent}
options={navigationStyle({
headerShown: false,
statusBarHidden: true,
presentation: 'fullScreenModal',
headerShadowVisible: false,
})(theme)}
/>
</Stack.Navigator>
);
};
export default ViewEditMultisigCosignersStackRoot;

9
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "bluewallet", "name": "bluewallet",
"version": "7.1.3", "version": "7.1.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "bluewallet", "name": "bluewallet",
"version": "7.1.3", "version": "7.1.5",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -72,7 +72,7 @@
"react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c", "react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c",
"react-native-device-info": "14.0.4", "react-native-device-info": "14.0.4",
"react-native-document-picker": "9.3.1", "react-native-document-picker": "9.3.1",
"react-native-draglist": "github:BlueWallet/react-native-draglist#8c52785", "react-native-draglist": "github:BlueWallet/react-native-draglist#2fb0c1f",
"react-native-fs": "2.20.0", "react-native-fs": "2.20.0",
"react-native-gesture-handler": "2.23.1", "react-native-gesture-handler": "2.23.1",
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4", "react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
@ -22087,8 +22087,7 @@
}, },
"node_modules/react-native-camera-kit": { "node_modules/react-native-camera-kit": {
"version": "14.2.0", "version": "14.2.0",
"resolved": "git+ssh://git@github.com/BlueWallet/react-native-camera-kit.git#1e1921223bc9da636f9889d96b03df5f77dc7bf1", "resolved": "git+ssh://git@github.com/BlueWallet/react-native-camera-kit.git#3193427143b73a6f304198b1123b2e8b90a90862",
"integrity": "sha512-jwVriBGZai7b4TCM0JXR0xqBY0HPtu2NSQQMETTNLyTjYYqkHEK2uaWkq/GY5B93gbAnTGJ5bRyQAqfWkPjDEw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"

View file

@ -1,6 +1,6 @@
{ {
"name": "bluewallet", "name": "bluewallet",
"version": "7.1.3", "version": "7.1.5",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@ -140,7 +140,7 @@
"react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c", "react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c",
"react-native-device-info": "14.0.4", "react-native-device-info": "14.0.4",
"react-native-document-picker": "9.3.1", "react-native-document-picker": "9.3.1",
"react-native-draglist": "github:BlueWallet/react-native-draglist#8c52785", "react-native-draglist": "github:BlueWallet/react-native-draglist#2fb0c1f",
"react-native-fs": "2.20.0", "react-native-fs": "2.20.0",
"react-native-gesture-handler": "2.23.1", "react-native-gesture-handler": "2.23.1",
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4", "react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",

View file

@ -1,6 +1,6 @@
import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { ActivityIndicator, I18nManager, Keyboard, Platform, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { RouteProp, useFocusEffect, useRoute } from '@react-navigation/native';
import { ActivityIndicator, I18nManager, Keyboard, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { Icon } from '@rneui/themed'; import { Icon } from '@rneui/themed';
import { btcToSatoshi, fiatToBTC } from '../../blue_modules/currency'; import { btcToSatoshi, fiatToBTC } from '../../blue_modules/currency';
@ -19,26 +19,35 @@ import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { useStorage } from '../../hooks/context/useStorage'; import { useStorage } from '../../hooks/context/useStorage';
import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory'; import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory';
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match'; import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { LNDStackParamsList } from '../../navigation/LNDStackParamsList';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { LightningCustodianWallet } from '../../class/wallets/lightning-custodian-wallet';
import { DecodedInvoice, TWallet } from '../../class/wallets/types';
import { useKeyboard } from '../../hooks/useKeyboard';
const ScanLndInvoice = () => { type RouteProps = RouteProp<LNDStackParamsList, 'ScanLNDInvoice'>;
type NavigationProps = NativeStackNavigationProp<LNDStackParamsList, 'ScanLNDInvoice'>;
const ScanLNDInvoice = () => {
const { wallets, fetchAndSaveWalletTransactions } = useStorage(); const { wallets, fetchAndSaveWalletTransactions } = useStorage();
const { isBiometricUseCapableAndEnabled } = useBiometrics(); const { isBiometricUseCapableAndEnabled } = useBiometrics();
const { colors } = useTheme(); const { colors } = useTheme();
const route = useRoute(); const route = useRoute<RouteProps>();
const { walletID, uri, invoice } = useRoute().params; const { walletID, uri, invoice } = route.params || {};
/** @type {LightningCustodianWallet} */ const [wallet, setWallet] = useState<LightningCustodianWallet | undefined>(
const [wallet, setWallet] = useState( (wallets.find(item => item.getID() === walletID) as LightningCustodianWallet) ||
wallets.find(item => item.getID() === walletID) || wallets.find(item => item.chain === Chain.OFFCHAIN), (wallets.find(item => item.chain === Chain.OFFCHAIN) as LightningCustodianWallet),
); );
const { navigate, setParams, goBack, pop } = useNavigation(); const { navigate, setParams, goBack, pop } = useExtendedNavigation<NavigationProps>();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [renderWalletSelectionButtonHidden, setRenderWalletSelectionButtonHidden] = useState(false); const [renderWalletSelectionButtonHidden, setRenderWalletSelectionButtonHidden] = useState<boolean>(false);
const [destination, setDestination] = useState(''); const [destination, setDestination] = useState<string>('');
const [unit, setUnit] = useState(BitcoinUnit.SATS); const [unit, setUnit] = useState<BitcoinUnit>(BitcoinUnit.SATS);
const [decoded, setDecoded] = useState(); const [decoded, setDecoded] = useState<DecodedInvoice | undefined>();
const [amount, setAmount] = useState(); const [amount, setAmount] = useState<string | undefined>();
const [isAmountInitiallyEmpty, setIsAmountInitiallyEmpty] = useState(); const [isAmountInitiallyEmpty, setIsAmountInitiallyEmpty] = useState<boolean | undefined>();
const [expiresIn, setExpiresIn] = useState(); const [expiresIn, setExpiresIn] = useState<string | undefined>();
const stylesHook = StyleSheet.create({ const stylesHook = StyleSheet.create({
walletWrapLabel: { walletWrapLabel: {
color: colors.buttonAlternativeTextColor, color: colors.buttonAlternativeTextColor,
@ -54,18 +63,12 @@ const ScanLndInvoice = () => {
}, },
}); });
useEffect(() => {
const showSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', _keyboardDidShow);
const hideSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', _keyboardDidHide);
return () => {
showSubscription.remove();
hideSubscription.remove();
};
}, []);
useEffect(() => { useEffect(() => {
if (walletID && wallet?.getID() !== walletID) { if (walletID && wallet?.getID() !== walletID) {
setWallet(wallets.find(w => w.getID() === walletID)); const newWallet = wallets.find(w => w.getID() === walletID) as LightningCustodianWallet;
if (newWallet) {
setWallet(newWallet);
}
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletID]); }, [walletID]);
@ -75,7 +78,10 @@ const ScanLndInvoice = () => {
if (!wallet) { if (!wallet) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError); triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
goBack(); goBack();
setTimeout(() => presentAlert({ message: loc.wallets.no_ln_wallet_error }), 500); setTimeout(
() => presentAlert({ message: loc.wallets.no_ln_wallet_error, hapticFeedback: HapticFeedbackTypes.NotificationError }),
500,
);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [wallet]), }, [wallet]),
@ -96,66 +102,68 @@ const ScanLndInvoice = () => {
data = data.replace('LIGHTNING:', '').replace('lightning:', ''); data = data.replace('LIGHTNING:', '').replace('lightning:', '');
console.log(data); console.log(data);
let newDecoded; let newDecoded: DecodedInvoice;
try { try {
newDecoded = wallet.decodeInvoice(data); newDecoded = wallet.decodeInvoice(data);
let newExpiresIn = (newDecoded.timestamp * 1 + newDecoded.expiry * 1) * 1000; // ms const expiryTimeMs = (newDecoded.timestamp * 1 + newDecoded.expiry * 1) * 1000; // ms
if (+new Date() > newExpiresIn) { let newExpiresIn: string;
if (+new Date() > expiryTimeMs) {
newExpiresIn = loc.lnd.expired; newExpiresIn = loc.lnd.expired;
} else { } else {
const time = Math.round((newExpiresIn - +new Date()) / (60 * 1000)); const time = Math.round((expiryTimeMs - +new Date()) / (60 * 1000));
newExpiresIn = loc.formatString(loc.lnd.expiresIn, { time }); newExpiresIn = loc.formatString(loc.lnd.expiresIn, { time });
} }
Keyboard.dismiss(); Keyboard.dismiss();
setParams({ uri: undefined, invoice: data }); setParams({ uri: undefined, invoice: data });
setIsAmountInitiallyEmpty(newDecoded.num_satoshis === '0'); setIsAmountInitiallyEmpty(newDecoded.num_satoshis === 0);
setDestination(data); setDestination(data);
setIsLoading(false); setIsLoading(false);
setAmount(newDecoded.num_satoshis); setAmount(newDecoded.num_satoshis.toString());
setExpiresIn(newExpiresIn); setExpiresIn(newExpiresIn);
setDecoded(newDecoded); setDecoded(newDecoded);
} catch (Err) { } catch (Err: any) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError); triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
Keyboard.dismiss(); Keyboard.dismiss();
setParams({ uri: undefined }); setParams({ uri: undefined });
setTimeout(() => presentAlert({ message: Err.message }), 10); setTimeout(() => presentAlert({ message: Err.message, hapticFeedback: HapticFeedbackTypes.NotificationError }), 10);
setIsLoading(false); setIsLoading(false);
setAmount(); setAmount(undefined);
setDestination(); setDestination('');
setExpiresIn(); setExpiresIn(undefined);
setDecoded(); setDecoded(undefined);
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [uri]); }, [uri]);
const _keyboardDidShow = () => { const _keyboardDidShow = (): void => {
setRenderWalletSelectionButtonHidden(true); setRenderWalletSelectionButtonHidden(true);
}; };
const _keyboardDidHide = () => { const _keyboardDidHide = (): void => {
setRenderWalletSelectionButtonHidden(false); setRenderWalletSelectionButtonHidden(false);
}; };
const processInvoice = data => { useKeyboard({ onKeyboardDidShow: _keyboardDidShow, onKeyboardDidHide: _keyboardDidHide });
const processInvoice = (data: string): void => {
if (Lnurl.isLnurl(data)) return processLnurlPay(data); if (Lnurl.isLnurl(data)) return processLnurlPay(data);
if (Lnurl.isLightningAddress(data)) return processLnurlPay(data); if (Lnurl.isLightningAddress(data)) return processLnurlPay(data);
setParams({ uri: data }); setParams({ uri: data });
}; };
const processLnurlPay = data => { const processLnurlPay = (data: string): void => {
navigate('ScanLndInvoiceRoot', { navigate('LnurlPay', {
screen: 'LnurlPay', lnurl: data,
params: { walletID: walletID || wallet?.getID() || '',
lnurl: data,
walletID: walletID || wallet.getID(),
},
}); });
}; };
const pay = async () => { const pay = async () => {
if (!decoded) { if (!decoded || !wallet || !amount || !invoice) {
return null; return null;
} }
@ -167,22 +175,22 @@ const ScanLndInvoice = () => {
} }
} }
let amountSats = amount; let amountSats: number = parseInt(amount, 10);
switch (unit) { switch (unit) {
case BitcoinUnit.SATS: case BitcoinUnit.SATS:
amountSats = parseInt(amountSats, 10); // nop // amount is already in sats
break; break;
case BitcoinUnit.BTC: case BitcoinUnit.BTC:
amountSats = btcToSatoshi(amountSats); amountSats = btcToSatoshi(amount);
break; break;
case BitcoinUnit.LOCAL_CURRENCY: case BitcoinUnit.LOCAL_CURRENCY:
amountSats = btcToSatoshi(fiatToBTC(amountSats)); amountSats = btcToSatoshi(fiatToBTC(Number(amount)));
break; break;
} }
setIsLoading(true); setIsLoading(true);
const newExpiresIn = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms const expiryTimeMs = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms
if (+new Date() > newExpiresIn) { if (+new Date() > expiryTimeMs) {
setIsLoading(false); setIsLoading(false);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError); triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
return presentAlert({ message: loc.lnd.errorInvoiceExpired }); return presentAlert({ message: loc.lnd.errorInvoiceExpired });
@ -197,7 +205,7 @@ const ScanLndInvoice = () => {
try { try {
await wallet.payInvoice(invoice, amountSats); await wallet.payInvoice(invoice, amountSats);
} catch (Err) { } catch (Err: any) {
console.log(Err.message); console.log(Err.message);
setIsLoading(false); setIsLoading(false);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError); triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
@ -212,7 +220,7 @@ const ScanLndInvoice = () => {
fetchAndSaveWalletTransactions(wallet.getID()); fetchAndSaveWalletTransactions(wallet.getID());
}; };
const processTextForInvoice = text => { const processTextForInvoice = (text: string): void => {
if ( if (
(text && text.toLowerCase().startsWith('lnb')) || (text && text.toLowerCase().startsWith('lnb')) ||
text.toLowerCase().startsWith('lightning:lnb') || text.toLowerCase().startsWith('lightning:lnb') ||
@ -227,7 +235,7 @@ const ScanLndInvoice = () => {
} }
}; };
const shouldDisablePayButton = () => { const shouldDisablePayButton = (): boolean => {
if (!decoded) { if (!decoded) {
return true; return true;
} else { } else {
@ -235,16 +243,15 @@ const ScanLndInvoice = () => {
return true; return true;
} }
} }
return !(amount > 0); return !(parseInt(amount, 10) > 0);
// return decoded.num_satoshis <= 0 || isLoading || isNaN(decoded.num_satoshis);
}; };
const naviageToSelectWallet = () => { const naviageToSelectWallet = (): void => {
navigate('SelectWallet', { onWalletSelect, chainType: Chain.OFFCHAIN }); navigate('SelectWallet', { onWalletSelect, chainType: Chain.OFFCHAIN });
}; };
const renderWalletSelectionButton = () => { const renderWalletSelectionButton = (): JSX.Element | undefined => {
if (renderWalletSelectionButtonHidden) return; if (renderWalletSelectionButtonHidden || !wallet) return;
const walletLabel = wallet.getLabel(); const walletLabel = wallet.getLabel();
return ( return (
<View style={styles.walletSelectRoot}> <View style={styles.walletSelectRoot}>
@ -267,25 +274,27 @@ const ScanLndInvoice = () => {
); );
}; };
const getFees = () => { const getFees = (): string => {
const min = Math.floor(decoded.num_satoshis * 0.003); if (!decoded) return '';
const max = Math.floor(decoded.num_satoshis * 0.01) + 1; const num_satoshis = parseInt(decoded.num_satoshis.toString(), 10);
const min = Math.floor(num_satoshis * 0.003);
const max = Math.floor(num_satoshis * 0.01) + 1;
return `${min} ${BitcoinUnit.SATS} - ${max} ${BitcoinUnit.SATS}`; return `${min} ${BitcoinUnit.SATS} - ${max} ${BitcoinUnit.SATS}`;
}; };
const onBlur = () => { const onBlur = (): void => {
processTextForInvoice(destination); processTextForInvoice(destination);
}; };
const onWalletSelect = selectedWallet => { const onWalletSelect = (selectedWallet: TWallet): void => {
setParams({ walletID: selectedWallet.getID() }); setParams({ walletID: selectedWallet.getID() });
pop(); pop();
}; };
const onBarScanned = useCallback( const onBarScanned = useCallback(
value => { (value: string): void => {
if (!value) return; if (!value) return;
DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => { DeeplinkSchemaMatch.navigationRouteFor({ url: value }, (completionValue: any) => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
navigate(...completionValue); navigate(...completionValue);
}); });
@ -301,6 +310,12 @@ const ScanLndInvoice = () => {
} }
}, [navigate, onBarScanned, route.params?.onBarScanned, setParams]); }, [navigate, onBarScanned, route.params?.onBarScanned, setParams]);
const onChangeText = (text: string): void => {
const trimmedText = text.trim();
setDestination(trimmedText);
processTextForInvoice(trimmedText);
};
if (wallet === undefined || !wallet) { if (wallet === undefined || !wallet) {
return ( return (
<View style={[styles.loadingIndicator, stylesHook.root]}> <View style={[styles.loadingIndicator, stylesHook.root]}>
@ -334,11 +349,7 @@ const ScanLndInvoice = () => {
<BlueCard> <BlueCard>
<AddressInput <AddressInput
onChangeText={text => { onChangeText={onChangeText}
text = text.trim();
setDestination(text);
}}
onBarScanned={data => processTextForInvoice(data.data)}
address={destination} address={destination}
isLoading={isLoading} isLoading={isLoading}
placeholder={loc.lnd.placeholder} placeholder={loc.lnd.placeholder}
@ -381,7 +392,7 @@ const ScanLndInvoice = () => {
); );
}; };
export default ScanLndInvoice; export default ScanLNDInvoice;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
walletSelectRoot: { walletSelectRoot: {

View file

@ -109,7 +109,7 @@ const LNDCreateInvoice = () => {
if (reply.tag === Lnurl.TAG_PAY_REQUEST) { 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 // 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) // invoices (including through lnurl-withdraw)
navigate('ScanLndInvoiceRoot', { navigate('ScanLNDInvoiceRoot', {
screen: 'LnurlPay', screen: 'LnurlPay',
params: { params: {
lnurl: data, lnurl: data,

View file

@ -149,7 +149,7 @@ const LnurlPay: React.FC = () => {
await _LN.storeSuccess(decoded.payment_hash, wallet.last_paid_invoice_result.payment_preimage); await _LN.storeSuccess(decoded.payment_hash, wallet.last_paid_invoice_result.payment_preimage);
} }
navigate('ScanLndInvoiceRoot', { navigate('ScanLNDInvoiceRoot', {
screen: 'LnurlPaySuccess', screen: 'LnurlPaySuccess',
params: { params: {
paymentHash: decoded.payment_hash, paymentHash: decoded.payment_hash,

View file

@ -109,7 +109,7 @@ const LnurlPaySuccess: React.FC = () => {
{repeatable ? ( {repeatable ? (
<Button <Button
onPress={() => { onPress={() => {
navigate('ScanLndInvoiceRoot', { navigate('ScanLNDInvoiceRoot', {
screen: 'LnurlPay', screen: 'LnurlPay',
params: { params: {
// @ts-ignore fixme // @ts-ignore fixme

View file

@ -1,7 +1,7 @@
import { RouteProp, StackActions, useFocusEffect, useIsFocused, useRoute } from '@react-navigation/native'; import { RouteProp, StackActions, useIsFocused, useRoute } from '@react-navigation/native';
import * as bitcoin from 'bitcoinjs-lib'; import * as bitcoin from 'bitcoinjs-lib';
import createHash from 'create-hash'; import createHash from 'create-hash';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Platform, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native'; import { Platform, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
import Base43 from '../../blue_modules/base43'; import Base43 from '../../blue_modules/base43';
import * as fs from '../../blue_modules/fs'; import * as fs from '../../blue_modules/fs';
@ -12,7 +12,6 @@ import Button from '../../components/Button';
import { useTheme } from '../../components/themes'; import { useTheme } from '../../components/themes';
import { isCameraAuthorizationStatusGranted } from '../../helpers/scan-qr'; import { isCameraAuthorizationStatusGranted } from '../../helpers/scan-qr';
import loc from '../../loc'; import loc from '../../loc';
import { useSettings } from '../../hooks/context/useSettings';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import CameraScreen from '../../components/CameraScreen'; import CameraScreen from '../../components/CameraScreen';
import SafeArea from '../../components/SafeArea'; import SafeArea from '../../components/SafeArea';
@ -57,7 +56,6 @@ const styles = StyleSheet.create({
const ScanQRCode = () => { const ScanQRCode = () => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { setIsDrawerShouldHide } = useSettings();
const navigation = useExtendedNavigation(); const navigation = useExtendedNavigation();
const route = useRoute<RouteProps>(); const route = useRoute<RouteProps>();
const navigationState = navigation.getState(); const navigationState = navigation.getState();
@ -96,16 +94,6 @@ const ScanQRCode = () => {
return createHash('sha256').update(s).digest().toString('hex'); return createHash('sha256').update(s).digest().toString('hex');
}; };
useFocusEffect(
useCallback(() => {
setIsDrawerShouldHide(true);
return () => {
setIsDrawerShouldHide(false);
};
}, [setIsDrawerShouldHide]),
);
const _onReadUniformResourceV2 = (part: string) => { const _onReadUniformResourceV2 = (part: string) => {
if (!decoder) decoder = new BlueURDecoder(); if (!decoder) decoder = new BlueURDecoder();
try { try {

View file

@ -118,7 +118,8 @@ const Currency: React.FC = () => {
<FlatList <FlatList
contentInsetAdjustmentBehavior="automatic" contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustContentInsets automaticallyAdjustContentInsets
keyExtractor={(_item, index) => `${index}`} automaticallyAdjustKeyboardInsets
keyExtractor={item => item.endPointKey}
data={data} data={data}
initialNumToRender={30} initialNumToRender={30}
renderItem={renderItem} renderItem={renderItem}

View file

@ -515,7 +515,6 @@ const ElectrumSettings: React.FC = () => {
onChangeText={text => setHost(text.trim())} onChangeText={text => setHost(text.trim())}
editable={!isLoading} editable={!isLoading}
keyboardType="default" keyboardType="default"
skipValidation
onBlur={() => setIsAndroidAddressKeyboardVisible(false)} onBlur={() => setIsAndroidAddressKeyboardVisible(false)}
onFocus={() => setIsAndroidAddressKeyboardVisible(true)} onFocus={() => setIsAndroidAddressKeyboardVisible(true)}
inputAccessoryViewID={DoneAndDismissKeyboardInputAccessoryViewID} inputAccessoryViewID={DoneAndDismissKeyboardInputAccessoryViewID}
@ -611,9 +610,7 @@ const ElectrumSettings: React.FC = () => {
onValueChange: onElectrumConnectionEnabledSwitchChange, onValueChange: onElectrumConnectionEnabledSwitchChange,
value: isElectrumDisabled, value: isElectrumDisabled,
testID: 'ElectrumConnectionEnabledSwitch', testID: 'ElectrumConnectionEnabledSwitch',
disabled: isLoading,
}} }}
disabled={isLoading}
bottomDivider={false} bottomDivider={false}
subtitle={loc.settings.electrum_offline_description} subtitle={loc.settings.electrum_offline_description}
/> />

View file

@ -7,9 +7,11 @@ import {
Alert, Alert,
I18nManager, I18nManager,
Animated, Animated,
LayoutAnimation,
FlatList, FlatList,
ActivityIndicator, ActivityIndicator,
LayoutAnimation,
UIManager,
Platform,
} from 'react-native'; } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { useFocusEffect, usePreventRemove } from '@react-navigation/native'; import { useFocusEffect, usePreventRemove } from '@react-navigation/native';
@ -99,19 +101,21 @@ type Action =
interface State { interface State {
searchQuery: string; searchQuery: string;
isSearchFocused: boolean; isSearchFocused: boolean;
order: Item[]; originalWalletsOrder: Item[];
tempOrder: Item[]; currentWalletsOrder: Item[];
wallets: TWallet[]; availableWallets: TWallet[];
txMetadata: TTXMetadata; txMetadata: TTXMetadata;
initialWalletsBackup: TWallet[];
} }
const initialState: State = { const initialState: State = {
searchQuery: '', searchQuery: '',
isSearchFocused: false, isSearchFocused: false,
order: [], originalWalletsOrder: [],
tempOrder: [], currentWalletsOrder: [],
wallets: [], availableWallets: [],
txMetadata: {}, txMetadata: {},
initialWalletsBackup: [],
}; };
const deepCopyWallets = (wallets: TWallet[]): TWallet[] => { const deepCopyWallets = (wallets: TWallet[]): TWallet[] => {
@ -131,21 +135,22 @@ const reducer = (state: State, action: Action): State => {
})); }));
return { return {
...state, ...state,
wallets: action.payload.wallets, availableWallets: action.payload.wallets,
txMetadata: action.payload.txMetadata, txMetadata: action.payload.txMetadata,
order: initialWalletsOrder, originalWalletsOrder: initialWalletsOrder,
tempOrder: initialWalletsOrder, currentWalletsOrder: initialWalletsOrder,
initialWalletsBackup: deepCopyWallets(action.payload.wallets),
}; };
} }
case SET_FILTERED_ORDER: { case SET_FILTERED_ORDER: {
const query = action.payload.toLowerCase(); const query = action.payload.toLowerCase();
const filteredWallets = state.wallets const filteredWallets = state.availableWallets
.filter(wallet => wallet.getLabel()?.toLowerCase().includes(query)) .filter(wallet => wallet.getLabel()?.toLowerCase().includes(query))
.map(wallet => ({ type: ItemType.WalletSection, data: wallet })); .map(wallet => ({ type: ItemType.WalletSection, data: wallet }));
const filteredTxMetadata = Object.entries(state.txMetadata).filter(([_, tx]) => tx.memo?.toLowerCase().includes(query)); const filteredTxMetadata = Object.entries(state.txMetadata).filter(([_, tx]) => tx.memo?.toLowerCase().includes(query));
const filteredTransactions = state.wallets.flatMap(wallet => const filteredTransactions = state.availableWallets.flatMap(wallet =>
wallet wallet
.getTransactions() .getTransactions()
.filter((tx: Transaction) => .filter((tx: Transaction) =>
@ -158,14 +163,16 @@ const reducer = (state: State, action: Action): State => {
return { return {
...state, ...state,
tempOrder: filteredOrder, currentWalletsOrder: filteredOrder,
}; };
} }
case SAVE_CHANGES: { case SAVE_CHANGES: {
const savedWallets = deepCopyWallets(action.payload);
return { return {
...state, ...state,
wallets: deepCopyWallets(action.payload), availableWallets: savedWallets,
tempOrder: state.tempOrder.map(item => initialWalletsBackup: savedWallets,
currentWalletsOrder: state.currentWalletsOrder.map(item =>
item.type === ItemType.WalletSection item.type === ItemType.WalletSection
? { ...item, data: action.payload.find(wallet => wallet.getID() === item.data.getID())! } ? { ...item, data: action.payload.find(wallet => wallet.getID() === item.data.getID())! }
: item, : item,
@ -173,13 +180,15 @@ const reducer = (state: State, action: Action): State => {
}; };
} }
case SET_TEMP_ORDER: { case SET_TEMP_ORDER: {
return { ...state, tempOrder: action.payload }; return { ...state, currentWalletsOrder: action.payload };
} }
case REMOVE_WALLET: { case REMOVE_WALLET: {
const updatedOrder = state.tempOrder.filter(item => item.type !== ItemType.WalletSection || item.data.getID() !== action.payload); const updatedOrder = state.currentWalletsOrder.filter(
item => item.type !== ItemType.WalletSection || item.data.getID() !== action.payload,
);
return { return {
...state, ...state,
tempOrder: updatedOrder, currentWalletsOrder: updatedOrder,
}; };
} }
default: default:
@ -187,12 +196,16 @@ const reducer = (state: State, action: Action): State => {
} }
}; };
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
const ManageWallets: React.FC = () => { const ManageWallets: React.FC = () => {
const { colors, closeImage } = useTheme(); const { colors, closeImage } = useTheme();
const { wallets: storedWallets, setWalletsWithNewOrder, txMetadata, handleWalletDeletion } = useStorage(); const { wallets: persistedWallets, setWalletsWithNewOrder, txMetadata, handleWalletDeletion } = useStorage();
const { setIsDrawerShouldHide } = useSettings(); const { setIsDrawerShouldHide } = useSettings();
const walletsRef = useRef<TWallet[]>(deepCopyWallets(storedWallets)); // Create a deep copy of wallets for the DraggableFlatList const initialWalletsRef = useRef<TWallet[]>(deepCopyWallets(persistedWallets));
const { navigate, setOptions, goBack } = useExtendedNavigation(); const { navigate, setOptions, goBack, dispatch: navigationDispatch } = useExtendedNavigation();
const [state, dispatch] = useReducer(reducer, initialState); const [state, dispatch] = useReducer(reducer, initialState);
const debouncedSearchQuery = useDebounce(state.searchQuery, 300); const debouncedSearchQuery = useDebounce(state.searchQuery, 300);
const bounceAnim = useBounceAnimation(state.searchQuery); const bounceAnim = useBounceAnimation(state.searchQuery);
@ -204,76 +217,123 @@ const ManageWallets: React.FC = () => {
color: colors.foregroundColor, color: colors.foregroundColor,
}, },
}; };
const [data, setData] = useState(state.tempOrder); const [uiData, setUiData] = useState(state.currentWalletsOrder);
const listRef = useRef<FlatList<Item> | null>(null); const listRef = useRef<FlatList<Item> | null>(null);
const [saveInProgress, setSaveInProgress] = useState(false);
useEffect(() => { useEffect(() => {
setData(state.tempOrder); setUiData(state.currentWalletsOrder);
}, [state.tempOrder]); }, [state.currentWalletsOrder]);
useEffect(() => { useEffect(() => {
dispatch({ type: SET_INITIAL_ORDER, payload: { wallets: walletsRef.current, txMetadata } }); dispatch({ type: SET_INITIAL_ORDER, payload: { wallets: initialWalletsRef.current, txMetadata } });
}, [txMetadata]); }, [txMetadata]);
useEffect(() => { useEffect(() => {
if (debouncedSearchQuery) { if (debouncedSearchQuery) {
dispatch({ type: SET_FILTERED_ORDER, payload: debouncedSearchQuery }); dispatch({ type: SET_FILTERED_ORDER, payload: debouncedSearchQuery });
} else { } else {
dispatch({ type: SET_TEMP_ORDER, payload: state.order }); dispatch({ type: SET_TEMP_ORDER, payload: state.originalWalletsOrder });
} }
}, [debouncedSearchQuery, state.order]); }, [debouncedSearchQuery, state.originalWalletsOrder]);
const hasUnsavedChanges = useMemo(() => { const hasUnsavedChanges = useMemo(() => {
return JSON.stringify(walletsRef.current) !== JSON.stringify(state.tempOrder.map(item => item.data)); const currentWalletIds = state.currentWalletsOrder
}, [state.tempOrder]); .filter((item): item is WalletItem => item.type === ItemType.WalletSection)
.map(item => item.data.getID());
usePreventRemove(hasUnsavedChanges, async () => { const originalWalletIds = state.initialWalletsBackup.map(wallet => wallet.getID());
await new Promise<void>(resolve => {
Alert.alert(loc._.discard_changes, loc._.discard_changes_explain, [ if (currentWalletIds.length !== originalWalletIds.length) {
{ text: loc._.cancel, style: 'cancel', onPress: () => resolve() }, return true;
{ text: loc._.ok, style: 'default', onPress: () => resolve() }, }
]);
}); for (let i = 0; i < currentWalletIds.length; i++) {
if (currentWalletIds[i] !== originalWalletIds[i]) {
return true;
}
}
const modifiedWallets = state.currentWalletsOrder
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
.map(item => item.data);
for (const modifiedWallet of modifiedWallets) {
const originalWallet = state.initialWalletsBackup.find(w => w.getID() === modifiedWallet.getID());
if (originalWallet && originalWallet.hideBalance !== modifiedWallet.hideBalance) {
return true;
}
}
return false;
}, [state.currentWalletsOrder, state.initialWalletsBackup]);
usePreventRemove(hasUnsavedChanges && !saveInProgress, ({ data: preventRemoveData }) => {
Alert.alert(loc._.discard_changes, loc._.discard_changes_explain, [
{ text: loc._.cancel, style: 'cancel' },
{
text: loc._.ok,
style: 'destructive',
onPress: () => navigationDispatch(preventRemoveData.action),
},
]);
}); });
useEffect(() => {
if (saveInProgress) {
goBack();
setSaveInProgress(false);
}
}, [saveInProgress, goBack]);
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
if (state.searchQuery.length === 0 && !state.isSearchFocused) { if (state.searchQuery.length === 0 && !state.isSearchFocused) {
const newWalletOrder = state.tempOrder const reorderedWallets = state.currentWalletsOrder
.filter((item): item is WalletItem => item.type === ItemType.WalletSection) .filter((item): item is WalletItem => item.type === ItemType.WalletSection)
.map(item => item.data); .map(item => item.data);
setWalletsWithNewOrder(newWalletOrder); const walletsToDelete = state.initialWalletsBackup.filter(
originalWallet => !reorderedWallets.some(wallet => wallet.getID() === originalWallet.getID()),
);
dispatch({ type: SAVE_CHANGES, payload: newWalletOrder }); setWalletsWithNewOrder(reorderedWallets);
dispatch({ type: SAVE_CHANGES, payload: reorderedWallets });
initialWalletsRef.current = deepCopyWallets(reorderedWallets);
walletsRef.current = deepCopyWallets(newWalletOrder); walletsToDelete.forEach(wallet => {
handleWalletDeletion(wallet.getID());
state.tempOrder.forEach(item => {
if (item.type === ItemType.WalletSection && !newWalletOrder.some(wallet => wallet.getID() === item.data.getID())) {
handleWalletDeletion(item.data.getID());
}
}); });
goBack(); setSaveInProgress(true);
} else { } else {
dispatch({ type: SET_SEARCH_QUERY, payload: '' }); dispatch({ type: SET_SEARCH_QUERY, payload: '' });
dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false }); dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false });
} }
}, [goBack, setWalletsWithNewOrder, state.searchQuery, state.isSearchFocused, state.tempOrder, handleWalletDeletion]); }, [
setWalletsWithNewOrder,
state.searchQuery,
state.isSearchFocused,
state.currentWalletsOrder,
state.initialWalletsBackup,
handleWalletDeletion,
]);
const buttonOpacity = useMemo(() => ({ opacity: saveInProgress ? 0.5 : 1 }), [saveInProgress]);
const HeaderLeftButton = useMemo( const HeaderLeftButton = useMemo(
() => ( () => (
<TouchableOpacity <TouchableOpacity
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={loc._.close} accessibilityLabel={loc._.close}
style={styles.button} style={[styles.button, buttonOpacity]}
onPress={goBack} onPress={goBack}
disabled={saveInProgress}
testID="NavigationCloseButton" testID="NavigationCloseButton"
> >
<Image source={closeImage} /> <Image source={closeImage} />
</TouchableOpacity> </TouchableOpacity>
), ),
[goBack, closeImage], [buttonOpacity, goBack, saveInProgress, closeImage],
); );
const SaveButton = useMemo( const SaveButton = useMemo(
@ -290,7 +350,6 @@ const ManageWallets: React.FC = () => {
onBlur: () => dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false }), onBlur: () => dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false }),
placeholder: loc.wallets.manage_wallets_search_placeholder, placeholder: loc.wallets.manage_wallets_search_placeholder,
}; };
setOptions({ setOptions({
headerLeft: () => HeaderLeftButton, headerLeft: () => HeaderLeftButton,
headerRight: () => SaveButton, headerRight: () => SaveButton,
@ -329,19 +388,30 @@ const ManageWallets: React.FC = () => {
[bounceAnim], [bounceAnim],
); );
const handleDeleteWallet = useCallback( const handleDeleteWallet = useCallback(async (wallet: TWallet) => {
async (wallet: TWallet) => { LayoutAnimation.configureNext({
const deletionSucceeded = await handleWalletDeletion(wallet.getID()); duration: 300,
if (deletionSucceeded) { create: {
dispatch({ type: REMOVE_WALLET, payload: wallet.getID() }); type: LayoutAnimation.Types.easeInEaseOut,
} property: LayoutAnimation.Properties.opacity,
}, },
[handleWalletDeletion], update: {
); type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity,
},
delete: {
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity,
duration: 200,
},
});
dispatch({ type: REMOVE_WALLET, payload: wallet.getID() });
}, []);
const handleToggleHideBalance = useCallback( const handleToggleHideBalance = useCallback(
(wallet: TWallet) => { (wallet: TWallet) => {
const updatedOrder = state.tempOrder.map(item => { const updatedOrder = state.currentWalletsOrder.map(item => {
if (item.type === ItemType.WalletSection && item.data.getID() === wallet.getID()) { if (item.type === ItemType.WalletSection && item.data.getID() === wallet.getID()) {
item.data.hideBalance = !item.data.hideBalance; item.data.hideBalance = !item.data.hideBalance;
return { return {
@ -351,11 +421,10 @@ const ManageWallets: React.FC = () => {
} }
return item; return item;
}); });
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
dispatch({ type: SET_TEMP_ORDER, payload: updatedOrder }); dispatch({ type: SET_TEMP_ORDER, payload: updatedOrder });
}, },
[state.tempOrder], [state.currentWalletsOrder],
); );
const navigateToWallet = useCallback( const navigateToWallet = useCallback(
@ -373,13 +442,19 @@ const ManageWallets: React.FC = () => {
const renderItem = useCallback( const renderItem = useCallback(
(info: DragListRenderItemInfo<Item>) => { (info: DragListRenderItemInfo<Item>) => {
const { item, onDragStart, isActive } = info; const { item, onDragStart, isActive } = info;
const compatibleState = {
wallets: state.availableWallets,
searchQuery: state.searchQuery,
};
return ( return (
<ManageWalletsListItem <ManageWalletsListItem
item={item} item={item}
onPressIn={undefined} onPressIn={undefined}
onPressOut={undefined} onPressOut={undefined}
isDraggingDisabled={state.searchQuery.length > 0 || state.isSearchFocused} isDraggingDisabled={state.searchQuery.length > 0 || state.isSearchFocused}
state={state} state={compatibleState}
navigateToWallet={navigateToWallet} navigateToWallet={navigateToWallet}
renderHighlightedText={renderHighlightedText} renderHighlightedText={renderHighlightedText}
handleDeleteWallet={handleDeleteWallet} handleDeleteWallet={handleDeleteWallet}
@ -389,33 +464,33 @@ const ManageWallets: React.FC = () => {
/> />
); );
}, },
[state, navigateToWallet, renderHighlightedText, handleDeleteWallet, handleToggleHideBalance], [
state.availableWallets,
state.searchQuery,
state.isSearchFocused,
navigateToWallet,
renderHighlightedText,
handleDeleteWallet,
handleToggleHideBalance,
],
); );
const onReordered = useCallback( const onReordered = useCallback(
(fromIndex: number, toIndex: number) => { (fromIndex: number, toIndex: number) => {
const copy = [...state.order]; const updatedOrder = [...state.currentWalletsOrder];
const removed = copy.splice(fromIndex, 1); const removed = updatedOrder.splice(fromIndex, 1);
copy.splice(toIndex, 0, removed[0]); updatedOrder.splice(toIndex, 0, removed[0]);
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); dispatch({ type: SET_TEMP_ORDER, payload: updatedOrder });
dispatch({ type: SET_TEMP_ORDER, payload: copy });
dispatch({
type: SET_INITIAL_ORDER,
payload: {
wallets: copy.filter(item => item.type === ItemType.WalletSection).map(item => item.data as TWallet),
txMetadata: state.txMetadata,
},
});
}, },
[state.order, state.txMetadata], [state.currentWalletsOrder],
); );
const keyExtractor = useCallback((item: Item, index: number) => index.toString(), []); const keyExtractor = useCallback((item: Item, index: number) => index.toString(), []);
const renderHeader = useMemo(() => { const renderHeader = useMemo(() => {
if (!state.searchQuery) return null; if (!state.searchQuery) return null;
const hasWallets = state.wallets.length > 0; const hasWallets = state.availableWallets.length > 0;
const filteredTxMetadata = Object.entries(state.txMetadata).filter(([_, tx]) => const filteredTxMetadata = Object.entries(state.txMetadata).filter(([_, tx]) =>
tx.memo?.toLowerCase().includes(state.searchQuery.toLowerCase()), tx.memo?.toLowerCase().includes(state.searchQuery.toLowerCase()),
); );
@ -425,7 +500,7 @@ const ManageWallets: React.FC = () => {
!hasWallets && !hasWallets &&
!hasTransactions && <Text style={[styles.noResultsText, stylesHook.noResultsText]}>{loc.wallets.no_results_found}</Text> !hasTransactions && <Text style={[styles.noResultsText, stylesHook.noResultsText]}>{loc.wallets.no_results_found}</Text>
); );
}, [state.searchQuery, state.wallets.length, state.txMetadata, stylesHook.noResultsText]); }, [state.searchQuery, state.availableWallets.length, state.txMetadata, stylesHook.noResultsText]);
return ( return (
<Suspense fallback={<ActivityIndicator size="large" color={colors.brandingColor} />}> <Suspense fallback={<ActivityIndicator size="large" color={colors.brandingColor} />}>
@ -437,7 +512,7 @@ const ManageWallets: React.FC = () => {
automaticallyAdjustKeyboardInsets automaticallyAdjustKeyboardInsets
automaticallyAdjustsScrollIndicatorInsets automaticallyAdjustsScrollIndicatorInsets
contentInsetAdjustmentBehavior="automatic" contentInsetAdjustmentBehavior="automatic"
data={data} data={uiData}
containerStyle={[{ backgroundColor: colors.background }, styles.root]} containerStyle={[{ backgroundColor: colors.background }, styles.root]}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
onReordered={onReordered} onReordered={onReordered}
@ -460,35 +535,37 @@ const styles = StyleSheet.create({
padding: 16, padding: 16,
}, },
noResultsText: { noResultsText: {
fontSize: 19,
fontWeight: 'bold',
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
textAlign: 'center', textAlign: 'center',
justifyContent: 'center', justifyContent: 'center',
marginTop: 34, marginTop: 34,
}, writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
highlightedContainer: { fontWeight: 'bold',
backgroundColor: 'white',
borderColor: 'black',
borderWidth: 1,
borderRadius: 5,
padding: 2,
alignSelf: 'flex-start',
textDecorationLine: 'underline',
textDecorationStyle: 'double',
textShadowColor: '#000',
textShadowOffset: { width: 1, height: 1 },
textShadowRadius: 1,
},
highlighted: {
color: 'black',
fontSize: 19,
fontWeight: '600',
},
defaultText: {
fontSize: 19, fontSize: 19,
}, },
dimmedText: { dimmedText: {
opacity: 0.8, opacity: 0.8,
}, },
defaultText: {
fontSize: 19,
fontWeight: '600',
},
highlighted: {
fontSize: 19,
fontWeight: '600',
color: 'black',
textShadowRadius: 1,
textShadowOffset: { width: 1, height: 1 },
textShadowColor: '#000',
textDecorationStyle: 'double',
textDecorationLine: 'underline',
alignSelf: 'flex-start',
padding: 2,
borderRadius: 5,
borderWidth: 1,
borderColor: 'black',
backgroundColor: 'white',
},
highlightedContainer: {
alignSelf: 'flex-start',
},
}); });

View file

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { RouteProp, useFocusEffect, useRoute, usePreventRemove, CommonActions } from '@react-navigation/native'; import { RouteProp, useFocusEffect, useRoute, usePreventRemove, StackActions } from '@react-navigation/native';
import { import {
ActivityIndicator, ActivityIndicator,
Alert, Alert,
@ -18,7 +18,15 @@ import {
import { Badge, Icon } from '@rneui/themed'; import { Badge, Icon } from '@rneui/themed';
import { isDesktop } from '../../blue_modules/environment'; import { isDesktop } from '../../blue_modules/environment';
import { encodeUR } from '../../blue_modules/ur'; import { encodeUR } from '../../blue_modules/ur';
import { BlueCard, BlueFormMultiInput, BlueLoading, BlueSpacing10, BlueSpacing20, BlueTextCentered } from '../../BlueComponents'; import {
BlueCard,
BlueFormMultiInput,
BlueLoading,
BlueSpacing10,
BlueSpacing20,
BlueSpacing40,
BlueTextCentered,
} from '../../BlueComponents';
import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class'; import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class';
import presentAlert from '../../components/Alert'; import presentAlert from '../../components/Alert';
import BottomModal, { BottomModalHandle } from '../../components/BottomModal'; import BottomModal, { BottomModalHandle } from '../../components/BottomModal';
@ -40,14 +48,14 @@ import { useStorage } from '../../hooks/context/useStorage';
import ToolTipMenu from '../../components/TooltipMenu'; import ToolTipMenu from '../../components/TooltipMenu';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions'; import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { useSettings } from '../../hooks/context/useSettings'; import { useSettings } from '../../hooks/context/useSettings';
import { ViewEditMultisigCosignersStackParamList } from '../../navigation/ViewEditMultisigCosignersStack';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import SafeArea from '../../components/SafeArea'; import SafeArea from '../../components/SafeArea';
import { TWallet } from '../../class/wallets/types'; import { TWallet } from '../../class/wallets/types';
import { AddressInputScanButton } from '../../components/AddressInputScanButton'; import { AddressInputScanButton } from '../../components/AddressInputScanButton';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
type RouteParams = RouteProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>; type RouteParams = RouteProp<DetailViewStackParamList, 'ViewEditMultisigCosigners'>;
type NavigationProp = NativeStackNavigationProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>; type NavigationProp = NativeStackNavigationProp<DetailViewStackParamList, 'ViewEditMultisigCosigners'>;
const ViewEditMultisigCosigners: React.FC = () => { const ViewEditMultisigCosigners: React.FC = () => {
const hasLoaded = useRef(false); const hasLoaded = useRef(false);
@ -169,9 +177,11 @@ const ViewEditMultisigCosigners: React.FC = () => {
setIsSaveButtonDisabled(true); setIsSaveButtonDisabled(true);
setWalletsWithNewOrder(newWallets); setWalletsWithNewOrder(newWallets);
setTimeout(() => { setTimeout(() => {
dispatch( const popTo = StackActions.popTo('WalletTransactions', {
CommonActions.navigate({ name: 'WalletTransactions', params: { walletID: wallet.getID(), walletType: MultisigHDWallet.type } }), walletID,
); walletType: wallet.type,
});
dispatch(popTo);
}, 500); }, 500);
}, 100); }, 100);
}; };
@ -560,6 +570,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
{!isLoading && ( {!isLoading && (
<> <>
<BlueSpacing40 />
<AddressInputScanButton <AddressInputScanButton
beforePress={async () => { beforePress={async () => {
await provideMnemonicsModalRef.current?.dismiss(); await provideMnemonicsModalRef.current?.dismiss();
@ -568,7 +579,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
type="link" type="link"
onChangeText={setImportText} onChangeText={setImportText}
/> />
<BlueSpacing20 /> <BlueSpacing40 />
</> </>
)} )}
</> </>

View file

@ -95,7 +95,8 @@ const WalletDetails: React.FC = () => {
} else { } else {
setIsLoading(false); setIsLoading(false);
} }
}, [handleWalletDeletion, wallet]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const presentWalletHasBalanceAlert = useCallback(async () => { const presentWalletHasBalanceAlert = useCallback(async () => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning); triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning);
@ -305,11 +306,8 @@ const WalletDetails: React.FC = () => {
}); });
}; };
const navigateToViewEditCosigners = () => { const navigateToViewEditCosigners = () => {
navigate('ViewEditMultisigCosignersRoot', { navigate('ViewEditMultisigCosigners', {
screen: 'ViewEditMultisigCosigners', walletID,
params: {
walletID,
},
}); });
}; };
const navigateToXPub = () => const navigateToXPub = () =>
@ -446,7 +444,7 @@ const WalletDetails: React.FC = () => {
return ( return (
<> <>
<Text style={[styles.textLabel1, stylesHook.textLabel1]}>{loc.wallets.details_address.toLowerCase()}</Text> <Text style={[styles.textLabel1, stylesHook.textLabel1]}>{loc.wallets.details_address.toLowerCase()}</Text>
<Text style={[styles.textValue, stylesHook.textValue]}> <Text style={[styles.textValue, stylesHook.textValue]} selectable>
{(() => { {(() => {
// gracefully handling faulty wallets, so at least user has an option to delete the wallet // gracefully handling faulty wallets, so at least user has an option to delete the wallet
try { try {

View file

@ -7,7 +7,6 @@ import {
findNodeHandle, findNodeHandle,
FlatList, FlatList,
I18nManager, I18nManager,
InteractionManager,
LayoutAnimation, LayoutAnimation,
PixelRatio, PixelRatio,
ScrollView, ScrollView,
@ -53,12 +52,14 @@ const buttonFontSize =
? 22 ? 22
: PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26); : PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
type WalletTransactionsProps = NativeStackScreenProps<DetailViewStackParamList, 'WalletTransactions'>;
type RouteProps = RouteProp<DetailViewStackParamList, 'WalletTransactions'>; type RouteProps = RouteProp<DetailViewStackParamList, 'WalletTransactions'>;
type WalletTransactionsProps = NativeStackScreenProps<DetailViewStackParamList, 'WalletTransactions'>;
type TransactionListItem = Transaction & { type: 'transaction' | 'header' }; type TransactionListItem = Transaction & { type: 'transaction' | 'header' };
const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => { const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
const { wallets, saveToDisk, setSelectedWalletID } = useStorage(); const { wallets, saveToDisk, setSelectedWalletID } = useStorage();
const { setReloadTransactionsMenuActionFunction } = useMenuElements(); const { registerTransactionsHandler, unregisterTransactionsHandler } = useMenuElements();
const { isBiometricUseCapableAndEnabled } = useBiometrics(); const { isBiometricUseCapableAndEnabled } = useBiometrics();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { params, name } = useRoute<RouteProps>(); const { params, name } = useRoute<RouteProps>();
@ -98,7 +99,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
if (wallet?.chain === Chain.ONCHAIN) { if (wallet?.chain === Chain.ONCHAIN) {
navigate('SendDetailsRoot', { screen: 'SendDetails', params: parameters }); navigate('SendDetailsRoot', { screen: 'SendDetails', params: parameters });
} else { } else {
navigate('ScanLndInvoiceRoot', { screen: 'ScanLndInvoice', params: parameters }); navigate('ScanLNDInvoiceRoot', { screen: 'ScanLNDInvoice', params: parameters });
} }
setIsLoading(false); setIsLoading(false);
} }
@ -256,11 +257,8 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
); );
const navigateToViewEditCosigners = useCallback(() => { const navigateToViewEditCosigners = useCallback(() => {
navigate('ViewEditMultisigCosignersRoot', { navigate('ViewEditMultisigCosigners', {
screen: 'ViewEditMultisigCosigners', walletID,
params: {
walletID,
},
}); });
}, [navigate, walletID]); }, [navigate, walletID]);
@ -321,7 +319,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
const sendButtonPress = () => { const sendButtonPress = () => {
if (wallet?.chain === Chain.OFFCHAIN) { if (wallet?.chain === Chain.OFFCHAIN) {
return navigate('ScanLndInvoiceRoot', { screen: 'ScanLndInvoice', params: { walletID } }); return navigate('ScanLNDInvoiceRoot', { screen: 'ScanLNDInvoice', params: { walletID } });
} }
if (wallet?.type === WatchOnlyWallet.type && wallet.isHd() && !wallet.useWithHardwareWalletEnabled()) { if (wallet?.type === WatchOnlyWallet.type && wallet.isHd() && !wallet.useWithHardwareWalletEnabled()) {
@ -387,17 +385,27 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
); );
}; };
useEffect(() => {
if (wallet) {
const screenKey = `WalletTransactions-${walletID}`;
registerTransactionsHandler(() => refreshTransactions(true), screenKey);
return () => {
unregisterTransactionsHandler(screenKey);
};
}
}, [wallet, walletID, refreshTransactions, registerTransactionsHandler, unregisterTransactionsHandler]);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
const task = InteractionManager.runAfterInteractions(() => { if (wallet) {
setReloadTransactionsMenuActionFunction(() => refreshTransactions); const screenKey = `WalletTransactions-${walletID}`;
});
return () => { return () => {
task.cancel(); unregisterTransactionsHandler(screenKey);
console.debug('Next screen is focused, clearing reloadTransactionsMenuActionFunction'); };
setReloadTransactionsMenuActionFunction(() => {}); }
}; }, [wallet, walletID, unregisterTransactionsHandler]),
}, [setReloadTransactionsMenuActionFunction, refreshTransactions]),
); );
const [balance, setBalance] = useState(wallet ? wallet.getBalance() : 0); const [balance, setBalance] = useState(wallet ? wallet.getBalance() : 0);

View file

@ -98,7 +98,7 @@ const WalletsList: React.FC = () => {
const { isLargeScreen } = useIsLargeScreen(); const { isLargeScreen } = useIsLargeScreen();
const walletsCarousel = useRef<any>(); const walletsCarousel = useRef<any>();
const currentWalletIndex = useRef<number>(0); const currentWalletIndex = useRef<number>(0);
const { setReloadTransactionsMenuActionFunction, clearReloadTransactionsMenuAction } = useMenuElements(); const { registerTransactionsHandler, unregisterTransactionsHandler } = useMenuElements();
const { wallets, getTransactions, getBalance, refreshAllWalletTransactions, setSelectedWalletID } = useStorage(); const { wallets, getTransactions, getBalance, refreshAllWalletTransactions, setSelectedWalletID } = useStorage();
const { isTotalBalanceEnabled, isElectrumDisabled } = useSettings(); const { isTotalBalanceEnabled, isElectrumDisabled } = useSettings();
const { width } = useWindowDimensions(); const { width } = useWindowDimensions();
@ -159,19 +159,41 @@ const WalletsList: React.FC = () => {
} }
}, [getBalance]); }, [getBalance]);
useEffect(() => {
const screenKey = route.name;
console.log(`[WalletsList] Registering handler with key: ${screenKey}`);
registerTransactionsHandler(onRefresh, screenKey);
return () => {
console.log(`[WalletsList] Unmounting - cleaning up handler for: ${screenKey}`);
unregisterTransactionsHandler(screenKey);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onRefresh, registerTransactionsHandler, unregisterTransactionsHandler]);
useFocusEffect(
useCallback(() => {
const screenKey = route.name;
return () => {
console.log(`[WalletsList] Blurred - cleaning up handler for: ${screenKey}`);
unregisterTransactionsHandler(screenKey);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [unregisterTransactionsHandler]),
);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
const task = InteractionManager.runAfterInteractions(() => { const task = InteractionManager.runAfterInteractions(() => {
setReloadTransactionsMenuActionFunction(onRefresh);
verifyBalance(); verifyBalance();
setSelectedWalletID(undefined); setSelectedWalletID(undefined);
}); });
return () => { return () => {
task.cancel(); task.cancel();
clearReloadTransactionsMenuAction();
}; };
}, [onRefresh, setReloadTransactionsMenuActionFunction, clearReloadTransactionsMenuAction, verifyBalance, setSelectedWalletID]), }, [verifyBalance, setSelectedWalletID]),
); );
useEffect(() => { useEffect(() => {
@ -207,6 +229,7 @@ const WalletsList: React.FC = () => {
useEffect(() => { useEffect(() => {
refreshTransactions(); refreshTransactions();
// es-lint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);

View file

@ -148,9 +148,9 @@ describe.each(['', '//'])('unit - DeepLinkSchemaMatch', function (suffix) {
url: `lightning:${suffix}lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde`, url: `lightning:${suffix}lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde`,
}, },
expected: [ expected: [
'ScanLndInvoiceRoot', 'ScanLNDInvoiceRoot',
{ {
screen: 'ScanLndInvoice', screen: 'ScanLNDInvoice',
params: { params: {
uri: 'lightning:lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde', uri: 'lightning:lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde',
}, },
@ -162,9 +162,9 @@ describe.each(['', '//'])('unit - DeepLinkSchemaMatch', function (suffix) {
url: `bluewallet:lightning:${suffix}lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde`, url: `bluewallet:lightning:${suffix}lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde`,
}, },
expected: [ expected: [
'ScanLndInvoiceRoot', 'ScanLNDInvoiceRoot',
{ {
screen: 'ScanLndInvoice', screen: 'ScanLNDInvoice',
params: { params: {
uri: 'lightning:lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde', uri: 'lightning:lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde',
}, },
@ -248,9 +248,9 @@ describe.each(['', '//'])('unit - DeepLinkSchemaMatch', function (suffix) {
url: 'lnaddress@zbd.gg', url: 'lnaddress@zbd.gg',
}, },
expected: [ expected: [
'ScanLndInvoiceRoot', 'ScanLNDInvoiceRoot',
{ {
screen: 'ScanLndInvoice', screen: 'ScanLNDInvoice',
params: { params: {
uri: 'lnaddress@zbd.gg', uri: 'lnaddress@zbd.gg',
}, },
@ -518,13 +518,13 @@ describe.each(['', '//'])('unit - DeepLinkSchemaMatch', function (suffix) {
navigate: (...args) => { navigate: (...args) => {
navigateWasCalled = true; navigateWasCalled = true;
assert.deepStrictEqual(args, [ assert.deepStrictEqual(args, [
'ScanLndInvoiceRoot', 'ScanLNDInvoiceRoot',
{ {
params: { params: {
uri: 'lightning:LNBC1855790N1PNUPWSFPP5P5RVQJA067PV6NJQ3EFKLP78TN6MHUK842ZFGDCTXRDSGNTY765QDZ62PSKJEPQW3HJQSNPD36XJCEQFPHKUETEVFSKGEM9WGSRYVPJXSSZSNMJV3JHYGZFGSAZQARFVD4K2AR5V95KCMMJ9YCQZPUXQZ6GSP53E4EX9YTD2MGDN2C2CFA0J0SM3E7PVLPJ208H5LMYPNJMGZ7RLGS9QXPQYSGQ6GQMEQXJKKF2DHXJK8XQ4WGLM5NTE3RKEXGYQC6HYGFKS9SHHA6HL9X4339MXHNNQFSH7TS62PU8T9RSWTK6HQ4LV4GW3DPD25DQ8UQQYC909N', uri: 'lightning:LNBC1855790N1PNUPWSFPP5P5RVQJA067PV6NJQ3EFKLP78TN6MHUK842ZFGDCTXRDSGNTY765QDZ62PSKJEPQW3HJQSNPD36XJCEQFPHKUETEVFSKGEM9WGSRYVPJXSSZSNMJV3JHYGZFGSAZQARFVD4K2AR5V95KCMMJ9YCQZPUXQZ6GSP53E4EX9YTD2MGDN2C2CFA0J0SM3E7PVLPJ208H5LMYPNJMGZ7RLGS9QXPQYSGQ6GQMEQXJKKF2DHXJK8XQ4WGLM5NTE3RKEXGYQC6HYGFKS9SHHA6HL9X4339MXHNNQFSH7TS62PU8T9RSWTK6HQ4LV4GW3DPD25DQ8UQQYC909N',
walletID: 'bfcacb7288cf43c6c02a1154c432ec155b813798fa4e87cd2c1e5531d6363f71', walletID: 'bfcacb7288cf43c6c02a1154c432ec155b813798fa4e87cd2c1e5531d6363f71',
}, },
screen: 'ScanLndInvoice', screen: 'ScanLNDInvoice',
}, },
]); ]);
}, },