Compare commits

..

No commits in common. "dc0370ba1749c444cacb9c13d2179fa04c654ca5" and "d3ca87a78bfe85b37fd1fe2f94b8304c2cc3f33b" have entirely different histories.

16 changed files with 85 additions and 243 deletions

View File

@ -1,8 +1,8 @@
name: Lint Check name: Eslint Check
on: [pull_request] on: [pull_request]
jobs: jobs:
lint-run: eslint-run:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout

View File

@ -202,68 +202,3 @@ From the [postgres docs](https://www.postgresql.org/docs/current/transaction-iso
From the [postgres source docs](https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/executor/README#l350): From the [postgres source docs](https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/executor/README#l350):
> It is also possible that there are relations in the query that are not to be locked (they are neither the UPDATE/DELETE/MERGE target nor specified to be locked in SELECT FOR UPDATE/SHARE). When re-running the test query ***we want to use the same rows*** from these relations that were joined to the locked rows. > It is also possible that there are relations in the query that are not to be locked (they are neither the UPDATE/DELETE/MERGE target nor specified to be locked in SELECT FOR UPDATE/SHARE). When re-running the test query ***we want to use the same rows*** from these relations that were joined to the locked rows.
## `IMPORTANT: deadlocks`
Deadlocks can occur when two transactions are waiting for each other to release locks. This can happen when two transactions lock rows in different orders whether explicit or implicit.
### Incorrect
```sql
-- transaction 1
BEGIN;
UPDATE users set msats = msats + 1 WHERE id = 1;
-- transaction 2
BEGIN;
UPDATE users set msats = msats + 1 WHERE id = 2;
-- transaction 1 (blocks here until transaction 2 commits)
UPDATE users set msats = msats + 1 WHERE id = 2;
-- transaction 2 (blocks here until transaction 1 commits)
UPDATE users set msats = msats + 1 WHERE id = 1;
-- deadlock occurs because neither transaction can proceed to here
```
If both transactions lock the rows in the same order, the deadlock is avoided.
Most often this occurs when selecting multiple rows for update in different orders. Recently, we had a deadlock when spliting zaps to multiple users. The solution was to select the rows for update in the same order.
### Incorrect
```sql
WITH forwardees AS (
SELECT "userId", (($1::BIGINT * pct) / 100)::BIGINT AS msats
FROM "ItemForward"
WHERE "itemId" = $2::INTEGER
),
UPDATE users
SET
msats = users.msats + forwardees.msats,
"stackedMsats" = users."stackedMsats" + forwardees.msats
FROM forwardees
WHERE users.id = forwardees."userId";
```
If forwardees are selected in a different order in two concurrent transactions, e.g. (1,2) in tx 1 and (2,1) in tx 2, a deadlock can occur. To avoid this, always select rows for update in the same order.
### Correct
We fixed the deadlock by selecting the forwardees in the same order in these transactions.
```sql
WITH forwardees AS (
SELECT "userId", (($1::BIGINT * pct) / 100)::BIGINT AS msats
FROM "ItemForward"
WHERE "itemId" = $2::INTEGER
ORDER BY "userId" ASC
),
UPDATE users
SET
msats = users.msats + forwardees.msats,
"stackedMsats" = users."stackedMsats" + forwardees.msats
FROM forwardees
WHERE users.id = forwardees."userId";
```
### More resources
- https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-DEADLOCKS

View File

@ -77,19 +77,19 @@ export async function onPaid ({ invoice, actIds }, { models, tx }) {
), total_forwarded AS ( ), total_forwarded AS (
SELECT COALESCE(SUM(msats), 0) as msats SELECT COALESCE(SUM(msats), 0) as msats
FROM forwardees FROM forwardees
), recipients AS ( ), forward AS (
SELECT "userId", msats FROM forwardees UPDATE users
UNION SET
SELECT ${itemAct.item.userId}::INTEGER as "userId", msats = users.msats + forwardees.msats,
${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT as msats "stackedMsats" = users."stackedMsats" + forwardees.msats
ORDER BY "userId" ASC -- order to prevent deadlocks FROM forwardees
WHERE users.id = forwardees."userId"
) )
UPDATE users UPDATE users
SET SET
msats = users.msats + recipients.msats, msats = msats + ${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT,
"stackedMsats" = users."stackedMsats" + recipients.msats "stackedMsats" = "stackedMsats" + ${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT
FROM recipients WHERE id = ${itemAct.item.userId}::INTEGER`
WHERE users.id = recipients."userId"`
// perform denomormalized aggregates: weighted votes, upvotes, msats, lastZapAt // perform denomormalized aggregates: weighted votes, upvotes, msats, lastZapAt
// NOTE: for the rows that might be updated by a concurrent zap, we use UPDATE for implicit locking // NOTE: for the rows that might be updated by a concurrent zap, we use UPDATE for implicit locking

View File

@ -1004,12 +1004,6 @@ export default {
}) })
return relays?.map(r => r.nostrRelayAddr) return relays?.map(r => r.nostrRelayAddr)
},
tipRandom: async (user, args, { me }) => {
if (!me || me.id !== user.id) {
return false
}
return !!user.tipRandomMin && !!user.tipRandomMax
} }
}, },

