Compare commits

...

4 Commits

Author SHA1 Message Date
ekzyis d9024ff837
Reinitialize wallet form if initial values change + fix readOnly hydration error (#1354)
* Reinitialize wallet form if initial values change

This fixes that enabled is not set on first render if only recv is configured

* Remove unnecessary old usage of ClientCheckbox

This isn't needed even without enableReinitialize since for send, enabled is correctly set on first render.

It was needed in the past when we were still validating wallets before enabling them on first page load but now, we simply load the configuration from localStorage which is immediately available on the client.

* Fix readOnly hydration error

* Replace repetitive isMounted logic with useIsClient hook
2024-09-03 09:15:04 -05:00
k00b 69916117b1 refine popover close timing 2024-09-02 18:25:02 -05:00
k00b 67799a508a image loading fixes (fixes #1345) 2024-09-02 18:15:21 -05:00
ekzyis 7428738b23
Update wallets/README.md (#1353)
* Remove warning about send+recv not tested

* Add file comment

* Fix createInvoice description
2024-09-02 17:15:46 -05:00
10 changed files with 40 additions and 37 deletions

View File

@ -3,6 +3,7 @@ import { Checkbox, Input } from './form'
import { useMe } from './me'
import { useEffect, useState } from 'react'
import { isNumber } from '@/lib/validate'
import { useIsClient } from './use-client'
function autoWithdrawThreshold ({ me }) {
return isNumber(me?.privates?.autoWithdrawThreshold) ? me?.privates?.autoWithdrawThreshold : 10000
@ -25,15 +26,12 @@ export function AutowithdrawSettings ({ wallet }) {
setSendThreshold(Math.max(Math.floor(threshold / 10), 1))
}, [autoWithdrawThreshold])
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
const isClient = useIsClient()
return (
<>
<Checkbox
disabled={mounted && !wallet.isConfigured}
disabled={isClient && !wallet.isConfigured}
label='enabled'
id='enabled'
name='enabled'

View File

@ -803,7 +803,7 @@ const StorageKeyPrefixContext = createContext()
export function Form ({
initial, validate, schema, onSubmit, children, initialError, validateImmediately,
storageKeyPrefix, validateOnChange = true, requireSession, innerRef,
storageKeyPrefix, validateOnChange = true, requireSession, innerRef, enableReinitialize,
...props
}) {
const toaster = useToast()
@ -855,6 +855,7 @@ export function Form ({
return (
<Formik
initialValues={initial}
enableReinitialize={enableReinitialize}
validateOnChange={validateOnChange}
validate={validate}
validationSchema={schema}

View File

@ -13,7 +13,7 @@ export default function HoverablePopover ({ trigger, body, onShow }) {
onShow?.()
timeoutId.current = setTimeout(() => setShow(true), 500)
} else {
timeoutId.current = setTimeout(() => setShow(!!popRef.current?.matches(':hover')), 500)
timeoutId.current = setTimeout(() => setShow(!!popRef.current?.matches(':hover')), 300)
}
}
@ -23,8 +23,7 @@ export default function HoverablePopover ({ trigger, body, onShow }) {
trigger={['hover', 'focus']}
show={show}
onToggle={onToggle}
delay={1}
transition={false}
transition
rootClose
overlay={
<Popover style={{ position: 'fixed' }} onPointerLeave={() => onToggle(false)}>

View File

@ -57,7 +57,7 @@ function ImageOriginal ({ src, topLevel, rel, tab, children, onClick, ...props }
target='_blank'
rel={rel ?? UNKNOWN_LINK_REL}
href={src}
>{isRawURL ? src : children}
>{isRawURL || !children ? src : children}
</a>
)
}

View File

@ -150,7 +150,7 @@ export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, o
return url
}
const srcSet = imgproxyUrls?.[url]
return <ZoomableImage srcSet={srcSet} tab={tab} src={src} rel={rel ?? UNKNOWN_LINK_REL} {...props} topLevel />
return <ZoomableImage srcSet={srcSet} tab={tab} src={src} rel={rel ?? UNKNOWN_LINK_REL} {...props} topLevel={topLevel} />
}, [imgproxyUrls, topLevel, tab])
return (

View File

@ -129,16 +129,13 @@
}
.text img {
--height: 35vh;
--width: 100%;
display: block;
margin-top: .5rem;
margin-bottom: .5rem;
width: auto;
max-width: min(var(--width), 100%);
max-width: 100%;
cursor: zoom-in;
height: auto;
max-height: min(var(--height), 25vh);
max-height: 25vh;
object-fit: contain;
object-position: left top;
min-width: 50%;
@ -148,7 +145,7 @@
.text img.topLevel {
margin-top: .75rem;
margin-bottom: .75rem;
max-height: min(var(--height), 35vh);
max-height: 35vh;
}
img.fullScreen {

12
components/use-client.js Normal file
View File

@ -0,0 +1,12 @@
import { useEffect, useState } from 'react'
// https://usehooks-ts.com/react-hook/use-is-client#hook
export function useIsClient () {
const [isClient, setClient] = useState(false)
useEffect(() => {
setClient(true)
}, [])
return isClient
}

View File

@ -1,15 +1,16 @@
import { getGetServerSideProps } from '@/api/ssrApollo'
import { Form, ClientInput, ClientCheckbox, PasswordInput, CheckboxGroup } from '@/components/form'
import { Form, ClientInput, PasswordInput, CheckboxGroup, Checkbox } from '@/components/form'
import { CenterLayout } from '@/components/layout'
import { WalletSecurityBanner } from '@/components/banners'
import { WalletLogs } from '@/components/wallet-logger'
import { useToast } from '@/components/toast'
import { useRouter } from 'next/router'
import { useWallet, Status } from 'wallets'
import { useWallet } from 'wallets'
import Info from '@/components/info'
import Text from '@/components/text'
import { AutowithdrawSettings } from '@/components/autowithdraw-shared'
import dynamic from 'next/dynamic'
import { useIsClient } from '@/components/use-client'
const WalletButtonBar = dynamic(() => import('@/components/wallet-buttonbar.js'), { ssr: false })
@ -45,6 +46,7 @@ export default function WalletSettings () {
{wallet.canSend && wallet.hasConfig > 0 && <WalletSecurityBanner />}
<Form
initial={initial}
enableReinitialize
{...validateProps}
onSubmit={async ({ amount, ...values }) => {
try {
@ -70,9 +72,8 @@ export default function WalletSettings () {
? <AutowithdrawSettings wallet={wallet} />
: (
<CheckboxGroup name='enabled'>
<ClientCheckbox
<Checkbox
disabled={!wallet.isConfigured}
initialValue={wallet.status === Status.Enabled}
label='enabled'
name='enabled'
groupClassName='mb-0'
@ -101,13 +102,15 @@ export default function WalletSettings () {
}
function WalletFields ({ wallet: { config, fields, isConfigured } }) {
const isClient = useIsClient()
return fields
.map(({ name, label = '', type, help, optional, editable, clientOnly, serverOnly, ...props }, i) => {
const rawProps = {
...props,
name,
initialValue: config?.[name],
readOnly: isConfigured && editable === false && !!config?.[name],
readOnly: isClient && isConfigured && editable === false && !!config?.[name],
groupClassName: props.hidden ? 'd-none' : undefined,
label: label
? (

View File

@ -3,8 +3,9 @@ import Layout from '@/components/layout'
import styles from '@/styles/wallet.module.css'
import Link from 'next/link'
import { useWallets, walletPrioritySort } from 'wallets'
import { useEffect, useState } from 'react'
import { useState } from 'react'
import dynamic from 'next/dynamic'
import { useIsClient } from '@/components/use-client'
const WalletCard = dynamic(() => import('@/components/wallet-card'), { ssr: false })
@ -29,17 +30,10 @@ async function reorder (wallets, sourceIndex, targetIndex) {
export default function Wallet ({ ssrData }) {
const { wallets } = useWallets()
const [mounted, setMounted] = useState(false)
const isClient = useIsClient()
const [sourceIndex, setSourceIndex] = useState(null)
const [targetIndex, setTargetIndex] = useState(null)
useEffect(() => {
// mounted is required since draggable is false
// for wallets only available on the client during SSR
// and thus we need to render the component again on the client
setMounted(true)
}, [])
const onDragStart = (i) => (e) => {
// e.dataTransfer.dropEffect = 'move'
// We can only use the DataTransfer API inside the drop event
@ -94,7 +88,7 @@ export default function Wallet ({ ssrData }) {
return walletPrioritySort(w1, w2)
})
.map((w, i) => {
const draggable = mounted && w.enabled
const draggable = isClient && w.enabled
return (
<div

View File

@ -42,9 +42,6 @@ A _server.js_ file is only required for wallets that support receiving by exposi
>
> If a wallet does not support paying invoices, this is all that client.js of this wallet does. The reason for this structure is to make sure the client does not import dependencies that can only be imported on the server and would thus break the build.
> [!WARNING]
> Wallets that support spending **AND** receiving have not been tested yet. For now, only implement either the interface for spending **OR** receiving until this warning is removed.
> [!TIP]
> Don't hesitate to use the implementation of existing wallets as a reference.
@ -173,6 +170,7 @@ The first argument is the [BOLT11 payment request](https://github.com/lightning/
> As mentioned above, this file must exist for every wallet and at least reexport everything in index.js so make sure that the following line is included:
>
> ```js
> // wallets/<wallet>/client.js
> export * from 'wallets/<name>'
> ```
>
@ -207,15 +205,16 @@ It should attempt to create a test invoice to make sure that this wallet can lat
Again, like `testSendPayment`, the first argument is the wallet configuration that we should validate and this should thrown an error if validation fails. However, unlike `testSendPayment`, the `context` argument here contains `me` (the user object) and `models` (the Prisma client).
- `createInvoice: async (amount: int, config, context) => Promise<bolt11: string>`
- `createInvoice: async (invoiceParams, config, context) => Promise<bolt11: string>`
`createInvoice` will be called whenever this wallet should receive a payment. It should return a BOLT11 payment request. The first argument `amount` specifies the amount in satoshis. The second argument `config` is the current configuration of this wallet. The third argument `context` is the same as in `testCreateInvoice` except it also includes `lnd` which is the return value of [`authenticatedLndGrpc`](https://github.com/alexbosworth/ln-service?tab=readme-ov-file#authenticatedlndgrpc) using the SN node credentials.
`createInvoice` will be called whenever this wallet should receive a payment. It should return a BOLT11 payment request. The first argument `invoiceParams` is an object that contains the invoice parameters. These include `msats`, `description`, `descriptionHash` and `expiry`. The second argument `config` is the current configuration of this wallet. The third argument `context` is the same as in `testCreateInvoice` except it also includes `lnd` which is the return value of [`authenticatedLndGrpc`](https://github.com/alexbosworth/ln-service?tab=readme-ov-file#authenticatedlndgrpc) using the SN node credentials.
> [!IMPORTANT]
> Don't forget to include the following line:
>
> ```js
> // wallets/<wallet>/server.js
> export * from 'wallets/<name>'
> ```
>