|
1 | | -import { useCallback, useEffect, useMemo, useRef } from 'react'; |
| 1 | +import { useEffect, useCallback } from 'react'; |
2 | 2 | import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; |
| 3 | +import { navigationRef } from '../NavigationService'; |
3 | 4 | import { CommonActions } from '@react-navigation/native'; |
4 | | -import * as NavigationService from '../NavigationService'; |
5 | | -import { useStorage } from './context/useStorage'; |
6 | 5 |
|
7 | 6 | /* |
8 | 7 | Hook for managing iPadOS and macOS menu actions with keyboard shortcuts. |
9 | | -Uses MenuElementsEmitter for event handling. |
| 8 | +Uses MenuElementsEmitter for event handling and navigation state. |
10 | 9 | */ |
11 | 10 |
|
12 | | -type MenuEventHandler = () => void; |
| 11 | +type MenuActionHandler = () => void; |
13 | 12 |
|
| 13 | +// Singleton setup - initialize once at module level |
14 | 14 | const { MenuElementsEmitter } = NativeModules; |
15 | 15 | let eventEmitter: NativeEventEmitter | null = null; |
| 16 | +let listenersInitialized = false; |
16 | 17 |
|
17 | | -let globalReloadTransactionsFunction: MenuEventHandler | null = null; |
| 18 | +// Registry for transaction handlers by screen ID |
| 19 | +const handlerRegistry = new Map<string, MenuActionHandler>(); |
18 | 20 |
|
19 | | -// Only create the emitter if the module exists and we're on iOS/macOS |
| 21 | +// Store subscription references for proper cleanup |
| 22 | +let subscriptions: { remove: () => void }[] = []; |
| 23 | + |
| 24 | +// Create a more robust emitter with error handling |
20 | 25 | try { |
21 | | - if ((Platform.OS === 'ios' || Platform.OS === 'macos') && MenuElementsEmitter) { |
| 26 | + if (Platform.OS === 'ios' && MenuElementsEmitter) { |
22 | 27 | eventEmitter = new NativeEventEmitter(MenuElementsEmitter); |
23 | 28 | } |
24 | 29 | } catch (error) { |
| 30 | + console.warn('[MenuElements] Failed to initialize event emitter: ', error); |
25 | 31 | eventEmitter = null; |
26 | 32 | } |
27 | 33 |
|
28 | | -// Empty function that does nothing - used as default |
29 | | -const noop = () => {}; |
30 | | - |
31 | | -const useMenuElements = () => { |
32 | | - const { walletsInitialized } = useStorage(); |
33 | | - const reloadTransactionsMenuActionRef = useRef<MenuEventHandler>(noop); |
34 | | - // Track if listeners have been set up |
35 | | - const listenersInitialized = useRef<boolean>(false); |
36 | | - const listenersRef = useRef<any[]>([]); |
37 | | - |
38 | | - const setReloadTransactionsMenuActionFunction = useCallback((handler: MenuEventHandler) => { |
39 | | - if (typeof handler !== 'function') { |
| 34 | +/** |
| 35 | + * Safely navigate using multiple fallback approaches |
| 36 | + */ |
| 37 | +function safeNavigate(routeName: string, params?: Record<string, any>): void { |
| 38 | + try { |
| 39 | + if (navigationRef.current?.isReady()) { |
| 40 | + navigationRef.current.navigate(routeName as never, params as never); |
40 | 41 | return; |
41 | 42 | } |
42 | 43 |
|
43 | | - reloadTransactionsMenuActionRef.current = handler; |
44 | | - globalReloadTransactionsFunction = handler; |
45 | | - }, []); |
| 44 | + if (navigationRef.isReady()) { |
| 45 | + navigationRef.dispatch( |
| 46 | + CommonActions.navigate({ |
| 47 | + name: routeName, |
| 48 | + params, |
| 49 | + }), |
| 50 | + ); |
| 51 | + } |
| 52 | + } catch (error) { |
| 53 | + console.error(`[MenuElements] Navigation error:`, error); |
| 54 | + } |
| 55 | +} |
46 | 56 |
|
47 | | - const clearReloadTransactionsMenuAction = useCallback(() => { |
48 | | - reloadTransactionsMenuActionRef.current = noop; |
49 | | - }, []); |
| 57 | +// Cleanup event listeners to prevent memory leaks |
| 58 | +function cleanupListeners(): void { |
| 59 | + if (subscriptions.length > 0) { |
| 60 | + subscriptions.forEach(subscription => { |
| 61 | + try { |
| 62 | + subscription.remove(); |
| 63 | + } catch (e) { |
| 64 | + console.warn('[MenuElements] Error removing subscription:', e); |
| 65 | + } |
| 66 | + }); |
| 67 | + subscriptions = []; |
| 68 | + listenersInitialized = false; |
| 69 | + } |
| 70 | +} |
50 | 71 |
|
51 | | - const dispatchNavigate = useCallback((routeName: string, screen?: string) => { |
52 | | - try { |
53 | | - NavigationService.dispatch(CommonActions.navigate({ name: routeName, params: screen ? { screen } : undefined })); |
54 | | - } catch (error) { |
55 | | - // Navigation failed silently |
56 | | - } |
57 | | - }, []); |
| 72 | +function initializeListeners(): void { |
| 73 | + if (!eventEmitter || listenersInitialized) return; |
58 | 74 |
|
59 | | - const eventActions = useMemo( |
60 | | - () => ({ |
61 | | - openSettings: () => { |
62 | | - dispatchNavigate('Settings'); |
63 | | - }, |
64 | | - addWallet: () => { |
65 | | - dispatchNavigate('AddWalletRoot'); |
66 | | - }, |
67 | | - importWallet: () => { |
68 | | - dispatchNavigate('AddWalletRoot', 'ImportWallet'); |
69 | | - }, |
70 | | - reloadTransactions: () => { |
71 | | - try { |
72 | | - const handler = reloadTransactionsMenuActionRef.current || globalReloadTransactionsFunction || noop; |
73 | | - handler(); |
74 | | - } catch (error) { |
75 | | - // Execution failed silently |
76 | | - } |
77 | | - }, |
78 | | - }), |
79 | | - [dispatchNavigate], |
80 | | - ); |
| 75 | + cleanupListeners(); |
81 | 76 |
|
82 | | - useEffect(() => { |
83 | | - // Skip if emitter doesn't exist or wallets aren't initialized yet |
84 | | - if (!eventEmitter || !walletsInitialized) { |
85 | | - return; |
86 | | - } |
| 77 | + // Navigation actions |
| 78 | + const globalActions = { |
| 79 | + navigateToSettings: (): void => { |
| 80 | + safeNavigate('Settings'); |
| 81 | + }, |
87 | 82 |
|
88 | | - if (listenersInitialized.current) { |
89 | | - return; |
90 | | - } |
| 83 | + navigateToAddWallet: (): void => { |
| 84 | + safeNavigate('AddWalletRoot'); |
| 85 | + }, |
| 86 | + |
| 87 | + navigateToImportWallet: (): void => { |
| 88 | + safeNavigate('AddWalletRoot', { screen: 'ImportWallet' }); |
| 89 | + }, |
| 90 | + |
| 91 | + executeReloadTransactions: (): void => { |
| 92 | + const currentRoute = navigationRef.current?.getCurrentRoute(); |
| 93 | + if (!currentRoute) return; |
| 94 | + |
| 95 | + const screenName = currentRoute.name; |
| 96 | + const params = (currentRoute.params as { walletID?: string }) || {}; |
| 97 | + const walletID = params.walletID; |
| 98 | + |
| 99 | + const specificKey = walletID ? `${screenName}-${walletID}` : null; |
| 100 | + |
| 101 | + const specificHandler = specificKey ? handlerRegistry.get(specificKey) : undefined; |
| 102 | + const genericHandler = handlerRegistry.get(screenName); |
| 103 | + const handler = specificHandler || genericHandler; |
91 | 104 |
|
92 | | - try { |
93 | | - if (listenersRef.current.length > 0) { |
94 | | - listenersRef.current.forEach(listener => listener?.remove?.()); |
95 | | - listenersRef.current = []; |
| 105 | + if (typeof handler === 'function') { |
| 106 | + handler(); |
96 | 107 | } |
| 108 | + }, |
| 109 | + }; |
97 | 110 |
|
98 | | - eventEmitter.removeAllListeners('openSettings'); |
99 | | - eventEmitter.removeAllListeners('addWalletMenuAction'); |
100 | | - eventEmitter.removeAllListeners('importWalletMenuAction'); |
101 | | - eventEmitter.removeAllListeners('reloadTransactionsMenuAction'); |
102 | | - } catch (error) { |
103 | | - // Error cleanup silently ignored |
104 | | - } |
| 111 | + try { |
| 112 | + subscriptions.push(eventEmitter.addListener('openSettings', globalActions.navigateToSettings)); |
| 113 | + subscriptions.push(eventEmitter.addListener('addWalletMenuAction', globalActions.navigateToAddWallet)); |
| 114 | + subscriptions.push(eventEmitter.addListener('importWalletMenuAction', globalActions.navigateToImportWallet)); |
| 115 | + subscriptions.push(eventEmitter.addListener('reloadTransactionsMenuAction', globalActions.executeReloadTransactions)); |
| 116 | + } catch (error) { |
| 117 | + console.error('[MenuElements] Error setting up event listeners:', error); |
| 118 | + } |
105 | 119 |
|
106 | | - try { |
107 | | - const listeners = [ |
108 | | - eventEmitter.addListener('openSettings', eventActions.openSettings), |
109 | | - eventEmitter.addListener('addWalletMenuAction', eventActions.addWallet), |
110 | | - eventEmitter.addListener('importWalletMenuAction', eventActions.importWallet), |
111 | | - eventEmitter.addListener('reloadTransactionsMenuAction', eventActions.reloadTransactions), |
112 | | - ]; |
113 | | - |
114 | | - listenersRef.current = listeners; |
115 | | - listenersInitialized.current = true; |
116 | | - } catch (error) { |
117 | | - // Listener setup failed silently |
118 | | - } |
| 120 | + listenersInitialized = true; |
| 121 | +} |
| 122 | + |
| 123 | +interface MenuElementsHook { |
| 124 | + registerTransactionsHandler: (handler: MenuActionHandler, screenKey?: string) => boolean; |
| 125 | + unregisterTransactionsHandler: (screenKey: string) => void; |
| 126 | + isMenuElementsSupported: boolean; |
| 127 | +} |
| 128 | + |
| 129 | +const mountedComponents = new Set<string>(); |
| 130 | + |
| 131 | +const useMenuElements = (): MenuElementsHook => { |
| 132 | + useEffect(() => { |
| 133 | + initializeListeners(); |
| 134 | + |
| 135 | + const unsubscribe = navigationRef.addListener('state', () => {}); |
119 | 136 |
|
120 | 137 | return () => { |
121 | | - try { |
122 | | - listenersRef.current.forEach(listener => { |
123 | | - if (listener && typeof listener.remove === 'function') { |
124 | | - listener.remove(); |
125 | | - } |
126 | | - }); |
127 | | - listenersRef.current = []; |
128 | | - listenersInitialized.current = false; |
129 | | - } catch (error) { |
130 | | - // Cleanup error silently ignored |
131 | | - } |
| 138 | + unsubscribe(); |
132 | 139 | }; |
133 | | - }, [walletsInitialized, eventActions]); |
| 140 | + }, []); |
| 141 | + |
| 142 | + const registerTransactionsHandler = useCallback((handler: MenuActionHandler, screenKey?: string): boolean => { |
| 143 | + if (typeof handler !== 'function') return false; |
| 144 | + |
| 145 | + const key = screenKey || navigationRef.current?.getCurrentRoute()?.name; |
| 146 | + if (!key) return false; |
| 147 | + |
| 148 | + mountedComponents.add(key); |
| 149 | + |
| 150 | + handlerRegistry.set(key, handler); |
| 151 | + |
| 152 | + return true; |
| 153 | + }, []); |
| 154 | + |
| 155 | + const unregisterTransactionsHandler = useCallback((screenKey: string): void => { |
| 156 | + if (!screenKey) return; |
| 157 | + |
| 158 | + handlerRegistry.delete(screenKey); |
| 159 | + mountedComponents.delete(screenKey); |
| 160 | + }, []); |
134 | 161 |
|
135 | 162 | return { |
136 | | - setReloadTransactionsMenuActionFunction, |
137 | | - clearReloadTransactionsMenuAction, |
| 163 | + registerTransactionsHandler, |
| 164 | + unregisterTransactionsHandler, |
138 | 165 | isMenuElementsSupported: !!eventEmitter, |
139 | 166 | }; |
140 | 167 | }; |
|
0 commit comments