import { useEffect, useCallback, useMemo, createContext, useContext } from 'react' import { Button, InputGroup, Nav } from 'react-bootstrap' import Link from 'next/link' import { useParams, usePathname } from 'next/navigation' import { useRouter } from 'next/router' import { WalletLayout, WalletLayoutHeader, WalletLayoutImageOrName, WalletLogs } from '@/wallets/client/components' import { protocolDisplayName, protocolFields, protocolClientSchema, unurlify, urlify, isWallet, isTemplate, walletLud16Domain } from '@/wallets/lib/util' 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 { useWalletProtocolUpsert, useWalletProtocolRemove, useWalletQuery, TemplateLogsProvider } from '@/wallets/client/hooks' import { useToast } from '@/components/toast' import Text from '@/components/text' import Info from '@/components/info' import classNames from 'classnames' const WalletFormsContext = createContext() export function WalletForms ({ id, name }) { // TODO(wallet-v2): handle loading and error states const { data, refetch } = useWalletQuery({ name, id }) const wallet = data?.wallet return (
{wallet && } {wallet && ( )}
) } function WalletFormsProvider ({ children, wallet, refetch }) { const value = useMemo(() => ({ refetch, wallet }), [refetch, wallet]) return ( {children} ) } function useWalletRefetch () { const { refetch } = useContext(WalletFormsContext) return refetch } function useWallet () { const { wallet } = useContext(WalletFormsContext) return wallet } function WalletFormSelector () { const sendRecvParam = useSendRecvParam() const protocolParam = useWalletProtocolParam() return ( <> {sendRecvParam && (
{protocolParam && ( )}
)} ) } function WalletSendRecvSelector () { const path = useWalletPathname() const selected = useSendRecvParam() // TODO(wallet-v2): if you click a nav link again, it will update the URL // but not run the effect again to select the first protocol by default return ( ) } function WalletProtocolSelector () { const walletPath = useWalletPathname() const sendRecvParam = useSendRecvParam() const path = `${walletPath}/${sendRecvParam}` const protocols = useWalletProtocols() const selected = useWalletProtocolParam() const router = useRouter() useEffect(() => { if (!selected && protocols.length > 0) { router.replace(`/${path}/${urlify(protocols[0].name)}`, null, { shallow: true }) } }, [path]) if (protocols.length === 0) { // TODO(wallet-v2): let user know how to request support if the wallet actually does support sending return (
{sendRecvParam === 'send' ? 'sending' : 'receiving'} not supported
) } return ( ) } function WalletProtocolForm () { const sendRecvParam = useSendRecvParam() const router = useRouter() const protocol = useSelectedProtocol() if (!protocol) return null // I think it is okay to skip this hook if the protocol is not found // because we will need to change the URL to get a different protocol // so the amount of rendered hooks should stay the same during the lifecycle of this component const wallet = useWallet() const upsertWalletProtocol = useWalletProtocolUpsert(wallet, protocol) const toaster = useToast() const refetch = useWalletRefetch() const { fields, initial, schema } = 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 upsert = await upsertWalletProtocol(values) if (isWallet(wallet)) { toaster.success('wallet saved') refetch() return } // we just created a new user wallet from a template router.replace(`/wallets/${upsert.id}/${sendRecvParam}`, null, { shallow: true }) toaster.success('wallet attached', { persistOnNavigate: true }) }, [upsertWalletProtocol, toaster, wallet, router]) return ( <>
{fields.map(field => )} ) } function WalletProtocolFormButtons () { const protocol = useSelectedProtocol() const removeWalletProtocol = useWalletProtocolRemove(protocol) const refetch = useWalletRefetch() const router = useRouter() const wallet = useWallet() const isLastProtocol = wallet.protocols.length === 1 const onDetach = useCallback(async () => { await removeWalletProtocol() if (isLastProtocol) { router.replace('/wallets', null, { shallow: true }) return } refetch() }, [removeWalletProtocol, refetch, isLastProtocol, router]) return (
{!isTemplate(protocol) && } cancel {isWallet(wallet) ? 'save' : 'attach'}
) } function WalletProtocolFormField ({ type, ...props }) { const wallet = useWallet() const protocol = useSelectedProtocol() function transform ({ validate, encrypt, editable, help, ...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 = (
{props.label} {_help && ( {_help.text} )} {upperHint ? {upperHint} : (!props.required ? 'optional' : null)}
) return { ...props, hint: bottomHint, label, readOnly } } switch (type) { case 'text': { let append const lud16Domain = walletLud16Domain(wallet.name) if (props.name === 'address' && lud16Domain) { append = @{lud16Domain} } return } case 'password': return default: return null } } function useWalletPathname () { const pathname = usePathname() // returns /wallets/:name return pathname.split('/').filter(Boolean).slice(0, 2).join('/') } function useSendRecvParam () { const params = useParams() // returns only :send in /wallets/:name/:send return ['send', 'receive'].includes(params.slug[1]) ? params.slug[1] : null } function useWalletProtocolParam () { const params = useParams() const name = params.slug[2] // returns only :protocol in /wallets/:name/:send/:protocol return name ? unurlify(name) : null } function useWalletProtocols () { const wallet = useWallet() const sendRecvParam = useSendRecvParam() if (!sendRecvParam) return [] const protocolFilter = p => sendRecvParam === 'send' ? p.send : !p.send return isWallet(wallet) ? wallet.template.protocols.filter(protocolFilter) : wallet.protocols.filter(protocolFilter) } function useSelectedProtocol () { const wallet = useWallet() const sendRecvParam = useSendRecvParam() const protocolParam = useWalletProtocolParam() const send = sendRecvParam === 'send' let protocol = wallet.protocols.find(p => p.name === protocolParam && p.send === send) if (!protocol && isWallet(wallet)) { // the protocol was not found as configured, look for it in the template protocol = wallet.template.protocols.find(p => p.name === protocolParam && p.send === send) } return protocol } function useProtocolForm (protocol) { const wallet = useWallet() const lud16Domain = walletLud16Domain(wallet.name) const fields = protocolFields(protocol) const initial = fields.reduce((acc, field) => { // wallet templates don't have a config let value = protocol.config?.[field.name] if (field.name === 'address' && lud16Domain && value) { value = value.split('@')[0] } return { ...acc, [field.name]: value || '' } }, { enabled: protocol.enabled }) let schema = protocolClientSchema(protocol) if (lud16Domain) { schema = schema.transform(({ address, ...rest }) => { return { address: address ? `${address}@${lud16Domain}` : '', ...rest } }) } return { fields, initial, schema } }