diff --git a/wallets/index.js b/wallets/index.js index eec62b6aa..1986c6752 100644 --- a/wallets/index.js +++ b/wallets/index.js @@ -16,21 +16,26 @@ const WalletsContext = createContext({ wallets: [] }) +function loadWallet (walletDef, me) { + try { + const storageKey = getStorageKey(walletDef.name, me?.id) + const config = window.localStorage.getItem(storageKey) + return { def: walletDef, config: JSON.parse(config) } + } catch (e) { + // ignore SSR errors + return null + } +} + function useLocalWallets () { const { me } = useMe() const [wallets, setWallets] = useState([]) const loadWallets = useCallback(() => { // form wallets from local storage into a list of { config, def } - const wallets = walletDefs.map(w => { - try { - const storageKey = getStorageKey(w.name, me?.id) - const config = window.localStorage.getItem(storageKey) - return { def: w, config: JSON.parse(config) } - } catch (e) { - return null - } - }).filter(Boolean) + const wallets = walletDefs + .map(w => loadWallet(w, me)) + .filter(Boolean) setWallets(wallets) }, [me?.id, setWallets]) @@ -64,9 +69,30 @@ const walletDefsOnly = walletDefs.map(w => ({ def: w, config: {} })) export function WalletsProvider ({ children }) { const { isActive, decrypt } = useVault() const { me } = useMe() + const { wallets: localWallets, reloadLocalWallets, removeLocalWallets } = useLocalWallets() const [setWalletPriority] = useMutation(SET_WALLET_PRIORITY) const [serverWallets, setServerWallets] = useState([]) + + // XXX calling hooks in a loop is usually a bad idea + // we do this here anyway so we can return all wallets with their loggers included. + // we ensure hooks are always called in the same order by sorting the wallets by name. + // + // we don't use localWallets here because it is empty on first render + // so using it would change the order of the hooks between renders. + // + // see https://legacy.reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level + const loggers = walletDefs + .map(w => loadWallet(w, me)) + .filter(Boolean) + .sort((w1, w2) => w1.def.name.localeCompare(w2.def.name)) + .reduce((acc, w) => { + return { + ...acc, + [w.def.name]: useWalletLogger(w.def)?.logger + } + }, {}) + const client = useApolloClient() const { logs } = useWalletLogs() const [darkMode] = useDarkMode() @@ -110,6 +136,7 @@ export function WalletsProvider ({ children }) { }, [data?.wallets, decrypt, isActive]) // merge wallets on name like: { ...unconfigured, ...localConfig, ...serverConfig } + // also include the logger and the sendPayment function const wallets = useMemo(() => { const merged = {} for (const wallet of [...walletDefsOnly, ...localWallets, ...serverWallets]) { @@ -131,7 +158,7 @@ export function WalletsProvider ({ children }) { } } - // sort by priority, then add status field + // sort by priority, then add status field, logger and sendPayment function return Object.values(merged) .sort(walletPrioritySort) .map(w => { @@ -141,6 +168,9 @@ export function WalletsProvider ({ children }) { // wallet.png <-> wallet-dark.png imgSrc = image.src.replace(/\.([a-z]{3})$/, '-dark.$1') } + + const logger = loggers[w.def.name] + return { ...w, support: { @@ -158,10 +188,13 @@ export function WalletsProvider ({ children }) { ...w.def.card, image: imgSrc ? { src: imgSrc } : undefined } - } + }, + logger, + sendPayment: sendPaymentWithLogger({ ...w, logger }) } - }).map(w => statusFromLog(w, logs)) - }, [serverWallets, localWallets, logs, darkMode]) + }) + .map(w => statusFromLog(w, logs)) + }, [serverWallets, localWallets, logs, darkMode, loggers]) const settings = useMemo(() => { return { @@ -261,9 +294,12 @@ export function useWallet (name) { .filter(w => w.config?.enabled && canSend(w))[0] }, [wallets, name]) - const { logger } = useWalletLogger(wallet?.def) + return wallet +} - const sendPayment = useCallback(async (bolt11) => { +function sendPaymentWithLogger (wallet) { + return async (bolt11) => { + const { logger } = wallet const decoded = bolt11Decode(bolt11) logger.info(`↗ sending payment: ${formatSats(decoded.satoshis)}`, { bolt11 }) try { @@ -274,9 +310,5 @@ export function useWallet (name) { logger.error(`payment failed: ${message}`, { bolt11 }) throw err } - }, [wallet, logger]) - - if (!wallet) return null - - return { ...wallet, sendPayment } + } }