Compare commits

...

9 commits

Author SHA1 Message Date
Marcos Rodriguez Velez
5f18540ca7 FIX: UX in ManageWallets 2025-03-12 20:42:29 -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
14 changed files with 543 additions and 286 deletions

View file

@ -87,7 +87,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "7.1.4"
versionName "7.1.5"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}

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

@ -22,11 +22,11 @@ import ActionSheet from '../screen/ActionSheet';
import { useStorage } from './context/useStorage';
import RNQRGenerator from 'rn-qr-generator';
import presentAlert from '../components/Alert';
import useMenuElements from './useMenuElements';
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',

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

@ -1483,7 +1483,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.4;
MARKETING_VERSION = 7.1.5;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@ -1541,7 +1541,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.4;
MARKETING_VERSION = 7.1.5;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@ -1590,7 +1590,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.4;
MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -1633,7 +1633,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.4;
MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.Stickers;
@ -1683,7 +1683,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.4;
MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -1739,7 +1739,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.4;
MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.MarketWidget;
@ -1927,7 +1927,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.4;
MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -1980,7 +1980,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.4;
MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch.extension;
@ -2026,7 +2026,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.4;
MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -2075,7 +2075,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.1.4;
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

@ -364,7 +364,7 @@ const DetailViewStackScreensStack = () => {
options={navigationStyle({
headerBackVisible: false,
gestureEnabled: false,
presentation: 'containedModal',
presentation: 'fullScreenModal',
title: loc.wallets.manage_title,
statusBarStyle: 'auto',
})(theme)}

6
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "bluewallet",
"version": "7.1.4",
"version": "7.1.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bluewallet",
"version": "7.1.4",
"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",

View file

@ -1,6 +1,6 @@
{
"name": "bluewallet",
"version": "7.1.4",
"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

@ -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

@ -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>();
@ -384,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
}, []);