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

232 lines
7.4 KiB
JavaScript

import { useCallback, useMemo } from 'react'
import { InputGroup, Nav } from 'react-bootstrap'
import classNames from 'classnames'
import styles from '@/styles/wallet.module.css'
import navStyles from '@/styles/nav.module.css'
import { Checkbox, Form, Input, PasswordInput, SubmitButton } from '@/components/form'
import CancelButton from '@/components/cancel-button'
import Text from '@/components/text'
import Info from '@/components/info'
import { useFormState, useMaxSteps, useNext, useStepIndex } from '@/components/multi-step-form'
import { isTemplate, isWallet, protocolDisplayName, protocolFormId, protocolLogName, walletLud16Domain } from '@/wallets/lib/util'
import { WalletLayout, WalletLayoutHeader, WalletLayoutImageOrName, WalletLogs } from '@/wallets/client/components'
import { TemplateLogsProvider, useTestSendPayment, useWalletLogger, useTestCreateInvoice, useWalletSupport } from '@/wallets/client/hooks'
import ArrowRight from '@/svgs/arrow-right-s-fill.svg'
import { WalletMultiStepFormContextProvider, Step, useWallet, useWalletProtocols, useProtocol, useProtocolForm } from './hooks'
import { Settings } from './settings'
import { BackButton, SkipButton } from './button'
export function WalletMultiStepForm ({ wallet }) {
const initial = useMemo(() => wallet.protocols
.filter(p => !isTemplate(p))
.reduce((acc, p) => {
const formId = protocolFormId(p)
return {
...acc,
[formId]: p
}
}, {}), [wallet])
const support = useWalletSupport(wallet)
const steps = useMemo(() =>
[
support.send && Step.SEND,
support.receive && Step.RECEIVE,
Step.SETTINGS
].filter(Boolean),
[support])
return (
<WalletLayout>
<div className={styles.form}>
<WalletLayoutHeader>
<WalletLayoutImageOrName name={wallet.name} maxHeight='80px' />
</WalletLayoutHeader>
<WalletMultiStepFormContextProvider wallet={wallet} initial={initial} steps={steps}>
{steps.map(step => {
// WalletForm is aware of the current step via hooks
// and can thus render a different form for send vs. receive
if (step === Step.SEND) return <WalletForm key={step} />
if (step === Step.RECEIVE) return <WalletForm key={step} />
return <Settings key={step} />
})}
</WalletMultiStepFormContextProvider>
</div>
</WalletLayout>
)
}
function WalletForm () {
return (
<TemplateLogsProvider>
<WalletProtocolSelector />
<WalletProtocolForm />
</TemplateLogsProvider>
)
}
function WalletProtocolSelector () {
const protocols = useWalletProtocols()
const [protocol, selectProtocol] = useProtocol()
return (
<Nav className={classNames(navStyles.nav, 'mt-0')} activeKey={protocol?.name}>
{
protocols.map(p => {
return (
<Nav.Item key={p.id} onClick={() => selectProtocol(p)}>
<Nav.Link eventKey={p.name}>
{protocolDisplayName(p)}
</Nav.Link>
</Nav.Item>
)
})
}
</Nav>
)
}
function WalletProtocolForm () {
const wallet = useWallet()
const [protocol] = useProtocol()
const next = useNext()
const testSendPayment = useTestSendPayment(protocol)
const testCreateInvoice = useTestCreateInvoice(protocol)
const logger = useWalletLogger(protocol)
const [{ fields, initial, schema }, setFormState] = useProtocolForm(protocol)
// create a copy of values to avoid mutating the original
const onSubmit = useCallback(async ({ ...values }) => {
const lud16Domain = walletLud16Domain(wallet.name)
if (values.address && lud16Domain) {
values.address = `${values.address}@${lud16Domain}`
}
const name = protocolLogName(protocol)
if (isTemplate(protocol)) {
values.enabled = true
}
if (values.enabled) {
try {
if (protocol.send) {
logger.info(`testing ${name} send ...`)
const additionalValues = await testSendPayment(values)
values = { ...values, ...additionalValues }
logger.ok(`${name} send ok`)
} else {
logger.info(`testing ${name} receive ...`)
await testCreateInvoice(values)
logger.ok(`${name} receive ok`)
}
} catch (err) {
logger.error(err.message)
throw err
}
}
setFormState(values)
next()
}, [protocol, wallet, setFormState, testSendPayment, logger, next])
return (
<>
<Form
key={`form-${protocol.id}`}
enableReinitialize
initial={initial}
schema={schema}
onSubmit={onSubmit}
>
{fields.map(field => <WalletProtocolFormField key={field.name} {...field} />)}
{!isTemplate(protocol) && <Checkbox name='enabled' label='enabled' />}
<WalletProtocolFormNavigator />
</Form>
<WalletLogs className='mt-3' protocol={protocol} key={`logs-${protocol.id}`} />
</>
)
}
function WalletProtocolFormNavigator () {
const wallet = useWallet()
const stepIndex = useStepIndex()
const maxSteps = useMaxSteps()
const [formState] = useFormState()
// was something already configured or was something configured just now?
const configExists = (isWallet(wallet) && wallet.protocols.length > 0) || Object.keys(formState).length > 0
// don't allow going to settings as last step with nothing configured
const hideSkip = stepIndex === maxSteps - 2 && !configExists
return (
<div className='d-flex justify-content-end align-items-center'>
{stepIndex === 0 ? <CancelButton>cancel</CancelButton> : <BackButton />}
{!hideSkip ? <SkipButton /> : <div className='ms-auto' />}
<SubmitButton variant='primary' className='ps-3 pe-2 d-flex align-items-center'>
next
<ArrowRight width={24} height={24} />
</SubmitButton>
</div>
)
}
function WalletProtocolFormField ({ type, ...props }) {
const wallet = useWallet()
const [protocol] = useProtocol()
function transform ({ validate, encrypt, editable, help, share, ...props }) {
const [upperHint, bottomHint] = Array.isArray(props.hint) ? props.hint : [null, props.hint]
const parseHelpText = text => Array.isArray(text) ? text.join('\n\n') : text
const _help = help
? (
typeof help === 'string'
? { label: null, text: help }
: (
Array.isArray(help)
? { label: null, text: parseHelpText(help) }
: { label: help.label, text: parseHelpText(help.text) }
)
)
: null
const readOnly = !!protocol.config?.[props.name] && editable === false
const label = (
<div className='d-flex align-items-center'>
{props.label}
{_help && (
<Info label={_help.label}>
<Text>{_help.text}</Text>
</Info>
)}
<small className='text-muted ms-2'>
{upperHint
? <Text>{upperHint}</Text>
: (!props.required ? 'optional' : null)}
</small>
</div>
)
return { ...props, hint: bottomHint, label, readOnly }
}
switch (type) {
case 'text': {
let append
const lud16Domain = walletLud16Domain(wallet.name)
if (props.name === 'address' && lud16Domain) {
append = <InputGroup.Text className='text-monospace'>@{lud16Domain}</InputGroup.Text>
}
return <Input {...transform(props)} append={append} />
}
case 'password':
return <PasswordInput {...transform(props)} />
default:
return null
}
}