Skip to content

Commit

Permalink
Refactor setting of default providers
Browse files Browse the repository at this point in the history
* fixed warning about component update while rendering another component
* individual providers no longer need to know if they are the default or not
* default setting is now handled by WebLNContext -- the same context that returns the provider. this makes a lot more sense and is a lot easier to read
* default payment checkbox is now also disabled if there is only one enabled provider or if it is the default provider
  • Loading branch information
ekzyis committed Feb 9, 2024
1 parent 3e4a409 commit fdbc792
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 82 deletions.
119 changes: 54 additions & 65 deletions components/webln/index.js
Original file line number Diff line number Diff line change
@@ -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`
Expand All @@ -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', {
Expand Down Expand Up @@ -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 (
<WebLNContext.Provider value={{ ...provider, sendPayment: sendPaymentWithToast }}>
<WebLNContext.Provider value={value}>
{children}
</WebLNContext.Provider>
)
Expand All @@ -136,5 +120,10 @@ export function WebLNProvider ({ children }) {
}

export function useWebLN () {
const { provider } = useContext(WebLNContext)
return provider
}

export function useWebLNConfigurator () {
return useContext(WebLNContext)
}
7 changes: 2 additions & 5 deletions components/webln/lnbits.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 (
<LNbitsContext.Provider value={value}>
{children}
Expand Down
9 changes: 3 additions & 6 deletions components/webln/nwc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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 (
<NWCContext.Provider value={value}>
{children}
Expand Down
11 changes: 8 additions & 3 deletions pages/settings/wallets/lnbits.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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) {
Expand All @@ -53,7 +58,7 @@ export default function LNbits () {
name='adminKey'
/>
<ClientCheckbox
disabled={!enabled}
disabled={!enabled || isDefault || enabledProviders.length === 1}
initialValue={isDefault}
label='default payment method'
name='isDefault'
Expand Down
11 changes: 8 additions & 3 deletions pages/settings/wallets/nwc.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import { useToast } from '../../../components/toast'
import { useRouter } from 'next/router'
import { useNWC } from '../../../components/webln/nwc'
import { WalletSecurityBanner } from '../../../components/banners'
import { useWebLNConfigurator } from '../../../components/webln'

export const getServerSideProps = getGetServerSideProps({ authRequired: true })

export default function NWC () {
const { nwcUrl, saveConfig, clearConfig, enabled, isDefault } = useNWC()
const { provider, enabledProviders, setProvider } = useWebLNConfigurator()
const nwc = useNWC()
const { name, nwcUrl, saveConfig, clearConfig, enabled } = nwc
const isDefault = provider?.name === name
const toaster = useToast()
const router = useRouter()

Expand All @@ -26,9 +30,10 @@ export default function NWC () {
isDefault: isDefault || false
}}
schema={nwcSchema}
onSubmit={async (values) => {
onSubmit={async ({ isDefault, ...values }) => {
try {
await saveConfig(values)
if (isDefault) setProvider(nwc)
toaster.success('saved settings')
router.push('/settings/wallets')
} catch (err) {
Expand All @@ -45,7 +50,7 @@ export default function NWC () {
autoFocus
/>
<ClientCheckbox
disabled={!enabled}
disabled={!enabled || isDefault || enabledProviders.length === 1}
initialValue={isDefault}
label='default payment method'
name='isDefault'
Expand Down

0 comments on commit fdbc792

Please sign in to comment.