ekzyis e46f4f01b2
Wallet flow (#2362)
* Wallet flow

* Prepopulate fields of complementary protocol

* Remove TODO about one mutation for save

We need to save protocols in separate mutations so we can use the wallet id returned by the first protocol save for the following protocol saves and save them all to the same wallet.

* Fix badges not updated on wallet delete

* Fix useProtocol call

* Fix lightning address save via prompt

* Don't pass share as attribute to DOM

* Fix useCallback dependency

* Progress numbers as SVGs

* Fix progress line margins

* Remove unused saveWallet arguments

* Update cache with settings response

* Fix line does not connect with number 1

* Don't reuse page nav arrows in form nav

* Fix missing SVG hover style

* Fix missing space in wallet save log message

* Reuse CSS from nav.module.css

* align buttons and their icons/text

* center form progress line

* increase top padding of form on smaller screens

* provide margin above button bar on settings form

---------

Co-authored-by: k00b <k00b@stacker.news>
2025-08-26 09:19:52 -05:00

137 lines
4.8 KiB
JavaScript

import { isTemplate, isWallet, protocolClientSchema, protocolFields, protocolFormId, walletLud16Domain } from '@/wallets/lib/util'
import { createContext, useContext, useEffect, useMemo, useCallback, useState } from 'react'
import { useWalletProtocolUpsert } from '@/wallets/client/hooks'
import { MultiStepForm, useFormState, useStep } from '@/components/multi-step-form'
export const Step = {
SEND: 'send',
RECEIVE: 'receive',
SETTINGS: 'settings'
}
const WalletMultiStepFormContext = createContext()
export function WalletMultiStepFormContextProvider ({ wallet, initial, steps, children }) {
// save selected protocol, but useProtocol will always return the first protocol if no protocol is selected
const [protocol, setProtocol] = useState(null)
const value = useMemo(() => ({ wallet, protocol, setProtocol }), [wallet, protocol, setProtocol])
return (
<WalletMultiStepFormContext.Provider value={value}>
<MultiStepForm initial={initial} steps={steps}>
{children}
</MultiStepForm>
</WalletMultiStepFormContext.Provider>
)
}
export function useWallet () {
const { wallet } = useContext(WalletMultiStepFormContext)
return wallet
}
export function useWalletProtocols () {
const step = useStep()
const wallet = useWallet()
const protocolFilter = useCallback(p => step === Step.SEND ? p.send : !p.send, [step])
return useMemo(() => {
// all protocols are templates if wallet is a template
if (isTemplate(wallet)) {
return wallet.protocols.filter(protocolFilter)
}
// return template for every protocol that isn't configured
const configured = wallet.protocols.filter(protocolFilter)
const templates = wallet.template.protocols.filter(protocolFilter)
return templates.map(p => configured.find(c => c.name === p.name) ?? p)
}, [wallet, protocolFilter])
}
export function useProtocol () {
const { protocol, setProtocol } = useContext(WalletMultiStepFormContext)
const protocols = useWalletProtocols()
useEffect(() => {
// when we move between send and receive, we need to make sure that we've selected a protocol
// that actually exists, so if the protocol is not found, we set it to the first protocol
if (!protocol || !protocols.find(p => p.id === protocol.id)) {
setProtocol(protocols[0])
}
}, [protocol, protocols, setProtocol])
// make sure we always have a protocol, even on first render before useEffect runs
return useMemo(() => [protocol ?? protocols[0], setProtocol], [protocol, protocols, setProtocol])
}
function useProtocolFormState (protocol) {
const formId = protocolFormId(protocol)
const [formState, setFormState] = useFormState(formId)
const setProtocolFormState = useCallback(
({ enabled, ...config }) => {
setFormState({ ...protocol, enabled, config })
},
[setFormState, protocol])
return useMemo(() => [formState, setProtocolFormState], [formState, setProtocolFormState])
}
export function useProtocolForm (protocol) {
const [formState, setFormState] = useProtocolFormState(protocol)
const [complementaryFormState] = useProtocolFormState({ name: protocol.name, send: !protocol.send })
const wallet = useWallet()
const lud16Domain = walletLud16Domain(wallet.name)
const fields = protocolFields(protocol)
const initial = fields.reduce((acc, field) => {
// we only fallback to the existing protocol config because formState was not initialized yet on first render
// after init, we use formState as the source of truth everywhere
let value = formState?.config?.[field.name] ?? protocol.config?.[field.name]
if (!value && field.share) {
value = complementaryFormState?.config?.[field.name]
}
if (field.name === 'address' && lud16Domain && value) {
value = value.split('@')[0]
}
return {
...acc,
[field.name]: value || ''
}
}, { enabled: formState?.enabled ?? protocol.enabled })
let schema = protocolClientSchema(protocol)
if (lud16Domain) {
schema = schema.transform(({ address, ...rest }) => {
return {
address: address ? `${address}@${lud16Domain}` : '',
...rest
}
})
}
return useMemo(() => [{ fields, initial, schema }, setFormState], [fields, initial, schema, setFormState])
}
export function useSaveWallet () {
const wallet = useWallet()
const [formState] = useFormState()
const upsert = useWalletProtocolUpsert()
const save = useCallback(async () => {
let walletId = isWallet(wallet) ? wallet.id : undefined
for (const protocol of Object.values(formState)) {
const { id } = await upsert(
{
...wallet,
id: walletId,
__typename: walletId ? 'Wallet' : 'WalletTemplate'
},
protocol, { ...protocol.config, enabled: protocol.enabled }
)
walletId ??= id
}
}, [wallet, formState, upsert])
return save
}