Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions components/multi-step-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,17 @@ export function useStep () {
return steps[stepIndex]
}

export function useIsFirstStep () {
const stepIndex = useStepIndex()
return stepIndex === 0
}

export function useIsLastStep () {
const maxSteps = useMaxSteps()
const stepIndex = useStepIndex()
return stepIndex === maxSteps - 1
}

export function useNext () {
const { next } = useContext(MultiStepFormContext)
return next
Expand Down
2 changes: 2 additions & 0 deletions pages/wallets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ export default function Wallet () {
<WalletLayoutSubHeader>use real bitcoin</WalletLayoutSubHeader>
<div className='text-center'>
<WalletLayoutLink href='/wallets/logs'>wallet logs</WalletLayoutLink>
<span className='mx-2'>•</span>
<WalletLayoutLink href='/wallets/settings'>settings</WalletLayoutLink>
{showPassphrase && (
<>
<span className='mx-2'>•</span>
Expand Down
213 changes: 213 additions & 0 deletions pages/wallets/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { useCallback } from 'react'
import InputGroup from 'react-bootstrap/InputGroup'
import { useField } from 'formik'
import { useRouter } from 'next/router'
import { useMutation, useQuery } from '@apollo/client'
import { Checkbox, Form, Input, SubmitButton } from '@/components/form'
import Info from '@/components/info'
import { useToast } from '@/components/toast'
import AccordianItem from '@/components/accordian-item'
import { isNumber } from '@/lib/format'
import { walletSettingsSchema } from '@/lib/validate'
import { SET_WALLET_SETTINGS, WALLET_SETTINGS } from '@/wallets/client/fragments'
import { WalletLayout, WalletLayoutHeader } from '@/wallets/client/components'

export default function WalletSettings () {
const { data } = useQuery(WALLET_SETTINGS)
const [setSettings] = useMutation(SET_WALLET_SETTINGS)
const toaster = useToast()
const router = useRouter()

const onSubmit = useCallback(async (settings) => {
try {
await setSettings({
variables: { settings },
update: (cache, { data }) => {
cache.writeQuery({
query: WALLET_SETTINGS,
data: {
walletSettings: {
__typename: 'WalletSettings',
...data?.setWalletSettings
}
}
})
}
})
router.push('/wallets')
} catch (err) {
console.error(err)
toaster.danger('failed to save wallet')
}
}, [setSettings, toaster, router])

const initial = {
receiveCreditsBelowSats: data?.walletSettings?.receiveCreditsBelowSats ?? 10,
sendCreditsBelowSats: data?.walletSettings?.sendCreditsBelowSats ?? 10,
autoWithdrawThreshold: data?.walletSettings?.autoWithdrawThreshold ?? 10000,
autoWithdrawMaxFeePercent: data?.walletSettings?.autoWithdrawMaxFeePercent ?? 1,
autoWithdrawMaxFeeTotal: data?.walletSettings?.autoWithdrawMaxFeeTotal ?? 1,
proxyReceive: data?.walletSettings?.proxyReceive ?? true
}

return (
<WalletLayout>
<Form
enableReinitialize
initial={initial}
schema={walletSettingsSchema}
onSubmit={onSubmit}
>
<div className='py-5'>
<WalletLayoutHeader>wallet settings</WalletLayoutHeader>
</div>
<Settings />
<div className='d-flex mt-5 justify-content-end align-items-center'>
<SubmitButton variant='primary'>save</SubmitButton>
</div>
</Form>
</WalletLayout>
)
}

function Settings () {
return (
<>
<AutowithdrawSettings />
<AccordianItem
header='advanced'
body={
<>
<LightningAddressSettings />
<CowboyCreditsSettings />
</>
}
/>
</>
)
}

function AutowithdrawSettings () {
const [{ value: threshold }] = useField('autoWithdrawThreshold')
const sendThreshold = Math.max(Math.floor(threshold / 10), 1)

return (
<>
<Input
label='desired balance'
name='autoWithdrawThreshold'
hint={isNumber(sendThreshold) ? `will attempt autowithdrawal when your balance exceeds ${sendThreshold * 11} sats` : undefined}
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
required
type='number'
min={0}
groupClassName='mb-2'
/>
<Input
label={
<div className='d-flex align-items-center'>
max fee rate
<Info>
<ul>
<li>configure fee budget for autowithdrawals</li>
<li>if max fee total is higher for a withdrawal, we will use it instead to find a route</li>
<li>higher fee settings increase the likelihood of successful withdrawals</li>
</ul>
</Info>
</div>
}
name='autoWithdrawMaxFeePercent'
append={<InputGroup.Text>%</InputGroup.Text>}
required
type='number'
min={0}
/>
<Input
label={
<div className='d-flex align-items-center'>
max fee total
<Info>
<ul>
<li>configure fee budget for autowithdrawals</li>
<li>if max fee rate is higher for a withdrawal, we will use it instead to find a route to your wallet</li>
<li>higher fee settings increase the likelihood of successful withdrawals</li>
</ul>
</Info>
</div>
}
name='autoWithdrawMaxFeeTotal'
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
required
type='number'
min={0}
/>
</>
)
}

function LightningAddressSettings () {
return (
<>
<Checkbox
label={
<div className='d-flex align-items-center'>enhance privacy of my lightning address
<Info>
<ul>
<li>Enabling this setting hides details (ie node pubkey) of your attached wallets when anyone pays your SN lightning address or lnurl-pay</li>
<li>The lightning invoice will appear to have SN's node as the destination to preserve your wallet's privacy</li>
<li className='fw-bold'>This will incur in a 10% fee</li>
<li>Disable this setting to receive payments directly to your attached wallets (which will reveal their details to the payer)</li>
<li>Note: this privacy behavior is standard for internal zaps/payments on SN, and this setting only applies to external payments</li>
</ul>
</Info>
</div>
}
name='proxyReceive'
groupClassName='mb-3'
/>
</>
)
}

function CowboyCreditsSettings () {
return (
<>
<Input
label={
<div className='d-flex align-items-center'>
receive credits for zaps below
<Info>
<ul>
<li>we will not attempt to forward zaps below this amount to you, you will receive credits instead</li>
<li>this setting is useful if small amounts are expensive to receive for you</li>
</ul>
</Info>
</div>
}
name='receiveCreditsBelowSats'
required
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
type='number'
min={0}
/>
<Input
label={
<div className='d-flex align-items-center'>
send credits for zaps below
<Info>
<ul>
<li>we will not attempt to send zaps below this amount from your wallet if you have enough credits</li>
<li>this setting is useful if small amounts are expensive to send for you</li>
</ul>
</Info>
</div>
}
name='sendCreditsBelowSats'
required
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
type='number'
min={0}
/>
</>
)
}
5 changes: 4 additions & 1 deletion wallets/client/components/form/hooks.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isTemplate, isWallet, protocolClientSchema, protocolFields, protocolFormId, walletLud16Domain } from '@/wallets/lib/util'
import { createContext, useContext, useEffect, useMemo, useCallback, useState } from 'react'
import { useRouter } from 'next/router'
import { useWalletProtocolUpsert } from '@/wallets/client/hooks'
import { MultiStepForm, useFormState, useStep } from '@/components/multi-step-form'
import { parseNwcUrl } from '@/wallets/lib/validate'
Expand Down Expand Up @@ -133,6 +134,7 @@ export function useSaveWallet () {
const wallet = useWallet()
const [formState] = useFormState()
const upsert = useWalletProtocolUpsert()
const router = useRouter()

const save = useCallback(async () => {
let walletId = isWallet(wallet) ? wallet.id : undefined
Expand All @@ -147,7 +149,8 @@ export function useSaveWallet () {
)
walletId ??= id
}
}, [wallet, formState, upsert])
router.push('/wallets')
}, [wallet, formState, upsert, router])

return save
}
61 changes: 37 additions & 24 deletions wallets/client/components/form/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ import { Checkbox, Form, Input, PasswordInput, SubmitButton } from '@/components
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 { useIsFirstStep, useIsLastStep, useNext } from '@/components/multi-step-form'
import { isTemplate, protocolDisplayName, protocolFormId, protocolLogName, walletLud16Domain } from '@/wallets/lib/util'
import { WalletGuide, 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 { useFormikContext } from 'formik'

import { WalletMultiStepFormContextProvider, Step, useWallet, useWalletProtocols, useProtocol, useProtocolForm } from './hooks'
import { Settings } from './settings'
import { WalletMultiStepFormContextProvider, Step, useWallet, useWalletProtocols, useProtocol, useProtocolForm, useSaveWallet } from './hooks'
import { BackButton, SkipButton } from './button'

export function WalletMultiStepForm ({ wallet }) {
Expand All @@ -33,8 +32,7 @@ export function WalletMultiStepForm ({ wallet }) {
const steps = useMemo(() =>
[
support.send && Step.SEND,
support.receive && Step.RECEIVE,
Step.SETTINGS
support.receive && Step.RECEIVE
].filter(Boolean),
[support])

Expand All @@ -51,7 +49,7 @@ export function WalletMultiStepForm ({ wallet }) {
// 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} />
return null
})}
</WalletMultiStepFormContextProvider>
</div>
Expand Down Expand Up @@ -92,12 +90,21 @@ function WalletProtocolSelector () {
function WalletProtocolForm () {
const wallet = useWallet()
const [protocol] = useProtocol()
const next = useNext()

// on the last step, we save the wallet, otherwise we just go to the next step
const isLastStep = useIsLastStep()
const formNext = useNext()
const formSave = useSaveWallet()

const testSendPayment = useTestSendPayment(protocol)
const testCreateInvoice = useTestCreateInvoice(protocol)
const logger = useWalletLogger(protocol)
const [{ fields, initial, schema }, setFormState] = useProtocolForm(protocol)

const next = useCallback(() => {
isLastStep ? formSave() : formNext()
}, [isLastStep, formSave, formNext])

// create a copy of values to avoid mutating the original
const onSubmit = useCallback(async ({ ...values }) => {
const lud16Domain = walletLud16Domain(wallet.name)
Expand Down Expand Up @@ -152,25 +159,31 @@ function WalletProtocolForm () {
}

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
// show 'cancel' in the first step
const showCancel = useIsFirstStep()
// show 'save' instead of 'next' in the last step
const isLastStep = useIsLastStep()
// show 'skip' if there's a next step
const showSkip = !isLastStep

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>
{showCancel ? <CancelButton>cancel</CancelButton> : <BackButton />}
{showSkip ? <SkipButton /> : <div className='ms-auto' />}
{
isLastStep
? (
<SubmitButton variant='primary' className='d-flex align-items-center'>
save
</SubmitButton>
)
: (
<SubmitButton variant='primary' className='ps-3 pe-2 d-flex align-items-center'>
next
<ArrowRight width={24} height={24} />
</SubmitButton>
)
}
</div>
)
}
Expand Down
1 change: 0 additions & 1 deletion wallets/client/components/form/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export function Settings () {
})
}
})
router.push('/wallets')
} catch (err) {
console.error(err)
toaster.danger('failed to save wallet')
Expand Down