View File

@ -98,8 +98,6 @@ export default gql`
noteItemMentions: Boolean! noteItemMentions: Boolean!
nsfwMode: Boolean! nsfwMode: Boolean!
tipDefault: Int! tipDefault: Int!
tipRandomMin: Int
tipRandomMax: Int
turboTipping: Boolean! turboTipping: Boolean!
zapUndos: Int zapUndos: Int
wildWestMode: Boolean! wildWestMode: Boolean!
@ -167,9 +165,6 @@ export default gql`
noteItemMentions: Boolean! noteItemMentions: Boolean!
nsfwMode: Boolean! nsfwMode: Boolean!
tipDefault: Int! tipDefault: Int!
tipRandom: Boolean!
tipRandomMin: Int
tipRandomMax: Int
turboTipping: Boolean! turboTipping: Boolean!
zapUndos: Int zapUndos: Int
wildWestMode: Boolean! wildWestMode: Boolean!

View File

@ -7,7 +7,7 @@ import UpBolt from '@/svgs/bolt.svg'
import { amountSchema } from '@/lib/validate' import { amountSchema } from '@/lib/validate'
import { useToast } from './toast' import { useToast } from './toast'
import { useLightning } from './lightning' import { useLightning } from './lightning'
import { nextTip, defaultTipIncludingRandom } from './upvote' import { nextTip } from './upvote'
import { ZAP_UNDO_DELAY_MS } from '@/lib/constants' import { ZAP_UNDO_DELAY_MS } from '@/lib/constants'
import { usePaidMutation } from './use-paid-mutation' import { usePaidMutation } from './use-paid-mutation'
import { ACT_MUTATION } from '@/fragments/paidAction' import { ACT_MUTATION } from '@/fragments/paidAction'
@ -101,7 +101,7 @@ export default function ItemAct ({ onClose, item, down, children, abortSignal })
return ( return (
<Form <Form
initial={{ initial={{
amount: defaultTipIncludingRandom(me?.privates) || defaultTips[0], amount: me?.privates?.tipDefault || defaultTips[0],
default: false default: false
}} }}
schema={amountSchema} schema={amountSchema}

View File

@ -26,7 +26,7 @@ const UpvotePopover = ({ target, show, handleClose }) => {
<button type='button' className='btn-close' onClick={handleClose}><span className='visually-hidden-focusable'>Close alert</span></button> <button type='button' className='btn-close' onClick={handleClose}><span className='visually-hidden-focusable'>Close alert</span></button>
</Popover.Header> </Popover.Header>
<Popover.Body> <Popover.Body>
<div className='mb-2'>Press the bolt again to zap {me?.privates?.tipRandom ? 'a random amount of' : `${me?.privates?.tipDefault || 1} more`} sat{me?.privates?.tipDefault > 1 ? 's' : ''}.</div> <div className='mb-2'>Press the bolt again to zap {me?.privates?.tipDefault || 1} more sat{me?.privates?.tipDefault > 1 ? 's' : ''}.</div>
<div>Repeatedly press the bolt to zap more sats.</div> <div>Repeatedly press the bolt to zap more sats.</div>
</Popover.Body> </Popover.Body>
</Popover> </Popover>
@ -67,20 +67,11 @@ export function DropdownItemUpVote ({ item }) {
) )
} }
export const defaultTipIncludingRandom = ({ tipDefault, tipRandom, tipRandomMin, tipRandomMax }) => export const nextTip = (meSats, { tipDefault, turboTipping }) => {
tipRandom
? Math.floor((Math.random() * (tipRandomMax - tipRandomMin)) + tipRandomMin)
: (tipDefault || 1)
export const nextTip = (meSats, { tipDefault, turboTipping, tipRandom, tipRandomMin, tipRandomMax }) => {
// what should our next tip be? // what should our next tip be?
const calculatedDefault = defaultTipIncludingRandom({ tipDefault, tipRandom, tipRandomMin, tipRandomMax }) if (!turboTipping) return (tipDefault || 1)
if (!turboTipping) { let sats = tipDefault || 1
return calculatedDefault
}
let sats = calculatedDefault
if (turboTipping) { if (turboTipping) {
while (meSats >= sats) { while (meSats >= sats) {
sats *= 10 sats *= 10
@ -147,17 +138,11 @@ export default function UpVote ({ item, className }) {
// what should our next tip be? // what should our next tip be?
const sats = nextTip(meSats, { ...me?.privates }) const sats = nextTip(meSats, { ...me?.privates })
let overlayTextContent
if (me) {
overlayTextContent = me.privates?.tipRandom ? 'random' : numWithUnits(sats, { abbreviate: false })
} else {
overlayTextContent = 'zap it'
}
return [ return [
meSats, overlayTextContent, meSats, me ? numWithUnits(sats, { abbreviate: false }) : 'zap it',
getColor(meSats), getColor(meSats + sats)] getColor(meSats), getColor(meSats + sats)]
}, [item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault, me?.privates?.tipRandom, me?.privates?.tipRandomMin, me?.privates?.tipRandomMax]) }, [item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault])
const handleModalClosed = () => { const handleModalClosed = () => {
setHover(false) setHover(false)

View File

@ -41,9 +41,6 @@ export const ME = gql`
noteItemMentions noteItemMentions
sats sats
tipDefault tipDefault
tipRandom
tipRandomMin
tipRandomMax
tipPopover tipPopover
turboTipping turboTipping
zapUndos zapUndos
@ -69,9 +66,6 @@ export const SETTINGS_FIELDS = gql`
fragment SettingsFields on User { fragment SettingsFields on User {
privates { privates {
tipDefault tipDefault
tipRandom
tipRandomMin
tipRandomMax
turboTipping turboTipping
zapUndos zapUndos
fiatCurrency fiatCurrency

View File

@ -540,24 +540,8 @@ export const actSchema = object({
act: string().required('required').oneOf(['TIP', 'DONT_LIKE_THIS']) act: string().required('required').oneOf(['TIP', 'DONT_LIKE_THIS'])
}) })
export const settingsSchema = object().shape({ export const settingsSchema = object({
tipDefault: intValidator.required('required').positive('must be positive'), tipDefault: intValidator.required('required').positive('must be positive'),
tipRandomMin: intValidator.nullable().positive('must be positive')
.when(['tipRandomMax'], ([max], schema) => {
let res = schema
if (max) {
res = schema.required('minimum and maximum must either both be omitted or specified').nonNullable()
}
return res.lessThan(max, 'must be less than maximum')
}),
tipRandomMax: intValidator.nullable().positive('must be positive')
.when(['tipRandomMin'], ([min], schema) => {
let res = schema
if (min) {
res = schema.required('minimum and maximum must either both be omitted or specified').nonNullable()
}
return res.moreThan(min, 'must be more than minimum')
}),
fiatCurrency: string().required('required').oneOf(SUPPORTED_CURRENCIES), fiatCurrency: string().required('required').oneOf(SUPPORTED_CURRENCIES),
withdrawMaxFeeDefault: intValidator.required('required').positive('must be positive'), withdrawMaxFeeDefault: intValidator.required('required').positive('must be positive'),
nostrPubkey: string().nullable() nostrPubkey: string().nullable()
@ -577,8 +561,7 @@ export const settingsSchema = object().shape({
noReferralLinks: boolean(), noReferralLinks: boolean(),
hideIsContributor: boolean(), hideIsContributor: boolean(),
zapUndos: intValidator.nullable().min(0, 'must be greater or equal to 0') zapUndos: intValidator.nullable().min(0, 'must be greater or equal to 0')
// exclude from cyclic analysis. see https://github.com/jquense/yup/issues/720 })
}, [['tipRandomMax', 'tipRandomMin']])
const warningMessage = 'If I logout, even accidentally, I will never be able to access my account again' const warningMessage = 'If I logout, even accidentally, I will never be able to access my account again'
export const lastAuthRemovalSchema = object({ export const lastAuthRemovalSchema = object({

View File

@ -112,9 +112,6 @@ export default function Settings ({ ssrData }) {
<Form <Form
initial={{ initial={{
tipDefault: settings?.tipDefault || 21, tipDefault: settings?.tipDefault || 21,
tipRandom: settings?.tipRandom,
tipRandomMin: settings?.tipRandomMin || 1,
tipRandomMax: settings?.tipRandomMax || settings?.tipDefault || 21,
turboTipping: settings?.turboTipping, turboTipping: settings?.turboTipping,
zapUndos: settings?.zapUndos || (settings?.tipDefault ? 100 * settings.tipDefault : 2100), zapUndos: settings?.zapUndos || (settings?.tipDefault ? 100 * settings.tipDefault : 2100),
zapUndosEnabled: settings?.zapUndos !== null, zapUndosEnabled: settings?.zapUndos !== null,
@ -152,7 +149,7 @@ export default function Settings ({ ssrData }) {
noReferralLinks: settings?.noReferralLinks noReferralLinks: settings?.noReferralLinks
}} }}
schema={settingsSchema} schema={settingsSchema}
onSubmit={async ({ tipDefault, tipRandom, tipRandomMin, tipRandomMax, withdrawMaxFeeDefault, zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, ...values }) => { onSubmit={async ({ tipDefault, withdrawMaxFeeDefault, zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, ...values }) => {
if (nostrPubkey.length === 0) { if (nostrPubkey.length === 0) {
nostrPubkey = null nostrPubkey = null
} else { } else {
@ -169,8 +166,6 @@ export default function Settings ({ ssrData }) {
variables: { variables: {
settings: { settings: {
tipDefault: Number(tipDefault), tipDefault: Number(tipDefault),
tipRandomMin: tipRandom ? Number(tipRandomMin) : null,
tipRandomMax: tipRandom ? Number(tipRandomMax) : null,
withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault), withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault),
zapUndos: zapUndosEnabled ? Number(zapUndos) : null, zapUndos: zapUndosEnabled ? Number(zapUndos) : null,
nostrPubkey, nostrPubkey,
@ -195,7 +190,7 @@ export default function Settings ({ ssrData }) {
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
hint={<small className='text-muted'>note: you can also press and hold the lightning bolt to zap custom amounts</small>} hint={<small className='text-muted'>note: you can also press and hold the lightning bolt to zap custom amounts</small>}
/> />
<div className='pb-4'> <div className='mb-2'>
<AccordianItem <AccordianItem
show={settings?.turboTipping} show={settings?.turboTipping}
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>advanced</div>} header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>advanced</div>}
@ -229,7 +224,6 @@ export default function Settings ({ ssrData }) {
groupClassName='mb-0' groupClassName='mb-0'
/> />
<ZapUndosField /> <ZapUndosField />
<TipRandomField />
</> </>
} }
/> />
@ -1001,79 +995,31 @@ function ApiKeyDeleteObstacle ({ onClose }) {
const ZapUndosField = () => { const ZapUndosField = () => {
const [checkboxField] = useField({ name: 'zapUndosEnabled' }) const [checkboxField] = useField({ name: 'zapUndosEnabled' })
return ( return (
<> <div className='d-flex flex-row align-items-center'>
<Checkbox <Input
name='zapUndosEnabled' name='zapUndos'
groupClassName='mb-0' disabled={!checkboxField.value}
label={ label={
<div className='d-flex align-items-center'> <Checkbox
zap undos name='zapUndosEnabled'
<Info> groupClassName='mb-0'
<ul className='fw-bold'> label={
<li>After every zap that exceeds or is equal to the threshold, the bolt will pulse</li> <div className='d-flex align-items-center'>
<li>You can undo the zap if you click the bolt while it's pulsing</li> zap undos
<li>The bolt will pulse for {ZAP_UNDO_DELAY_MS / 1000} seconds</li> <Info>
</ul> <ul className='fw-bold'>
</Info> <li>After every zap that exceeds or is equal to the threshold, the bolt will pulse</li>
</div> <li>You can undo the zap if you click the bolt while it's pulsing</li>
} <li>The bolt will pulse for {ZAP_UNDO_DELAY_MS / 1000} seconds</li>
/> </ul>
{checkboxField.value && </Info>
<Input </div>
name='zapUndos' }
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} />
hint={<small className='text-muted'>threshold at which undo button is shown</small>}
/>}
</>
)
}
const TipRandomField = () => {
const [tipRandomField] = useField({ name: 'tipRandom' })
const [tipRandomMinField] = useField({ name: 'tipRandomMin' })
const [tipRandomMaxField] = useField({ name: 'tipRandomMax' })
return (
<>
<Checkbox
name='tipRandom'
groupClassName='mb-0'
label={
<div className='d-flex align-items-center'>
Enable random zap values
<Info>
<ul className='fw-bold'>
<li>Set a minimum and maximum zap amount</li>
<li>Each time you zap something, a random amount of sats between your minimum and maximum will be zapped</li>
<li>If this setting is enabled, it will ignore your default zap amount</li>
</ul>
</Info>
</div>
} }
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
hint={<small className='text-muted'>threshold at which undo button is shown</small>}
/> />
{tipRandomField.value && </div>
<>
<Input
type='number'
label='minimum random zap'
name='tipRandomMin'
disabled={!tipRandomField.value}
groupClassName='mb-1'
required
autoFocus
max={tipRandomMaxField.value ? tipRandomMaxField.value - 1 : undefined}
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
<Input
type='number'
label='maximum random zap'
name='tipRandomMax'
disabled={!tipRandomField.value}
required
autoFocus
min={tipRandomMinField.value ? tipRandomMinField.value + 1 : undefined}
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
</>}
</>
) )
} }

View File

@ -1,5 +1,5 @@
import { getGetServerSideProps } from '@/api/ssrApollo' import { getGetServerSideProps } from '@/api/ssrApollo'
import { Form, ClientInput, ClientCheckbox, PasswordInput, CheckboxGroup } from '@/components/form' import { Form, ClientInput, ClientCheckbox, PasswordInput } from '@/components/form'
import { CenterLayout } from '@/components/layout' import { CenterLayout } from '@/components/layout'
import { WalletSecurityBanner } from '@/components/banners' import { WalletSecurityBanner } from '@/components/banners'
import { WalletLogs } from '@/components/wallet-logger' import { WalletLogs } from '@/components/wallet-logger'
@ -10,6 +10,7 @@ import Info from '@/components/info'
import Text from '@/components/text' import Text from '@/components/text'
import { AutowithdrawSettings } from '@/components/autowithdraw-shared' import { AutowithdrawSettings } from '@/components/autowithdraw-shared'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { useEffect, useState } from 'react'
const WalletButtonBar = dynamic(() => import('@/components/wallet-buttonbar.js'), { ssr: false }) const WalletButtonBar = dynamic(() => import('@/components/wallet-buttonbar.js'), { ssr: false })
@ -21,6 +22,14 @@ export default function WalletSettings () {
const { wallet: name } = router.query const { wallet: name } = router.query
const wallet = useWallet(name) const wallet = useWallet(name)
const [mounted, setMounted] = useState(false)
useEffect(() => {
// mounted is required since available might depend
// on values that are only available on the client (and not during SSR)
// and thus we need to render the component again on the client
setMounted(true)
}, [])
const initial = wallet.fields.reduce((acc, field) => { const initial = wallet.fields.reduce((acc, field) => {
// We still need to run over all wallet fields via reduce // We still need to run over all wallet fields via reduce
// even though we use wallet.config as the initial value // even though we use wallet.config as the initial value
@ -38,6 +47,8 @@ export default function WalletSettings () {
? { validate: wallet.fieldValidation } ? { validate: wallet.fieldValidation }
: { schema: wallet.fieldValidation } : { schema: wallet.fieldValidation }
const available = mounted && wallet.available !== undefined ? wallet.available : wallet.isConfigured
return ( return (
<CenterLayout> <CenterLayout>
<h2 className='pb-2'>{wallet.card.title}</h2> <h2 className='pb-2'>{wallet.card.title}</h2>
@ -73,15 +84,12 @@ export default function WalletSettings () {
{wallet.walletType {wallet.walletType
? <AutowithdrawSettings wallet={wallet} /> ? <AutowithdrawSettings wallet={wallet} />
: ( : (
<CheckboxGroup name='enabled'> <ClientCheckbox
<ClientCheckbox disabled={!available}
disabled={!wallet.isConfigured} initialValue={wallet.status === Status.Enabled}
initialValue={wallet.status === Status.Enabled} label='enabled'
label='enabled' name='enabled'
name='enabled' />
groupClassName='mb-0'
/>
</CheckboxGroup>
)} )}
<WalletButtonBar <WalletButtonBar
wallet={wallet} onDelete={async () => { wallet={wallet} onDelete={async () => {

View File

@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "tipRandomMax" INTEGER,
ADD COLUMN "tipRandomMin" INTEGER;

View File

@ -30,8 +30,6 @@ model User {
apiKeyHash String? @unique(map: "users.apikeyhash_unique") @db.Char(64) apiKeyHash String? @unique(map: "users.apikeyhash_unique") @db.Char(64)
apiKeyEnabled Boolean @default(false) apiKeyEnabled Boolean @default(false)
tipDefault Int @default(100) tipDefault Int @default(100)
tipRandomMin Int?
tipRandomMax Int?
bioId Int? bioId Int?
inviteId String? inviteId String?
tipPopover Boolean @default(false) tipPopover Boolean @default(false)

View File

@ -64,6 +64,10 @@ Since `name` will also be used in [wallet logs](https://stacker.news/wallet/logs
Wallet fields define what this wallet requires for configuration and thus are used to construct the forms like the one you can see at [/settings/wallets/lnbits](https://stacker.news/settings/walletslnbits). Wallet fields define what this wallet requires for configuration and thus are used to construct the forms like the one you can see at [/settings/wallets/lnbits](https://stacker.news/settings/walletslnbits).
- `available?: boolean`
This property can be used to override the default behavior of the `enabled` checkbox in the wallet configuration form. By default, it will be clickable when a wallet is configured. However, if a wallet does not have any configuration, this checkbox will always be disabled. You can set `available` to an expression that will determine when a wallet can be enabled.
- `card: WalletCard` - `card: WalletCard`
Wallet cards are the components you can see at [/settings/wallets](https://stacker.news/settings/wallets). This property customizes this card for this wallet. Wallet cards are the components you can see at [/settings/wallets](https://stacker.news/settings/wallets). This property customizes this card for this wallet.

View File

@ -137,6 +137,11 @@ function useConfig (wallet) {
...(hasServerConfig ? serverConfig : {}) ...(hasServerConfig ? serverConfig : {})
} }
if (wallet?.available !== undefined && config.enabled !== undefined) {
// wallet must be available to be enabled
config.enabled &&= wallet.available
}
const saveConfig = useCallback(async (config) => { const saveConfig = useCallback(async (config) => {
if (hasLocalConfig) setLocalConfig(config) if (hasLocalConfig) setLocalConfig(config)
if (hasServerConfig) await setServerConfig(config) if (hasServerConfig) await setServerConfig(config)
@ -260,7 +265,13 @@ export function getEnabledWallet (me) {
const priority = config?.priority const priority = config?.priority
return { ...def, config, priority } return { ...def, config, priority }
}) })
.filter(({ config }) => config?.enabled) .filter(({ available, config }) => {
if (available !== undefined && config?.enabled !== undefined) {
// wallet must be available to be enabled
config.enabled &&= available
}
return config?.enabled
})
.sort(walletPrioritySort)[0] .sort(walletPrioritySort)[0]
} }

View File

@ -1,18 +1,10 @@
import { SSR } from '@/lib/constants'
export const name = 'webln' export const name = 'webln'
export const fields = [] export const fields = []
export const fieldValidation = ({ enabled }) => { export const available = SSR ? false : typeof window.webln !== 'undefined'
if (typeof window.webln === 'undefined') {
// don't prevent disabling WebLN if no WebLN provider found
if (enabled) {
return {
enabled: 'no WebLN provider found'
}
}
}
return {}
}
export const card = { export const card = {
title: 'WebLN', title: 'WebLN',