Merge pull request #7661 from BlueWallet/menu

FIX: MenuElements for macOS and iPad were not firing on nav 7
This commit is contained in:
GLaDOS 2025-02-28 22:49:39 +00:00 committed by GitHub
commit b3ff1b7c3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 219 additions and 86 deletions

View file

@ -9,60 +9,133 @@ Hook for managing iPadOS and macOS menu actions with keyboard shortcuts.
Uses MenuElementsEmitter for event handling.
*/
type MenuEventHandler = () => void;
const { MenuElementsEmitter } = NativeModules;
const eventEmitter =
(Platform.OS === 'ios' || Platform.OS === 'macos') && MenuElementsEmitter ? new NativeEventEmitter(MenuElementsEmitter) : null;
let eventEmitter: NativeEventEmitter | null = null;
let globalReloadTransactionsFunction: MenuEventHandler | null = null;
// Only create the emitter if the module exists and we're on iOS/macOS
try {
if ((Platform.OS === 'ios' || Platform.OS === 'macos') && MenuElementsEmitter) {
eventEmitter = new NativeEventEmitter(MenuElementsEmitter);
}
} catch (error) {
eventEmitter = null;
}
// Empty function that does nothing - used as default
const noop = () => {};
const useMenuElements = () => {
const { walletsInitialized } = useStorage();
const reloadTransactionsMenuActionRef = useRef<() => void>(() => {});
const reloadTransactionsMenuActionRef = useRef<MenuEventHandler>(noop);
// Track if listeners have been set up
const listenersInitialized = useRef<boolean>(false);
const listenersRef = useRef<any[]>([]);
const setReloadTransactionsMenuActionFunction = useCallback((newFunction: () => void) => {
console.debug('Setting reloadTransactionsMenuActionFunction.');
reloadTransactionsMenuActionRef.current = newFunction;
const setReloadTransactionsMenuActionFunction = useCallback((handler: MenuEventHandler) => {
if (typeof handler !== 'function') {
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
}
}, []);
const eventActions = useMemo(
() => ({
openSettings: () => dispatchNavigate('Settings'),
addWallet: () => dispatchNavigate('AddWalletRoot'),
importWallet: () => dispatchNavigate('AddWalletRoot', 'ImportWallet'),
openSettings: () => {
dispatchNavigate('Settings');
},
addWallet: () => {
dispatchNavigate('AddWalletRoot');
},
importWallet: () => {
dispatchNavigate('AddWalletRoot', 'ImportWallet');
},
reloadTransactions: () => {
console.debug('Calling reloadTransactionsMenuActionFunction');
reloadTransactionsMenuActionRef.current?.();
try {
const handler = reloadTransactionsMenuActionRef.current || globalReloadTransactionsFunction || noop;
handler();
} catch (error) {
// Execution failed silently
}
},
}),
[dispatchNavigate],
);
useEffect(() => {
if (!walletsInitialized || !eventEmitter) return;
// Skip if emitter doesn't exist or wallets aren't initialized yet
if (!eventEmitter || !walletsInitialized) {
return;
}
console.debug('Setting up menu event listeners');
if (listenersInitialized.current) {
return;
}
try {
if (listenersRef.current.length > 0) {
listenersRef.current.forEach(listener => listener?.remove?.());
listenersRef.current = [];
}
// Add permanent listeners only once
eventEmitter.removeAllListeners('openSettings');
eventEmitter.removeAllListeners('addWalletMenuAction');
eventEmitter.removeAllListeners('importWalletMenuAction');
eventEmitter.removeAllListeners('reloadTransactionsMenuAction');
} catch (error) {
// Error cleanup silently ignored
}
eventEmitter.addListener('openSettings', eventActions.openSettings);
eventEmitter.addListener('addWalletMenuAction', eventActions.addWallet);
eventEmitter.addListener('importWalletMenuAction', eventActions.importWallet);
try {
const listeners = [
eventEmitter.addListener('openSettings', eventActions.openSettings),
eventEmitter.addListener('addWalletMenuAction', eventActions.addWallet),
eventEmitter.addListener('importWalletMenuAction', eventActions.importWallet),
eventEmitter.addListener('reloadTransactionsMenuAction', eventActions.reloadTransactions),
];
const reloadTransactionsListener = eventEmitter.addListener('reloadTransactionsMenuAction', eventActions.reloadTransactions);
listenersRef.current = listeners;
listenersInitialized.current = true;
} catch (error) {
// Listener setup failed silently
}
return () => {
console.debug('Removing reloadTransactionsMenuAction listener');
reloadTransactionsListener.remove();
try {
listenersRef.current.forEach(listener => {
if (listener && typeof listener.remove === 'function') {
listener.remove();
}
});
listenersRef.current = [];
listenersInitialized.current = false;
} catch (error) {
// Cleanup error silently ignored
}
};
}, [walletsInitialized, eventActions]);
return {
setReloadTransactionsMenuActionFunction,
clearReloadTransactionsMenuAction,
isMenuElementsSupported: !!eventEmitter,
};
};

View file

@ -1,8 +1,8 @@
const useMenuElements = () => {
const setReloadTransactionsMenuActionFunction = (_: () => void) => {};
return {
setReloadTransactionsMenuActionFunction,
setReloadTransactionsMenuActionFunction: (_func: any) => {},
clearReloadTransactionsMenuAction: () => {},
isMenuElementsSupported: true,
};
};

View file

@ -45,6 +45,8 @@
782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B9D9B3A7B2CB4255876B67AF /* libz.tbd */; };
849047CA2702A32A008EE567 /* Handoff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849047C92702A32A008EE567 /* Handoff.swift */; };
84E05A842721191B001A0D3A /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 84E05A832721191B001A0D3A /* Settings.bundle */; };
B409AB042D71DFAA00BA06F8 /* MenuElementsEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = B409AB032D71DFAA00BA06F8 /* MenuElementsEmitter.m */; };
B409AB062D71E07500BA06F8 /* MenuElementsEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B409AB052D71E07500BA06F8 /* MenuElementsEmitter.swift */; };
B40D4E34225841EC00428FCC /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E32225841EC00428FCC /* Interface.storyboard */; };
B40D4E36225841ED00428FCC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E35225841ED00428FCC /* Assets.xcassets */; };
B40D4E3D225841ED00428FCC /* BlueWalletWatch Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = B40D4E3C225841ED00428FCC /* BlueWalletWatch Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@ -156,14 +158,13 @@
B4B1A4642BFA73110072E3BB /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */; };
B4B3EC222D69FF6C00327F3D /* CustomSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EC202D69FF6C00327F3D /* CustomSegmentedControl.swift */; };
B4B3EC252D69FF8700327F3D /* EventEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EC232D69FF8700327F3D /* EventEmitter.swift */; };
B4B3EC262D69FF8700327F3D /* MenuElementsEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EC242D69FF8700327F3D /* MenuElementsEmitter.swift */; };
B4D0B2622C1DEA11006B6B1B /* ReceivePageInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2612C1DEA11006B6B1B /* ReceivePageInterfaceController.swift */; };
B4D0B2642C1DEA99006B6B1B /* ReceiveType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2632C1DEA99006B6B1B /* ReceiveType.swift */; };
B4D0B2662C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2652C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift */; };
B4D0B2682C1DED67006B6B1B /* ReceiveMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2672C1DED67006B6B1B /* ReceiveMethod.swift */; };
B4EE583C226703320003363C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E35225841ED00428FCC /* Assets.xcassets */; };
B4EFF73B2C3F6C5E0095D655 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EFF73A2C3F6C5E0095D655 /* MockData.swift */; };
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; };
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -335,6 +336,8 @@
9F1F51A83D044F3BB26A35FC /* libRNSVG-tvOS.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = "libRNSVG-tvOS.a"; sourceTree = "<group>"; };
A7C4B1FDAD264618BAF8C335 /* libRNCWebView.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNCWebView.a; sourceTree = "<group>"; };
AB2325650CE04F018697ACFE /* libRNReactNativeHapticFeedback.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNReactNativeHapticFeedback.a; sourceTree = "<group>"; };
B409AB032D71DFAA00BA06F8 /* MenuElementsEmitter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = MenuElementsEmitter.m; path = MenuElementsEmitter/MenuElementsEmitter.m; sourceTree = SOURCE_ROOT; };
B409AB052D71E07500BA06F8 /* MenuElementsEmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MenuElementsEmitter.swift; path = MenuElementsEmitter/MenuElementsEmitter.swift; sourceTree = SOURCE_ROOT; };
B40D4E30225841EC00428FCC /* BlueWalletWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BlueWalletWatch.app; sourceTree = BUILT_PRODUCTS_DIR; };
B40D4E33225841EC00428FCC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = "<group>"; };
B40D4E35225841ED00428FCC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -396,7 +399,6 @@
B4B31A352C77BBA000663334 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Interface.strings; sourceTree = "<group>"; };
B4B3EC202D69FF6C00327F3D /* CustomSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSegmentedControl.swift; sourceTree = "<group>"; };
B4B3EC232D69FF8700327F3D /* EventEmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventEmitter.swift; sourceTree = "<group>"; };
B4B3EC242D69FF8700327F3D /* MenuElementsEmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuElementsEmitter.swift; sourceTree = "<group>"; };
B4D0B2612C1DEA11006B6B1B /* ReceivePageInterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceivePageInterfaceController.swift; sourceTree = "<group>"; };
B4D0B2632C1DEA99006B6B1B /* ReceiveType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveType.swift; sourceTree = "<group>"; };
B4D0B2652C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveInterfaceMode.swift; sourceTree = "<group>"; };
@ -426,7 +428,7 @@
files = (
782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */,
764B49B1420D4AEB8109BF62 /* libsqlite3.0.tbd in Frameworks */,
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */,
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */,
17CDA0718F42DB2CE856C872 /* libPods-BlueWallet.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -673,6 +675,15 @@
name = Products;
sourceTree = "<group>";
};
B409AB072D71E07C00BA06F8 /* MenuElementsEmitter */ = {
isa = PBXGroup;
children = (
B409AB052D71E07500BA06F8 /* MenuElementsEmitter.swift */,
B409AB032D71DFAA00BA06F8 /* MenuElementsEmitter.m */,
);
path = MenuElementsEmitter;
sourceTree = "<group>";
};
B40D4E31225841EC00428FCC /* BlueWalletWatch */ = {
isa = PBXGroup;
children = (
@ -803,9 +814,9 @@
B45010A12C1504E900619044 /* Components */ = {
isa = PBXGroup;
children = (
B409AB072D71E07C00BA06F8 /* MenuElementsEmitter */,
B44305BD2D6A04B9004675CC /* SegmentedControl */,
B4B3EC232D69FF8700327F3D /* EventEmitter.swift */,
B4B3EC242D69FF8700327F3D /* MenuElementsEmitter.swift */,
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */,
);
path = Components;
@ -1231,12 +1242,13 @@
B49A28C12CD199FC006B08E4 /* SwiftTCPClient.swift in Sources */,
B44033FE2BCC37D700162242 /* MarketAPI.swift in Sources */,
B450109C2C0FCD8A00619044 /* Utilities.swift in Sources */,
B409AB042D71DFAA00BA06F8 /* MenuElementsEmitter.m in Sources */,
B48630E52CCEE8B800A8425C /* PriceView.swift in Sources */,
B48630E72CCEE91900A8425C /* PriceWidgetProvider.swift in Sources */,
B4B3EC252D69FF8700327F3D /* EventEmitter.swift in Sources */,
B4B3EC262D69FF8700327F3D /* MenuElementsEmitter.swift in Sources */,
B49A28C02CD199C7006B08E4 /* MarketAPI+Electrum.swift in Sources */,
B48630ED2CCEEEB000A8425C /* WalletAppShortcuts.swift in Sources */,
B409AB062D71E07500BA06F8 /* MenuElementsEmitter.swift in Sources */,
B44033CE2BCC352900162242 /* UserDefaultsGroup.swift in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
B461B852299599F800E431AA /* AppDelegate.mm in Sources */,

View file

@ -254,22 +254,51 @@
}
- (void)openSettings:(UIKeyCommand *)keyCommand {
[MenuElementsEmitter.shared openSettings];
// Safely access the MenuElementsEmitter
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
if (emitter) {
dispatch_async(dispatch_get_main_queue(), ^{
[emitter openSettings];
});
} else {
NSLog(@"MenuElementsEmitter not available");
}
}
- (void)addWalletAction:(UIKeyCommand *)keyCommand {
[MenuElementsEmitter.shared addWalletMenuAction];
NSLog(@"Add Wallet action performed");
// Safely access the MenuElementsEmitter
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
if (emitter) {
dispatch_async(dispatch_get_main_queue(), ^{
[emitter addWalletMenuAction];
});
} else {
NSLog(@"MenuElementsEmitter not available");
}
}
- (void)importWalletAction:(UIKeyCommand *)keyCommand {
[MenuElementsEmitter.shared importWalletMenuAction];
NSLog(@"Import Wallet action performed");
// Safely access the MenuElementsEmitter
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
if (emitter) {
dispatch_async(dispatch_get_main_queue(), ^{
[emitter importWalletMenuAction];
});
} else {
NSLog(@"MenuElementsEmitter not available");
}
}
- (void)reloadTransactionsAction:(UIKeyCommand *)keyCommand {
[MenuElementsEmitter.shared reloadTransactionsMenuAction];
NSLog(@"Reload Transactions action performed");
// Safely access the MenuElementsEmitter
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
if (emitter) {
dispatch_async(dispatch_get_main_queue(), ^{
[emitter reloadTransactionsMenuAction];
});
} else {
NSLog(@"MenuElementsEmitter not available");
}
}
- (void)showHelp:(id)sender {

View file

@ -1,35 +0,0 @@
import Foundation
import React
@objc(MenuElementsEmitter)
class MenuElementsEmitter: RCTEventEmitter {
static let sharedInstance = MenuElementsEmitter()
override class func requiresMainQueueSetup() -> Bool {
return true
}
override func supportedEvents() -> [String]! {
return ["openSettings", "addWalletMenuAction", "importWalletMenuAction", "reloadTransactionsMenuAction"]
}
@objc static func shared() -> MenuElementsEmitter {
return sharedInstance
}
@objc func openSettings() {
sendEvent(withName: "openSettings", body: nil)
}
@objc func addWalletMenuAction() {
sendEvent(withName: "addWalletMenuAction", body: nil)
}
@objc func importWalletMenuAction() {
sendEvent(withName: "importWalletMenuAction", body: nil)
}
@objc func reloadTransactionsMenuAction() {
sendEvent(withName: "reloadTransactionsMenuAction", body: nil)
}
}

View file

@ -0,0 +1,13 @@
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface RCT_EXTERN_MODULE(MenuElementsEmitter, RCTEventEmitter)
RCT_EXTERN_METHOD(supportedEvents)
RCT_EXTERN_METHOD(openSettings)
RCT_EXTERN_METHOD(addWalletMenuAction)
RCT_EXTERN_METHOD(importWalletMenuAction)
RCT_EXTERN_METHOD(reloadTransactionsMenuAction)
RCT_EXTERN_METHOD(shared)
@end

View file

@ -3,7 +3,15 @@ import React
@objc(MenuElementsEmitter)
class MenuElementsEmitter: RCTEventEmitter {
static let sharedInstance = MenuElementsEmitter()
private static var _sharedInstance: MenuElementsEmitter?
private var hasListeners = false
override init() {
super.init()
MenuElementsEmitter._sharedInstance = self
NSLog("[MenuElements] Swift: Initialized MenuElementsEmitter instance")
}
override class func requiresMainQueueSetup() -> Bool {
return true
@ -13,23 +21,55 @@ class MenuElementsEmitter: RCTEventEmitter {
return ["openSettings", "addWalletMenuAction", "importWalletMenuAction", "reloadTransactionsMenuAction"]
}
@objc static func shared() -> MenuElementsEmitter {
return sharedInstance
@objc static func shared() -> MenuElementsEmitter? {
return _sharedInstance
}
override func startObserving() {
hasListeners = true
NSLog("[MenuElements] Swift: Started observing events")
}
override func stopObserving() {
hasListeners = false
NSLog("[MenuElements] Swift: Stopped observing events")
}
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)
}
} else {
NSLog("[MenuElements] Swift: Cannot emit %@ event. %@", name, !hasListeners ? "No listeners" : "Bridge not ready")
}
}
@objc func openSettings() {
sendEvent(withName: "openSettings", body: nil)
NSLog("[MenuElements] Swift: openSettings called")
safelyEmitEvent(withName: "openSettings")
}
@objc func addWalletMenuAction() {
sendEvent(withName: "addWalletMenuAction", body: nil)
NSLog("[MenuElements] Swift: addWalletMenuAction called")
safelyEmitEvent(withName: "addWalletMenuAction")
}
@objc func importWalletMenuAction() {
sendEvent(withName: "importWalletMenuAction", body: nil)
NSLog("[MenuElements] Swift: importWalletMenuAction called")
safelyEmitEvent(withName: "importWalletMenuAction")
}
@objc func reloadTransactionsMenuAction() {
sendEvent(withName: "reloadTransactionsMenuAction", body: nil)
NSLog("[MenuElements] Swift: reloadTransactionsMenuAction called")
safelyEmitEvent(withName: "reloadTransactionsMenuAction")
}
override func invalidate() {
NSLog("[MenuElements] Swift: Module invalidated")
MenuElementsEmitter._sharedInstance = nil
super.invalidate()
}
}

View file

@ -98,7 +98,7 @@ const WalletsList: React.FC = () => {
const { isLargeScreen } = useIsLargeScreen();
const walletsCarousel = useRef<any>();
const currentWalletIndex = useRef<number>(0);
const { setReloadTransactionsMenuActionFunction } = useMenuElements();
const { setReloadTransactionsMenuActionFunction, clearReloadTransactionsMenuAction } = useMenuElements();
const { wallets, getTransactions, getBalance, refreshAllWalletTransactions, setSelectedWalletID } = useStorage();
const { isTotalBalanceEnabled, isElectrumDisabled } = useSettings();
const { width } = useWindowDimensions();
@ -162,15 +162,16 @@ const WalletsList: React.FC = () => {
useFocusEffect(
useCallback(() => {
const task = InteractionManager.runAfterInteractions(() => {
setReloadTransactionsMenuActionFunction(() => onRefresh);
setReloadTransactionsMenuActionFunction(onRefresh);
verifyBalance();
setSelectedWalletID(undefined);
});
return () => {
task.cancel();
setReloadTransactionsMenuActionFunction(() => {});
clearReloadTransactionsMenuAction();
};
}, [onRefresh, setReloadTransactionsMenuActionFunction, verifyBalance, setSelectedWalletID]),
}, [onRefresh, setReloadTransactionsMenuActionFunction, clearReloadTransactionsMenuAction, verifyBalance, setSelectedWalletID]),
);
useEffect(() => {