diff --git a/components/webln/index.js b/components/webln/index.js index e8d4ce382..982084a75 100644 --- a/components/webln/index.js +++ b/components/webln/index.js @@ -1,45 +1,53 @@ -import { createContext, useContext, useEffect, useState } from 'react' +import { createContext, useCallback, useContext, useEffect, useState } from 'react' import { LNbitsProvider, useLNbits } from './lnbits' import { NWCProvider, useNWC } from './nwc' import { useToast } from '../toast' import { gql, useMutation } from '@apollo/client' const WebLNContext = createContext({}) -const storageKey = 'webln:providers' -const paymentMethodHook = (methods, { name, enabled }) => { - let newMethods - if (enabled) { - newMethods = methods.includes(name) ? methods : [...methods, name] - } else { - newMethods = methods.filter(m => m !== name) +const syncProvider = (array, provider) => { + const idx = array.findIndex(({ name }) => provider.name === name) + if (idx === -1) { + // add provider to end if enabled + return provider.enabled ? [...array, provider] : array } - savePaymentMethods(newMethods) - return newMethods -} - -const savePaymentMethods = (methods) => { - window.localStorage.setItem(storageKey, JSON.stringify(methods)) + return [ + ...array.slice(0, idx), + // remove provider if not enabled + ...provider.enabled ? [provider] : [], + ...array.slice(idx + 1) + ] } function RawWebLNProvider ({ children }) { const lnbits = useLNbits() const nwc = useNWC() - const providers = [lnbits, nwc] + const [enabledProviders, setEnabledProviders] = useState([lnbits, nwc].filter(({ enabled }) => enabled)) + // keep list in sync with underlying providers + useEffect(() => { + setEnabledProviders(providers => { + // Sync existing provider state with new provider state + // in the list while keeping the order they are in. + // If provider does not exist but is enabled, it is just added to the end of the list. + // This can be the case if we're syncing from a page reload + // where the providers are initially not enabled. + // If provider is no longer enabled, it is removed from the list. + const newProviders = [lnbits, nwc].reduce(syncProvider, providers) + return newProviders + }) + }, [lnbits, nwc]) - // TODO: Order of payment methods depends on user preference. - // Payment method at index 0 should be default, - // if that one fails we try the remaining ones in order as fallbacks. - // We should be able to implement this via dragging of cards. - // This list should then match the order in which the (payment) cards are rendered. - // eslint-disable-next-line no-unused-vars - const [paymentMethods, setPaymentMethods] = useState([]) - const loadPaymentMethods = () => { - const methods = window.localStorage.getItem(storageKey) - if (!methods) return - setPaymentMethods(JSON.parse(methods)) + // sanity check + for (const p of enabledProviders) { + if (!p.enabled) { + console.warn('Expected provider to be enabled but is not:', p.name) + } } - useEffect(loadPaymentMethods, []) + + // first provider in list is the default provider + // TODO: implement fallbacks via provider priority + const provider = enabledProviders[0] const toaster = useToast() const [cancelInvoice] = useMutation(gql` @@ -50,43 +58,6 @@ function RawWebLNProvider ({ children }) { } `) - useEffect(() => { - setPaymentMethods(methods => paymentMethodHook(methods, nwc)) - if (!nwc.enabled) nwc.setIsDefault(false) - }, [nwc.enabled]) - - useEffect(() => { - setPaymentMethods(methods => paymentMethodHook(methods, lnbits)) - if (!lnbits.enabled) lnbits.setIsDefault(false) - }, [lnbits.enabled]) - - const setDefaultPaymentMethod = (provider) => { - for (const p of providers) { - if (p.name !== provider.name) { - p.setIsDefault(false) - } - } - } - - useEffect(() => { - if (nwc.isDefault) setDefaultPaymentMethod(nwc) - }, [nwc.isDefault]) - - useEffect(() => { - if (lnbits.isDefault) setDefaultPaymentMethod(lnbits) - }, [lnbits.isDefault]) - - // TODO: implement numeric provider priority using paymentMethods list - // when we have more than two providers for sending - let provider = providers.filter(p => p.enabled && p.isDefault)[0] - if (!provider && providers.length > 0) { - // if no provider is the default, pick the first one and use that one as the default - provider = providers.filter(p => p.enabled)[0] - if (provider) { - provider.setIsDefault(true) - } - } - const sendPaymentWithToast = function ({ bolt11, hash, hmac }) { let canceled = false let removeToast = toaster.warning('payment pending', { @@ -116,8 +87,21 @@ function RawWebLNProvider ({ children }) { }) } + const setProvider = useCallback((defaultProvider) => { + // move provider to the start to set it as default + setEnabledProviders(providers => { + const idx = providers.findIndex(({ name }) => defaultProvider.name === name) + if (idx === -1) { + console.warn(`tried to set unenabled provider ${defaultProvider.name} as default`) + return providers + } + return [defaultProvider, ...providers.slice(0, idx), ...providers.slice(idx + 1)] + }) + }, [setEnabledProviders]) + + const value = { provider: { ...provider, sendPayment: sendPaymentWithToast }, enabledProviders, setProvider } return ( - + {children} ) @@ -136,5 +120,10 @@ export function WebLNProvider ({ children }) { } export function useWebLN () { + const { provider } = useContext(WebLNContext) + return provider +} + +export function useWebLNConfigurator () { return useContext(WebLNContext) } diff --git a/components/webln/lnbits.js b/components/webln/lnbits.js index 5bf5cf207..578d5c210 100644 --- a/components/webln/lnbits.js +++ b/components/webln/lnbits.js @@ -64,7 +64,6 @@ export function LNbitsProvider ({ children }) { const [url, setUrl] = useState('') const [adminKey, setAdminKey] = useState('') const [enabled, setEnabled] = useState() - const [isDefault, setIsDefault] = useState() const name = 'LNbits' const storageKey = 'webln:provider:lnbits' @@ -104,10 +103,9 @@ export function LNbitsProvider ({ children }) { const config = JSON.parse(configStr) - const { url, adminKey, isDefault } = config + const { url, adminKey } = config setUrl(url) setAdminKey(adminKey) - setIsDefault(isDefault) try { // validate config by trying to fetch wallet @@ -124,7 +122,6 @@ export function LNbitsProvider ({ children }) { // immediately store config so it's not lost even if config is invalid setUrl(config.url) setAdminKey(config.adminKey) - setIsDefault(config.isDefault) // XXX This is insecure, XSS vulns could lead to loss of funds! // -> check how mutiny encrypts their wallet and/or check if we can leverage web workers @@ -153,7 +150,7 @@ export function LNbitsProvider ({ children }) { loadConfig().catch(console.error) }, []) - const value = { name, url, adminKey, saveConfig, clearConfig, enabled, isDefault, setIsDefault, getInfo, sendPayment } + const value = { name, url, adminKey, saveConfig, clearConfig, enabled, getInfo, sendPayment } return ( {children} diff --git a/components/webln/nwc.js b/components/webln/nwc.js index ac72209a1..c3e887876 100644 --- a/components/webln/nwc.js +++ b/components/webln/nwc.js @@ -11,7 +11,6 @@ export function NWCProvider ({ children }) { const [relayUrl, setRelayUrl] = useState() const [secret, setSecret] = useState() const [enabled, setEnabled] = useState() - const [isDefault, setIsDefault] = useState() const [relay, setRelay] = useState() const name = 'NWC' @@ -26,9 +25,8 @@ export function NWCProvider ({ children }) { const config = JSON.parse(configStr) - const { nwcUrl, isDefault } = config + const { nwcUrl } = config setNwcUrl(nwcUrl) - setIsDefault(isDefault) const params = parseWalletConnectUrl(nwcUrl) setRelayUrl(params.relayUrl) @@ -47,9 +45,8 @@ export function NWCProvider ({ children }) { const saveConfig = useCallback(async (config) => { // immediately store config so it's not lost even if config is invalid - const { nwcUrl, isDefault } = config + const { nwcUrl } = config setNwcUrl(nwcUrl) - setIsDefault(isDefault) if (!nwcUrl) { setEnabled(undefined) return @@ -174,7 +171,7 @@ export function NWCProvider ({ children }) { loadConfig().catch(console.error) }, []) - const value = { name, nwcUrl, relayUrl, walletPubkey, secret, saveConfig, clearConfig, enabled, isDefault, setIsDefault, getInfo, sendPayment } + const value = { name, nwcUrl, relayUrl, walletPubkey, secret, saveConfig, clearConfig, enabled, getInfo, sendPayment } return ( {children} diff --git a/pages/settings/wallets/lnbits.js b/pages/settings/wallets/lnbits.js index 2d137d163..8ad2adb2d 100644 --- a/pages/settings/wallets/lnbits.js +++ b/pages/settings/wallets/lnbits.js @@ -7,11 +7,15 @@ import { useToast } from '../../../components/toast' import { useRouter } from 'next/router' import { useLNbits } from '../../../components/webln/lnbits' import { WalletSecurityBanner } from '../../../components/banners' +import { useWebLNConfigurator } from '../../../components/webln' export const getServerSideProps = getGetServerSideProps({ authRequired: true }) export default function LNbits () { - const { url, adminKey, saveConfig, clearConfig, enabled, isDefault } = useLNbits() + const { provider, enabledProviders, setProvider } = useWebLNConfigurator() + const lnbits = useLNbits() + const { name, url, adminKey, saveConfig, clearConfig, enabled } = lnbits + const isDefault = provider?.name === name const toaster = useToast() const router = useRouter() @@ -27,9 +31,10 @@ export default function LNbits () { isDefault: isDefault || false }} schema={lnbitsSchema} - onSubmit={async (values) => { + onSubmit={async ({ isDefault, ...values }) => { try { await saveConfig(values) + if (isDefault) setProvider(lnbits) toaster.success('saved settings') router.push('/settings/wallets') } catch (err) { @@ -53,7 +58,7 @@ export default function LNbits () { name='adminKey' /> { + onSubmit={async ({ isDefault, ...values }) => { try { await saveConfig(values) + if (isDefault) setProvider(nwc) toaster.success('saved settings') router.push('/settings/wallets') } catch (err) { @@ -45,7 +50,7 @@ export default function NWC () { autoFocus />