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
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "7.1.3"
versionName "7.1.5"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
@ -137,7 +137,7 @@ dependencies {
androidTestImplementation('com.wix:detox:0.1.1')
implementation 'androidx.appcompat:appcompat:1.7.0'
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.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()}"
"BNR" -> "https://www.bnr.ro/nbrfxrates.xml"
"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")
"Coinbase" -> json.getJSONObject("data").getString("amount")
"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
}
} catch (e: Exception) {

View file

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

View file

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

View file

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

View file

@ -1,10 +1,8 @@
import React, { useCallback } from 'react';
import { Keyboard, StyleProp, StyleSheet, TextInput, View, ViewStyle } from 'react-native';
import React from 'react';
import { StyleProp, StyleSheet, TextInput, View, ViewStyle } from 'react-native';
import loc from '../loc';
import { AddressInputScanButton } from './AddressInputScanButton';
import { useTheme } from './themes';
import DeeplinkSchemaMatch from '../class/deeplink-schema-match';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
interface AddressInputProps {
isLoading?: boolean;
@ -31,7 +29,6 @@ interface AddressInputProps {
| 'twitter'
| 'web-search'
| 'visible-password';
skipValidation?: boolean;
}
const AddressInput = ({
@ -46,7 +43,6 @@ const AddressInput = ({
onBlur = () => {},
keyboardType = 'default',
style,
skipValidation = false,
}: AddressInputProps) => {
const { colors } = useTheme();
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 (
<View style={[styles.root, stylesHook.root, style]}>
<TextInput
@ -100,7 +73,7 @@ const AddressInput = ({
autoCapitalize="none"
autoCorrect={false}
keyboardType={keyboardType}
{...(skipValidation ? { onBlur } : { onBlur: onBlurEditing })}
onBlur={onBlur}
/>
{editable ? <AddressInputScanButton isLoading={isLoading} onChangeText={onChangeText} /> : null}
</View>

View file

@ -89,6 +89,8 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
}) => {
const { colors } = useTheme();
const [isLoading, setIsLoading] = useState(false);
const [isSwipeActive, setIsSwipeActive] = useState(false);
const resetFunctionRef = useRef<(() => void) | null>(null);
const CARD_SORT_ACTIVE = 1.06;
const INACTIVE_SCALE_WHEN_ACTIVE = 0.9;
@ -125,24 +127,39 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
reset();
};
const leftContent = (reset: () => void) => (
<LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} />
);
const handleRightPress = (reset: () => void) => {
handleDeleteWallet(item.data as TWallet);
reset();
const leftContent = (reset: () => void) => {
resetFunctionRef.current = reset;
return <LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} />;
};
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(() => {
if (isSwipeActive) {
return;
}
if (resetFunctionRef.current) {
resetFunctionRef.current();
}
scaleValue.setValue(CARD_SORT_ACTIVE);
triggerHapticFeedback(HapticFeedbackTypes.ImpactMedium);
if (drag) {
drag();
}
}, [CARD_SORT_ACTIVE, drag, scaleValue]);
}, [CARD_SORT_ACTIVE, drag, scaleValue, isSwipeActive]);
if (isLoading) {
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 swipeDisabled = isActive || globalDragActive;
return (
<Animated.View style={animatedStyle}>
<ListItem.Swipeable
leftWidth={80}
rightWidth={90}
containerStyle={[style, { backgroundColor }, isActive || globalDragActive ? styles.transparentBackground : {}]}
leftContent={globalDragActive ? null : isActive ? null : leftContent}
rightContent={globalDragActive ? null : isActive ? null : rightContent}
leftWidth={swipeDisabled ? 0 : 80}
rightWidth={swipeDisabled ? 0 : 90}
containerStyle={[style, { backgroundColor }, swipeDisabled ? styles.transparentBackground : {}]}
leftContent={swipeDisabled ? null : leftContent}
rightContent={swipeDisabled ? null : rightContent}
onPressOut={onPressOut}
minSlideWidth={80}
minSlideWidth={swipeDisabled ? 0 : 80}
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>
<WalletCarouselItem
item={item.data}
handleLongPress={isDraggingDisabled ? undefined : startDrag}
handleLongPress={isDraggingDisabled || isSwipeActive ? undefined : startDrag}
onPress={onPress}
onPressIn={onPressIn}
onPressOut={onPressOut}

View file

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

View file

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

View file

@ -19,25 +19,42 @@ import loc from '../loc';
import { Chain } from '../models/bitcoinUnits';
import { navigationRef } from '../NavigationService';
import ActionSheet from '../screen/ActionSheet';
import { useStorage } from '../hooks/context/useStorage';
import { useStorage } from './context/useStorage';
import RNQRGenerator from 'rn-qr-generator';
import presentAlert from './Alert';
import useMenuElements from '../hooks/useMenuElements';
import useWidgetCommunication from '../hooks/useWidgetCommunication';
import useWatchConnectivity from '../hooks/useWatchConnectivity';
import useDeviceQuickActions from '../hooks/useDeviceQuickActions';
import useHandoffListener from '../hooks/useHandoffListener';
import presentAlert from '../components/Alert';
import useWidgetCommunication from './useWidgetCommunication';
import useWatchConnectivity from './useWatchConnectivity';
import useDeviceQuickActions from './useDeviceQuickActions';
import useHandoffListener from './useHandoffListener';
import useMenuElements from './useMenuElements';
const ClipboardContentType = Object.freeze({
BITCOIN: 'BITCOIN',
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 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();
useWidgetCommunication();
useMenuElements();
@ -45,6 +62,8 @@ const CompanionDelegates = () => {
useHandoffListener();
const processPushNotifications = useCallback(async () => {
if (!shouldActivateListeners) return false;
await new Promise(resolve => setTimeout(resolve, 200));
try {
const notifications2process = await getStoredNotifications();
@ -164,15 +183,19 @@ const CompanionDelegates = () => {
console.error('Failed to process push notifications:', error);
}
return false;
}, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets]);
}, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets, shouldActivateListeners]);
useEffect(() => {
if (!shouldActivateListeners) return;
initializeNotifications(processPushNotifications);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [shouldActivateListeners]);
const handleOpenURL = useCallback(
async (event: { url: string }): Promise<void> => {
if (!shouldActivateListeners) return;
try {
if (!event.url) return;
let decodedUrl: string;
@ -227,11 +250,13 @@ const CompanionDelegates = () => {
presentAlert({ message: err.message || loc.send.qr_error_no_qrcode });
}
},
[wallets, addWallet, saveToDisk, setSharedCosigner],
[wallets, addWallet, saveToDisk, setSharedCosigner, shouldActivateListeners],
);
const showClipboardAlert = useCallback(
({ contentType }: { contentType: undefined | string }) => {
if (!shouldActivateListeners) return;
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
getClipboardContent().then(clipboard => {
if (!clipboard) return;
@ -254,12 +279,13 @@ const CompanionDelegates = () => {
);
});
},
[handleOpenURL],
[handleOpenURL, shouldActivateListeners],
);
const handleAppStateChange = useCallback(
async (nextAppState: AppStateStatus | undefined) => {
if (wallets.length === 0) return;
if (!shouldActivateListeners || wallets.length === 0) return;
if ((appState.current.match(/background/) && nextAppState === 'active') || nextAppState === undefined) {
setTimeout(() => A(A.ENUM.APP_UNSUSPENDED), 2000);
updateExchangeRate();
@ -299,10 +325,12 @@ const CompanionDelegates = () => {
appState.current = nextAppState;
}
},
[processPushNotifications, showClipboardAlert, wallets],
[processPushNotifications, showClipboardAlert, wallets, shouldActivateListeners],
);
const addListeners = useCallback(() => {
if (!shouldActivateListeners) return { urlSubscription: null, appStateSubscription: null };
const urlSubscription = Linking.addEventListener('url', handleOpenURL);
const appStateSubscription = AppState.addEventListener('change', handleAppStateChange);
@ -310,18 +338,16 @@ const CompanionDelegates = () => {
urlSubscription,
appStateSubscription,
};
}, [handleOpenURL, handleAppStateChange]);
}, [handleOpenURL, handleAppStateChange, shouldActivateListeners]);
useEffect(() => {
const subscriptions = addListeners();
return () => {
subscriptions.urlSubscription?.remove();
subscriptions.appStateSubscription?.remove();
subscriptions.urlSubscription?.remove?.();
subscriptions.appStateSubscription?.remove?.();
};
}, [addListeners]);
return null;
};
export default CompanionDelegates;
export default useCompanionListeners;

View file

@ -10,7 +10,7 @@ import { useCallback, useMemo } from 'react';
const requiresBiometrics = [
'WalletExportRoot',
'WalletXpubRoot',
'ViewEditMultisigCosignersRoot',
'ViewEditMultisigCosigners',
'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 { navigationRef } from '../NavigationService';
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.
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;
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 {
if ((Platform.OS === 'ios' || Platform.OS === 'macos') && MenuElementsEmitter) {
if (Platform.OS === 'ios' && MenuElementsEmitter) {
eventEmitter = new NativeEventEmitter(MenuElementsEmitter);
}
} catch (error) {
console.warn('[MenuElements] Failed to initialize event emitter: ', error);
eventEmitter = null;
}
// Empty function that does nothing - used as default
const noop = () => {};
const useMenuElements = () => {
const { walletsInitialized } = useStorage();
const reloadTransactionsMenuActionRef = useRef<MenuEventHandler>(noop);
// Track if listeners have been set up
const listenersInitialized = useRef<boolean>(false);
const listenersRef = useRef<any[]>([]);
const setReloadTransactionsMenuActionFunction = useCallback((handler: MenuEventHandler) => {
if (typeof handler !== 'function') {
/**
* Safely navigate using multiple fallback approaches
*/
function safeNavigate(routeName: string, params?: Record<string, any>): void {
try {
if (navigationRef.current?.isReady()) {
navigationRef.current.navigate(routeName as never, params as never);
return;
}
reloadTransactionsMenuActionRef.current = handler;
globalReloadTransactionsFunction = handler;
}, []);
const clearReloadTransactionsMenuAction = useCallback(() => {
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
if (navigationRef.isReady()) {
navigationRef.dispatch(
CommonActions.navigate({
name: routeName,
params,
}),
);
}
}, []);
} catch (error) {
console.error(`[MenuElements] Navigation error:`, error);
}
}
const eventActions = useMemo(
() => ({
openSettings: () => {
dispatchNavigate('Settings');
},
addWallet: () => {
dispatchNavigate('AddWalletRoot');
},
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 = [];
// Cleanup event listeners to prevent memory leaks
function cleanupListeners(): void {
if (subscriptions.length > 0) {
subscriptions.forEach(subscription => {
try {
subscription.remove();
} catch (e) {
console.warn('[MenuElements] Error removing subscription:', e);
}
});
subscriptions = [];
listenersInitialized = false;
}
}
eventEmitter.removeAllListeners('openSettings');
eventEmitter.removeAllListeners('addWalletMenuAction');
eventEmitter.removeAllListeners('importWalletMenuAction');
eventEmitter.removeAllListeners('reloadTransactionsMenuAction');
} catch (error) {
// Error cleanup silently ignored
}
function initializeListeners(): void {
if (!eventEmitter || listenersInitialized) return;
try {
const listeners = [
eventEmitter.addListener('openSettings', eventActions.openSettings),
eventEmitter.addListener('addWalletMenuAction', eventActions.addWallet),
eventEmitter.addListener('importWalletMenuAction', eventActions.importWallet),
eventEmitter.addListener('reloadTransactionsMenuAction', eventActions.reloadTransactions),
];
cleanupListeners();
listenersRef.current = listeners;
listenersInitialized.current = true;
} catch (error) {
// Listener setup failed silently
}
// Navigation actions
const globalActions = {
navigateToSettings: (): void => {
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 () => {
try {
listenersRef.current.forEach(listener => {
if (listener && typeof listener.remove === 'function') {
listener.remove();
}
});
listenersRef.current = [];
listenersInitialized.current = false;
} catch (error) {
// Cleanup error silently ignored
}
unsubscribe();
};
}, [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 {
setReloadTransactionsMenuActionFunction,
clearReloadTransactionsMenuAction,
registerTransactionsHandler,
unregisterTransactionsHandler,
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 {
setReloadTransactionsMenuActionFunction: (_func: any) => {},
clearReloadTransactionsMenuAction: () => {},
isMenuElementsSupported: true,
registerTransactionsHandler,
unregisterTransactionsHandler,
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=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703157999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
@ -1483,7 +1483,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.3;
MARKETING_VERSION = 7.1.5;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@ -1518,7 +1518,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703157999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
@ -1541,7 +1541,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.3;
MARKETING_VERSION = 7.1.5;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@ -1577,7 +1577,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703157999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
@ -1590,7 +1590,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.3;
MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -1620,7 +1620,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1703157999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
@ -1633,7 +1633,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.3;
MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.Stickers;
@ -1664,7 +1664,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703157999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
@ -1683,7 +1683,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.3;
MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -1720,7 +1720,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1703157999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
@ -1739,7 +1739,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.3;
MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.MarketWidget;
@ -1907,7 +1907,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703157999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
@ -1927,7 +1927,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.3;
MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -1960,7 +1960,7 @@
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1703157999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
@ -1980,7 +1980,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.3;
MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch.extension;
@ -2012,7 +2012,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703157999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
@ -2026,7 +2026,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.3;
MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -2061,7 +2061,7 @@
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1703157999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
@ -2075,7 +2075,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.3;
MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch;

View file

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

View file

@ -3,14 +3,27 @@ import React
@objc(MenuElementsEmitter)
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
override init() {
super.init()
MenuElementsEmitter._sharedInstance = self
NSLog("[MenuElements] Swift: Initialized MenuElementsEmitter instance")
MenuElementsEmitter.sharedInstance = self
NSLog("[MenuElements] MenuElementsEmitter initialized")
}
deinit {
NSLog("[MenuElements] MenuElementsEmitter deallocated")
// Ensure all event listeners are removed in deinit
self.removeAllListeners()
}
override class func requiresMainQueueSetup() -> Bool {
@ -22,54 +35,100 @@ class MenuElementsEmitter: RCTEventEmitter {
}
@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() {
hasListeners = true
NSLog("[MenuElements] Swift: Started observing events")
NSLog("[MenuElements] Started observing events, bridge: \(self.bridge != nil ? "available" : "unavailable")")
}
override func stopObserving() {
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) {
if hasListeners && self.bridge != nil {
NSLog("[MenuElements] Swift: Emitting event: %@", name)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.sendEvent(withName: name, body: nil)
private func limitCacheSize() {
if lastEventTime.count > maxCacheSize {
// Remove oldest entries if cache is too large
let sortedKeys = lastEventTime.sorted(by: { $0.value < $1.value })
for i in 0..<(lastEventTime.count - maxCacheSize) {
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() {
NSLog("[MenuElements] Swift: openSettings called")
NSLog("[MenuElements] openSettings method called")
safelyEmitEvent(withName: "openSettings")
}
@objc func addWalletMenuAction() {
NSLog("[MenuElements] Swift: addWalletMenuAction called")
NSLog("[MenuElements] addWalletMenuAction method called")
safelyEmitEvent(withName: "addWalletMenuAction")
}
@objc func importWalletMenuAction() {
NSLog("[MenuElements] Swift: importWalletMenuAction called")
NSLog("[MenuElements] importWalletMenuAction method called")
safelyEmitEvent(withName: "importWalletMenuAction")
}
@objc func reloadTransactionsMenuAction() {
NSLog("[MenuElements] Swift: reloadTransactionsMenuAction called")
safelyEmitEvent(withName: "reloadTransactionsMenuAction")
}
override func invalidate() {
NSLog("[MenuElements] Swift: Module invalidated")
MenuElementsEmitter._sharedInstance = nil
NSLog("[MenuElements] Module invalidated")
if MenuElementsEmitter.sharedInstance === self {
MenuElementsEmitter.sharedInstance = nil
}
removeAllListeners()
super.invalidate()
}
}

View file

@ -30,8 +30,8 @@ class MarketAPI {
return "https://www.bnr.ro/nbrfxrates.xml"
case "Kraken":
return "https://api.kraken.com/0/public/Ticker?pair=XXBTZ\(endPointKey.uppercased())"
default:
return "https://api.coindesk.com/v1/bpi/currentprice/\(endPointKey).json"
default: // CoinDesk
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)")
}
}
default:
throw CurrencyError(errorDescription: "Unsupported data source \(source)")
default: // CoinDesk
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';
export const FiatUnitSource = {
@ -15,7 +16,8 @@ export const FiatUnitSource = {
const handleError = (source: string, ticker: string, error: 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 {
bpi: {
[ticker: string]: {
rate_float: number;
};
};
[ticker: string]: number;
}
interface CoinGeckoResponse {
@ -96,8 +94,10 @@ const RateExtractors = {
CoinDesk: async (ticker: string): Promise<number> => {
try {
const json = (await fetchRate(`https://api.coindesk.com/v1/bpi/currentprice/${ticker}.json`)) as CoinDeskResponse;
const rate = Number(json?.bpi?.[ticker]?.rate_float);
const json = (await fetchRate(
`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');
return rate;
} catch (error: any) {

View file

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

View file

@ -47,7 +47,7 @@ export type DetailViewStackParamList = {
AddWalletRoot: undefined;
SendDetailsRoot: SendDetailsParams;
LNDCreateInvoiceRoot: undefined;
ScanLndInvoiceRoot: {
ScanLNDInvoiceRoot: {
screen: string;
params: {
paymentHash: string;
@ -79,7 +79,7 @@ export type DetailViewStackParamList = {
ReleaseNotes: undefined;
ToolsScreen: undefined;
SettingsPrivacy: undefined;
ViewEditMultisigCosignersRoot: { walletID: string; cosigners: string[] };
ViewEditMultisigCosigners: { walletID: string; cosigners: string[]; onBarScanned?: string };
WalletXpubRoot: undefined;
SignVerifyRoot: {
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';
// 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 Success = lazy(() => import('../screen/send/success'));
const LnurlPay = lazy(() => import('../screen/lnd/lnurlPay'));
const LnurlPaySuccess = lazy(() => import('../screen/lnd/lnurlPaySuccess'));
export const ScanLndInvoiceComponent = () => (
export const ScanLNDInvoiceComponent = () => (
<Suspense fallback={<LazyLoadingIndicator />}>
<ScanLndInvoice />
<ScanLNDInvoice />
</Suspense>
);

View file

@ -1,20 +1,16 @@
import React, { lazy, Suspense } from 'react';
import { useStorage } from '../hooks/context/useStorage';
import React from 'react';
import DevMenu from '../components/DevMenu';
import MainRoot from './index';
const CompanionDelegates = lazy(() => import('../components/CompanionDelegates'));
import useCompanionListeners from '../hooks/useCompanionListeners';
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 (
<>
<MainRoot />
{walletsInitialized && (
<Suspense>
<CompanionDelegates />
</Suspense>
)}
{__DEV__ && <DevMenu />}
</>
);

View file

@ -4,24 +4,19 @@ import React from 'react';
import navigationStyle from '../components/navigationStyle';
import { useTheme } from '../components/themes';
import loc from '../loc';
import {
LnurlPayComponent,
LnurlPaySuccessComponent,
ScanLndInvoiceComponent,
SelectWalletComponent,
SuccessComponent,
} from './LazyLoadScanLndInvoiceStack';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
import { LnurlPayComponent, LnurlPaySuccessComponent, ScanLNDInvoiceComponent, SuccessComponent } from './LazyLoadScanLNDInvoiceStack';
import { SelectWalletComponent } from './LazyLoadLNDCreateInvoiceStack';
const Stack = createNativeStackNavigator();
const ScanLndInvoiceRoot = () => {
const ScanLNDInvoiceRoot = () => {
const theme = useTheme();
return (
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
<Stack.Screen
name="ScanLndInvoice"
component={ScanLndInvoiceComponent}
name="ScanLNDInvoice"
component={ScanLNDInvoiceComponent}
options={navigationStyle({ headerBackVisible: false, title: loc.send.header, statusBarStyle: 'light' })(theme)}
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;
};
Success: {
fee: number;
fee?: number;
amount: number;
amountUnit?: BitcoinUnit;
txid?: string;
invoiceDescription?: string;
};
SelectWallet: {
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",
"version": "7.1.3",
"version": "7.1.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bluewallet",
"version": "7.1.3",
"version": "7.1.5",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@ -72,7 +72,7 @@
"react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c",
"react-native-device-info": "14.0.4",
"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-gesture-handler": "2.23.1",
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
@ -22087,8 +22087,7 @@
},
"node_modules/react-native-camera-kit": {
"version": "14.2.0",
"resolved": "git+ssh://git@github.com/BlueWallet/react-native-camera-kit.git#1e1921223bc9da636f9889d96b03df5f77dc7bf1",
"integrity": "sha512-jwVriBGZai7b4TCM0JXR0xqBY0HPtu2NSQQMETTNLyTjYYqkHEK2uaWkq/GY5B93gbAnTGJ5bRyQAqfWkPjDEw==",
"resolved": "git+ssh://git@github.com/BlueWallet/react-native-camera-kit.git#3193427143b73a6f304198b1123b2e8b90a90862",
"license": "MIT",
"engines": {
"node": ">=18"

View file

@ -1,6 +1,6 @@
{
"name": "bluewallet",
"version": "7.1.3",
"version": "7.1.5",
"license": "MIT",
"repository": {
"type": "git",
@ -140,7 +140,7 @@
"react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c",
"react-native-device-info": "14.0.4",
"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-gesture-handler": "2.23.1",
"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 { 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 { btcToSatoshi, fiatToBTC } from '../../blue_modules/currency';
@ -19,26 +19,35 @@ import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { useStorage } from '../../hooks/context/useStorage';
import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory';
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
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 { isBiometricUseCapableAndEnabled } = useBiometrics();
const { colors } = useTheme();
const route = useRoute();
const { walletID, uri, invoice } = useRoute().params;
/** @type {LightningCustodianWallet} */
const [wallet, setWallet] = useState(
wallets.find(item => item.getID() === walletID) || wallets.find(item => item.chain === Chain.OFFCHAIN),
const route = useRoute<RouteProps>();
const { walletID, uri, invoice } = route.params || {};
const [wallet, setWallet] = useState<LightningCustodianWallet | undefined>(
(wallets.find(item => item.getID() === walletID) as LightningCustodianWallet) ||
(wallets.find(item => item.chain === Chain.OFFCHAIN) as LightningCustodianWallet),
);
const { navigate, setParams, goBack, pop } = useNavigation();
const [isLoading, setIsLoading] = useState(false);
const [renderWalletSelectionButtonHidden, setRenderWalletSelectionButtonHidden] = useState(false);
const [destination, setDestination] = useState('');
const [unit, setUnit] = useState(BitcoinUnit.SATS);
const [decoded, setDecoded] = useState();
const [amount, setAmount] = useState();
const [isAmountInitiallyEmpty, setIsAmountInitiallyEmpty] = useState();
const [expiresIn, setExpiresIn] = useState();
const { navigate, setParams, goBack, pop } = useExtendedNavigation<NavigationProps>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [renderWalletSelectionButtonHidden, setRenderWalletSelectionButtonHidden] = useState<boolean>(false);
const [destination, setDestination] = useState<string>('');
const [unit, setUnit] = useState<BitcoinUnit>(BitcoinUnit.SATS);
const [decoded, setDecoded] = useState<DecodedInvoice | undefined>();
const [amount, setAmount] = useState<string | undefined>();
const [isAmountInitiallyEmpty, setIsAmountInitiallyEmpty] = useState<boolean | undefined>();
const [expiresIn, setExpiresIn] = useState<string | undefined>();
const stylesHook = StyleSheet.create({
walletWrapLabel: {
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(() => {
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
}, [walletID]);
@ -75,7 +78,10 @@ const ScanLndInvoice = () => {
if (!wallet) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
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
}, [wallet]),
@ -96,66 +102,68 @@ const ScanLndInvoice = () => {
data = data.replace('LIGHTNING:', '').replace('lightning:', '');
console.log(data);
let newDecoded;
let newDecoded: DecodedInvoice;
try {
newDecoded = wallet.decodeInvoice(data);
let newExpiresIn = (newDecoded.timestamp * 1 + newDecoded.expiry * 1) * 1000; // ms
if (+new Date() > newExpiresIn) {
const expiryTimeMs = (newDecoded.timestamp * 1 + newDecoded.expiry * 1) * 1000; // ms
let newExpiresIn: string;
if (+new Date() > expiryTimeMs) {
newExpiresIn = loc.lnd.expired;
} 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 });
}
Keyboard.dismiss();
setParams({ uri: undefined, invoice: data });
setIsAmountInitiallyEmpty(newDecoded.num_satoshis === '0');
setIsAmountInitiallyEmpty(newDecoded.num_satoshis === 0);
setDestination(data);
setIsLoading(false);
setAmount(newDecoded.num_satoshis);
setAmount(newDecoded.num_satoshis.toString());
setExpiresIn(newExpiresIn);
setDecoded(newDecoded);
} catch (Err) {
} catch (Err: any) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
Keyboard.dismiss();
setParams({ uri: undefined });
setTimeout(() => presentAlert({ message: Err.message }), 10);
setTimeout(() => presentAlert({ message: Err.message, hapticFeedback: HapticFeedbackTypes.NotificationError }), 10);
setIsLoading(false);
setAmount();
setDestination();
setExpiresIn();
setDecoded();
setAmount(undefined);
setDestination('');
setExpiresIn(undefined);
setDecoded(undefined);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uri]);
const _keyboardDidShow = () => {
const _keyboardDidShow = (): void => {
setRenderWalletSelectionButtonHidden(true);
};
const _keyboardDidHide = () => {
const _keyboardDidHide = (): void => {
setRenderWalletSelectionButtonHidden(false);
};
const processInvoice = data => {
useKeyboard({ onKeyboardDidShow: _keyboardDidShow, onKeyboardDidHide: _keyboardDidHide });
const processInvoice = (data: string): void => {
if (Lnurl.isLnurl(data)) return processLnurlPay(data);
if (Lnurl.isLightningAddress(data)) return processLnurlPay(data);
setParams({ uri: data });
};
const processLnurlPay = data => {
navigate('ScanLndInvoiceRoot', {
screen: 'LnurlPay',
params: {
lnurl: data,
walletID: walletID || wallet.getID(),
},
const processLnurlPay = (data: string): void => {
navigate('LnurlPay', {
lnurl: data,
walletID: walletID || wallet?.getID() || '',
});
};
const pay = async () => {
if (!decoded) {
if (!decoded || !wallet || !amount || !invoice) {
return null;
}
@ -167,22 +175,22 @@ const ScanLndInvoice = () => {
}
}
let amountSats = amount;
let amountSats: number = parseInt(amount, 10);
switch (unit) {
case BitcoinUnit.SATS:
amountSats = parseInt(amountSats, 10); // nop
// amount is already in sats
break;
case BitcoinUnit.BTC:
amountSats = btcToSatoshi(amountSats);
amountSats = btcToSatoshi(amount);
break;
case BitcoinUnit.LOCAL_CURRENCY:
amountSats = btcToSatoshi(fiatToBTC(amountSats));
amountSats = btcToSatoshi(fiatToBTC(Number(amount)));
break;
}
setIsLoading(true);
const newExpiresIn = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms
if (+new Date() > newExpiresIn) {
const expiryTimeMs = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms
if (+new Date() > expiryTimeMs) {
setIsLoading(false);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
return presentAlert({ message: loc.lnd.errorInvoiceExpired });
@ -197,7 +205,7 @@ const ScanLndInvoice = () => {
try {
await wallet.payInvoice(invoice, amountSats);
} catch (Err) {
} catch (Err: any) {
console.log(Err.message);
setIsLoading(false);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
@ -212,7 +220,7 @@ const ScanLndInvoice = () => {
fetchAndSaveWalletTransactions(wallet.getID());
};
const processTextForInvoice = text => {
const processTextForInvoice = (text: string): void => {
if (
(text && text.toLowerCase().startsWith('lnb')) ||
text.toLowerCase().startsWith('lightning:lnb') ||
@ -227,7 +235,7 @@ const ScanLndInvoice = () => {
}
};
const shouldDisablePayButton = () => {
const shouldDisablePayButton = (): boolean => {
if (!decoded) {
return true;
} else {
@ -235,16 +243,15 @@ const ScanLndInvoice = () => {
return true;
}
}
return !(amount > 0);
// return decoded.num_satoshis <= 0 || isLoading || isNaN(decoded.num_satoshis);
return !(parseInt(amount, 10) > 0);
};
const naviageToSelectWallet = () => {
const naviageToSelectWallet = (): void => {
navigate('SelectWallet', { onWalletSelect, chainType: Chain.OFFCHAIN });
};
const renderWalletSelectionButton = () => {
if (renderWalletSelectionButtonHidden) return;
const renderWalletSelectionButton = (): JSX.Element | undefined => {
if (renderWalletSelectionButtonHidden || !wallet) return;
const walletLabel = wallet.getLabel();
return (
<View style={styles.walletSelectRoot}>
@ -267,25 +274,27 @@ const ScanLndInvoice = () => {
);
};
const getFees = () => {
const min = Math.floor(decoded.num_satoshis * 0.003);
const max = Math.floor(decoded.num_satoshis * 0.01) + 1;
const getFees = (): string => {
if (!decoded) return '';
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}`;
};
const onBlur = () => {
const onBlur = (): void => {
processTextForInvoice(destination);
};
const onWalletSelect = selectedWallet => {
const onWalletSelect = (selectedWallet: TWallet): void => {
setParams({ walletID: selectedWallet.getID() });
pop();
};
const onBarScanned = useCallback(
value => {
(value: string): void => {
if (!value) return;
DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => {
DeeplinkSchemaMatch.navigationRouteFor({ url: value }, (completionValue: any) => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
navigate(...completionValue);
});
@ -301,6 +310,12 @@ const ScanLndInvoice = () => {
}
}, [navigate, onBarScanned, route.params?.onBarScanned, setParams]);
const onChangeText = (text: string): void => {
const trimmedText = text.trim();
setDestination(trimmedText);
processTextForInvoice(trimmedText);
};
if (wallet === undefined || !wallet) {
return (
<View style={[styles.loadingIndicator, stylesHook.root]}>
@ -334,11 +349,7 @@ const ScanLndInvoice = () => {
<BlueCard>
<AddressInput
onChangeText={text => {
text = text.trim();
setDestination(text);
}}
onBarScanned={data => processTextForInvoice(data.data)}
onChangeText={onChangeText}
address={destination}
isLoading={isLoading}
placeholder={loc.lnd.placeholder}
@ -381,7 +392,7 @@ const ScanLndInvoice = () => {
);
};
export default ScanLndInvoice;
export default ScanLNDInvoice;
const styles = StyleSheet.create({
walletSelectRoot: {

View file

@ -109,7 +109,7 @@ const LNDCreateInvoice = () => {
if (reply.tag === Lnurl.TAG_PAY_REQUEST) {
// we are here by mistake. user wants to SEND to lnurl-pay, but he is on a screen that creates
// invoices (including through lnurl-withdraw)
navigate('ScanLndInvoiceRoot', {
navigate('ScanLNDInvoiceRoot', {
screen: 'LnurlPay',
params: {
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);
}
navigate('ScanLndInvoiceRoot', {
navigate('ScanLNDInvoiceRoot', {
screen: 'LnurlPaySuccess',
params: {
paymentHash: decoded.payment_hash,

View file

@ -109,7 +109,7 @@ const LnurlPaySuccess: React.FC = () => {
{repeatable ? (
<Button
onPress={() => {
navigate('ScanLndInvoiceRoot', {
navigate('ScanLNDInvoiceRoot', {
screen: 'LnurlPay',
params: {
// @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 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 Base43 from '../../blue_modules/base43';
import * as fs from '../../blue_modules/fs';
@ -12,7 +12,6 @@ import Button from '../../components/Button';
import { useTheme } from '../../components/themes';
import { isCameraAuthorizationStatusGranted } from '../../helpers/scan-qr';
import loc from '../../loc';
import { useSettings } from '../../hooks/context/useSettings';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import CameraScreen from '../../components/CameraScreen';
import SafeArea from '../../components/SafeArea';
@ -57,7 +56,6 @@ const styles = StyleSheet.create({
const ScanQRCode = () => {
const [isLoading, setIsLoading] = useState(false);
const { setIsDrawerShouldHide } = useSettings();
const navigation = useExtendedNavigation();
const route = useRoute<RouteProps>();
const navigationState = navigation.getState();
@ -96,16 +94,6 @@ const ScanQRCode = () => {
return createHash('sha256').update(s).digest().toString('hex');
};
useFocusEffect(
useCallback(() => {
setIsDrawerShouldHide(true);
return () => {
setIsDrawerShouldHide(false);
};
}, [setIsDrawerShouldHide]),
);
const _onReadUniformResourceV2 = (part: string) => {
if (!decoder) decoder = new BlueURDecoder();
try {

View file

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

View file

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

View file

@ -7,9 +7,11 @@ import {
Alert,
I18nManager,
Animated,
LayoutAnimation,
FlatList,
ActivityIndicator,
LayoutAnimation,
UIManager,
Platform,
} from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { useFocusEffect, usePreventRemove } from '@react-navigation/native';
@ -99,19 +101,21 @@ type Action =
interface State {
searchQuery: string;
isSearchFocused: boolean;
order: Item[];
tempOrder: Item[];
wallets: TWallet[];
originalWalletsOrder: Item[];
currentWalletsOrder: Item[];
availableWallets: TWallet[];
txMetadata: TTXMetadata;
initialWalletsBackup: TWallet[];
}
const initialState: State = {
searchQuery: '',
isSearchFocused: false,
order: [],
tempOrder: [],
wallets: [],
originalWalletsOrder: [],
currentWalletsOrder: [],
availableWallets: [],
txMetadata: {},
initialWalletsBackup: [],
};
const deepCopyWallets = (wallets: TWallet[]): TWallet[] => {
@ -131,21 +135,22 @@ const reducer = (state: State, action: Action): State => {
}));
return {
...state,
wallets: action.payload.wallets,
availableWallets: action.payload.wallets,
txMetadata: action.payload.txMetadata,
order: initialWalletsOrder,
tempOrder: initialWalletsOrder,
originalWalletsOrder: initialWalletsOrder,
currentWalletsOrder: initialWalletsOrder,
initialWalletsBackup: deepCopyWallets(action.payload.wallets),
};
}
case SET_FILTERED_ORDER: {
const query = action.payload.toLowerCase();
const filteredWallets = state.wallets
const filteredWallets = state.availableWallets
.filter(wallet => wallet.getLabel()?.toLowerCase().includes(query))
.map(wallet => ({ type: ItemType.WalletSection, data: wallet }));
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
.getTransactions()
.filter((tx: Transaction) =>
@ -158,14 +163,16 @@ const reducer = (state: State, action: Action): State => {
return {
...state,
tempOrder: filteredOrder,
currentWalletsOrder: filteredOrder,
};
}
case SAVE_CHANGES: {
const savedWallets = deepCopyWallets(action.payload);
return {
...state,
wallets: deepCopyWallets(action.payload),
tempOrder: state.tempOrder.map(item =>
availableWallets: savedWallets,
initialWalletsBackup: savedWallets,
currentWalletsOrder: state.currentWalletsOrder.map(item =>
item.type === ItemType.WalletSection
? { ...item, data: action.payload.find(wallet => wallet.getID() === item.data.getID())! }
: item,
@ -173,13 +180,15 @@ const reducer = (state: State, action: Action): State => {
};
}
case SET_TEMP_ORDER: {
return { ...state, tempOrder: action.payload };
return { ...state, currentWalletsOrder: action.payload };
}
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 {
...state,
tempOrder: updatedOrder,
currentWalletsOrder: updatedOrder,
};
}
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 { colors, closeImage } = useTheme();
const { wallets: storedWallets, setWalletsWithNewOrder, txMetadata, handleWalletDeletion } = useStorage();
const { wallets: persistedWallets, setWalletsWithNewOrder, txMetadata, handleWalletDeletion } = useStorage();
const { setIsDrawerShouldHide } = useSettings();
const walletsRef = useRef<TWallet[]>(deepCopyWallets(storedWallets)); // Create a deep copy of wallets for the DraggableFlatList
const { navigate, setOptions, goBack } = useExtendedNavigation();
const initialWalletsRef = useRef<TWallet[]>(deepCopyWallets(persistedWallets));
const { navigate, setOptions, goBack, dispatch: navigationDispatch } = useExtendedNavigation();
const [state, dispatch] = useReducer(reducer, initialState);
const debouncedSearchQuery = useDebounce(state.searchQuery, 300);
const bounceAnim = useBounceAnimation(state.searchQuery);
@ -204,76 +217,123 @@ const ManageWallets: React.FC = () => {
color: colors.foregroundColor,
},
};
const [data, setData] = useState(state.tempOrder);
const [uiData, setUiData] = useState(state.currentWalletsOrder);
const listRef = useRef<FlatList<Item> | null>(null);
const [saveInProgress, setSaveInProgress] = useState(false);
useEffect(() => {
setData(state.tempOrder);
}, [state.tempOrder]);
setUiData(state.currentWalletsOrder);
}, [state.currentWalletsOrder]);
useEffect(() => {
dispatch({ type: SET_INITIAL_ORDER, payload: { wallets: walletsRef.current, txMetadata } });
dispatch({ type: SET_INITIAL_ORDER, payload: { wallets: initialWalletsRef.current, txMetadata } });
}, [txMetadata]);
useEffect(() => {
if (debouncedSearchQuery) {
dispatch({ type: SET_FILTERED_ORDER, payload: debouncedSearchQuery });
} 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(() => {
return JSON.stringify(walletsRef.current) !== JSON.stringify(state.tempOrder.map(item => item.data));
}, [state.tempOrder]);
const currentWalletIds = state.currentWalletsOrder
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
.map(item => item.data.getID());
usePreventRemove(hasUnsavedChanges, async () => {
await new Promise<void>(resolve => {
Alert.alert(loc._.discard_changes, loc._.discard_changes_explain, [
{ text: loc._.cancel, style: 'cancel', onPress: () => resolve() },
{ text: loc._.ok, style: 'default', onPress: () => resolve() },
]);
});
const originalWalletIds = state.initialWalletsBackup.map(wallet => wallet.getID());
if (currentWalletIds.length !== originalWalletIds.length) {
return true;
}
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(() => {
if (state.searchQuery.length === 0 && !state.isSearchFocused) {
const newWalletOrder = state.tempOrder
const reorderedWallets = state.currentWalletsOrder
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
.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);
state.tempOrder.forEach(item => {
if (item.type === ItemType.WalletSection && !newWalletOrder.some(wallet => wallet.getID() === item.data.getID())) {
handleWalletDeletion(item.data.getID());
}
walletsToDelete.forEach(wallet => {
handleWalletDeletion(wallet.getID());
});
goBack();
setSaveInProgress(true);
} else {
dispatch({ type: SET_SEARCH_QUERY, payload: '' });
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(
() => (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.close}
style={styles.button}
style={[styles.button, buttonOpacity]}
onPress={goBack}
disabled={saveInProgress}
testID="NavigationCloseButton"
>
<Image source={closeImage} />
</TouchableOpacity>
),
[goBack, closeImage],
[buttonOpacity, goBack, saveInProgress, closeImage],
);
const SaveButton = useMemo(
@ -290,7 +350,6 @@ const ManageWallets: React.FC = () => {
onBlur: () => dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false }),
placeholder: loc.wallets.manage_wallets_search_placeholder,
};
setOptions({
headerLeft: () => HeaderLeftButton,
headerRight: () => SaveButton,
@ -329,19 +388,30 @@ const ManageWallets: React.FC = () => {
[bounceAnim],
);
const handleDeleteWallet = useCallback(
async (wallet: TWallet) => {
const deletionSucceeded = await handleWalletDeletion(wallet.getID());
if (deletionSucceeded) {
dispatch({ type: REMOVE_WALLET, payload: wallet.getID() });
}
},
[handleWalletDeletion],
);
const handleDeleteWallet = useCallback(async (wallet: TWallet) => {
LayoutAnimation.configureNext({
duration: 300,
create: {
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity,
},
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(
(wallet: TWallet) => {
const updatedOrder = state.tempOrder.map(item => {
const updatedOrder = state.currentWalletsOrder.map(item => {
if (item.type === ItemType.WalletSection && item.data.getID() === wallet.getID()) {
item.data.hideBalance = !item.data.hideBalance;
return {
@ -351,11 +421,10 @@ const ManageWallets: React.FC = () => {
}
return item;
});
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
dispatch({ type: SET_TEMP_ORDER, payload: updatedOrder });
},
[state.tempOrder],
[state.currentWalletsOrder],
);
const navigateToWallet = useCallback(
@ -373,13 +442,19 @@ const ManageWallets: React.FC = () => {
const renderItem = useCallback(
(info: DragListRenderItemInfo<Item>) => {
const { item, onDragStart, isActive } = info;
const compatibleState = {
wallets: state.availableWallets,
searchQuery: state.searchQuery,
};
return (
<ManageWalletsListItem
item={item}
onPressIn={undefined}
onPressOut={undefined}
isDraggingDisabled={state.searchQuery.length > 0 || state.isSearchFocused}
state={state}
state={compatibleState}
navigateToWallet={navigateToWallet}
renderHighlightedText={renderHighlightedText}
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(
(fromIndex: number, toIndex: number) => {
const copy = [...state.order];
const removed = copy.splice(fromIndex, 1);
copy.splice(toIndex, 0, removed[0]);
const updatedOrder = [...state.currentWalletsOrder];
const removed = updatedOrder.splice(fromIndex, 1);
updatedOrder.splice(toIndex, 0, removed[0]);
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
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,
},
});
dispatch({ type: SET_TEMP_ORDER, payload: updatedOrder });
},
[state.order, state.txMetadata],
[state.currentWalletsOrder],
);
const keyExtractor = useCallback((item: Item, index: number) => index.toString(), []);
const renderHeader = useMemo(() => {
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]) =>
tx.memo?.toLowerCase().includes(state.searchQuery.toLowerCase()),
);
@ -425,7 +500,7 @@ const ManageWallets: React.FC = () => {
!hasWallets &&
!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 (
<Suspense fallback={<ActivityIndicator size="large" color={colors.brandingColor} />}>
@ -437,7 +512,7 @@ const ManageWallets: React.FC = () => {
automaticallyAdjustKeyboardInsets
automaticallyAdjustsScrollIndicatorInsets
contentInsetAdjustmentBehavior="automatic"
data={data}
data={uiData}
containerStyle={[{ backgroundColor: colors.background }, styles.root]}
keyExtractor={keyExtractor}
onReordered={onReordered}
@ -460,35 +535,37 @@ const styles = StyleSheet.create({
padding: 16,
},
noResultsText: {
fontSize: 19,
fontWeight: 'bold',
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
textAlign: 'center',
justifyContent: 'center',
marginTop: 34,
},
highlightedContainer: {
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: {
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
fontWeight: 'bold',
fontSize: 19,
},
dimmedText: {
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 { RouteProp, useFocusEffect, useRoute, usePreventRemove, CommonActions } from '@react-navigation/native';
import { RouteProp, useFocusEffect, useRoute, usePreventRemove, StackActions } from '@react-navigation/native';
import {
ActivityIndicator,
Alert,
@ -18,7 +18,15 @@ import {
import { Badge, Icon } from '@rneui/themed';
import { isDesktop } from '../../blue_modules/environment';
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 presentAlert from '../../components/Alert';
import BottomModal, { BottomModalHandle } from '../../components/BottomModal';
@ -40,14 +48,14 @@ import { useStorage } from '../../hooks/context/useStorage';
import ToolTipMenu from '../../components/TooltipMenu';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { useSettings } from '../../hooks/context/useSettings';
import { ViewEditMultisigCosignersStackParamList } from '../../navigation/ViewEditMultisigCosignersStack';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import SafeArea from '../../components/SafeArea';
import { TWallet } from '../../class/wallets/types';
import { AddressInputScanButton } from '../../components/AddressInputScanButton';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
type RouteParams = RouteProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>;
type NavigationProp = NativeStackNavigationProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>;
type RouteParams = RouteProp<DetailViewStackParamList, 'ViewEditMultisigCosigners'>;
type NavigationProp = NativeStackNavigationProp<DetailViewStackParamList, 'ViewEditMultisigCosigners'>;
const ViewEditMultisigCosigners: React.FC = () => {
const hasLoaded = useRef(false);
@ -169,9 +177,11 @@ const ViewEditMultisigCosigners: React.FC = () => {
setIsSaveButtonDisabled(true);
setWalletsWithNewOrder(newWallets);
setTimeout(() => {
dispatch(
CommonActions.navigate({ name: 'WalletTransactions', params: { walletID: wallet.getID(), walletType: MultisigHDWallet.type } }),
);
const popTo = StackActions.popTo('WalletTransactions', {
walletID,
walletType: wallet.type,
});
dispatch(popTo);
}, 500);
}, 100);
};
@ -560,6 +570,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
{!isLoading && (
<>
<BlueSpacing40 />
<AddressInputScanButton
beforePress={async () => {
await provideMnemonicsModalRef.current?.dismiss();
@ -568,7 +579,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
type="link"
onChangeText={setImportText}
/>
<BlueSpacing20 />
<BlueSpacing40 />
</>
)}
</>

View file

@ -95,7 +95,8 @@ const WalletDetails: React.FC = () => {
} else {
setIsLoading(false);
}
}, [handleWalletDeletion, wallet]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const presentWalletHasBalanceAlert = useCallback(async () => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning);
@ -305,11 +306,8 @@ const WalletDetails: React.FC = () => {
});
};
const navigateToViewEditCosigners = () => {
navigate('ViewEditMultisigCosignersRoot', {
screen: 'ViewEditMultisigCosigners',
params: {
walletID,
},
navigate('ViewEditMultisigCosigners', {
walletID,
});
};
const navigateToXPub = () =>
@ -446,7 +444,7 @@ const WalletDetails: React.FC = () => {
return (
<>
<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
try {

View file

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

View file

@ -98,7 +98,7 @@ const WalletsList: React.FC = () => {
const { isLargeScreen } = useIsLargeScreen();
const walletsCarousel = useRef<any>();
const currentWalletIndex = useRef<number>(0);
const { setReloadTransactionsMenuActionFunction, clearReloadTransactionsMenuAction } = useMenuElements();
const { registerTransactionsHandler, unregisterTransactionsHandler } = useMenuElements();
const { wallets, getTransactions, getBalance, refreshAllWalletTransactions, setSelectedWalletID } = useStorage();
const { isTotalBalanceEnabled, isElectrumDisabled } = useSettings();
const { width } = useWindowDimensions();
@ -159,19 +159,41 @@ const WalletsList: React.FC = () => {
}
}, [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(
useCallback(() => {
const task = InteractionManager.runAfterInteractions(() => {
setReloadTransactionsMenuActionFunction(onRefresh);
verifyBalance();
setSelectedWalletID(undefined);
});
return () => {
task.cancel();
clearReloadTransactionsMenuAction();
};
}, [onRefresh, setReloadTransactionsMenuActionFunction, clearReloadTransactionsMenuAction, verifyBalance, setSelectedWalletID]),
}, [verifyBalance, setSelectedWalletID]),
);
useEffect(() => {
@ -207,6 +229,7 @@ const WalletsList: React.FC = () => {
useEffect(() => {
refreshTransactions();
// es-lint-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`,
},
expected: [
'ScanLndInvoiceRoot',
'ScanLNDInvoiceRoot',
{
screen: 'ScanLndInvoice',
screen: 'ScanLNDInvoice',
params: {
uri: 'lightning:lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde',
},
@ -162,9 +162,9 @@ describe.each(['', '//'])('unit - DeepLinkSchemaMatch', function (suffix) {
url: `bluewallet:lightning:${suffix}lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde`,
},
expected: [
'ScanLndInvoiceRoot',
'ScanLNDInvoiceRoot',
{
screen: 'ScanLndInvoice',
screen: 'ScanLNDInvoice',
params: {
uri: 'lightning:lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde',
},
@ -248,9 +248,9 @@ describe.each(['', '//'])('unit - DeepLinkSchemaMatch', function (suffix) {
url: 'lnaddress@zbd.gg',
},
expected: [
'ScanLndInvoiceRoot',
'ScanLNDInvoiceRoot',
{
screen: 'ScanLndInvoice',
screen: 'ScanLNDInvoice',
params: {
uri: 'lnaddress@zbd.gg',
},
@ -518,13 +518,13 @@ describe.each(['', '//'])('unit - DeepLinkSchemaMatch', function (suffix) {
navigate: (...args) => {
navigateWasCalled = true;
assert.deepStrictEqual(args, [
'ScanLndInvoiceRoot',
'ScanLNDInvoiceRoot',
{
params: {
uri: 'lightning:LNBC1855790N1PNUPWSFPP5P5RVQJA067PV6NJQ3EFKLP78TN6MHUK842ZFGDCTXRDSGNTY765QDZ62PSKJEPQW3HJQSNPD36XJCEQFPHKUETEVFSKGEM9WGSRYVPJXSSZSNMJV3JHYGZFGSAZQARFVD4K2AR5V95KCMMJ9YCQZPUXQZ6GSP53E4EX9YTD2MGDN2C2CFA0J0SM3E7PVLPJ208H5LMYPNJMGZ7RLGS9QXPQYSGQ6GQMEQXJKKF2DHXJK8XQ4WGLM5NTE3RKEXGYQC6HYGFKS9SHHA6HL9X4339MXHNNQFSH7TS62PU8T9RSWTK6HQ4LV4GW3DPD25DQ8UQQYC909N',
walletID: 'bfcacb7288cf43c6c02a1154c432ec155b813798fa4e87cd2c1e5531d6363f71',
},
screen: 'ScanLndInvoice',
screen: 'ScanLNDInvoice',
},
]);
},