Compare commits
No commits in common. "dc0370ba1749c444cacb9c13d2179fa04c654ca5" and "d3ca87a78bfe85b37fd1fe2f94b8304c2cc3f33b" have entirely different histories.
dc0370ba17
...
d3ca87a78b
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@ -1,8 +1,8 @@
|
||||
name: Lint Check
|
||||
name: Eslint Check
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
lint-run:
|
||||
eslint-run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
@ -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):
|
||||
> 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
|
||||
|
@ -77,19 +77,19 @@ export async function onPaid ({ invoice, actIds }, { models, tx }) {
|
||||
), total_forwarded AS (
|
||||
SELECT COALESCE(SUM(msats), 0) as msats
|
||||
FROM forwardees
|
||||
), recipients AS (
|
||||
SELECT "userId", msats FROM forwardees
|
||||
UNION
|
||||
SELECT ${itemAct.item.userId}::INTEGER as "userId",
|
||||
${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT as msats
|
||||
ORDER BY "userId" ASC -- order to prevent deadlocks
|
||||
), forward AS (
|
||||
UPDATE users
|
||||
SET
|
||||
msats = users.msats + forwardees.msats,
|
||||
"stackedMsats" = users."stackedMsats" + forwardees.msats
|
||||
FROM forwardees
|
||||
WHERE users.id = forwardees."userId"
|
||||
)
|
||||
UPDATE users
|
||||
SET
|
||||
msats = users.msats + recipients.msats,
|
||||
"stackedMsats" = users."stackedMsats" + recipients.msats
|
||||
FROM recipients
|
||||
WHERE users.id = recipients."userId"`
|
||||
msats = msats + ${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT,
|
||||
"stackedMsats" = "stackedMsats" + ${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT
|
||||
WHERE id = ${itemAct.item.userId}::INTEGER`
|
||||
|
||||
// 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
|
||||
|
@ -1004,12 +1004,6 @@ export default {
|
||||
})
|
||||
|
||||
return relays?.map(r => r.nostrRelayAddr)
|
||||
},
|
||||
tipRandom: async (user, args, { me }) => {
|
||||
if (!me || me.id !== user.id) {
|
||||
return false
|
||||
}
|
||||
return !!user.tipRandomMin && !!user.tipRandomMax
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -98,8 +98,6 @@ export default gql`
|
||||
noteItemMentions: Boolean!
|
||||
nsfwMode: Boolean!
|
||||
tipDefault: Int!
|
||||
tipRandomMin: Int
|
||||
tipRandomMax: Int
|
||||
turboTipping: Boolean!
|
||||
zapUndos: Int
|
||||
wildWestMode: Boolean!
|
||||
@ -167,9 +165,6 @@ export default gql`
|
||||
noteItemMentions: Boolean!
|
||||
nsfwMode: Boolean!
|
||||
tipDefault: Int!
|
||||
tipRandom: Boolean!
|
||||
tipRandomMin: Int
|
||||
tipRandomMax: Int
|
||||
turboTipping: Boolean!
|
||||
zapUndos: Int
|
||||
wildWestMode: Boolean!
|
||||
|
@ -7,7 +7,7 @@ import UpBolt from '@/svgs/bolt.svg'
|
||||
import { amountSchema } from '@/lib/validate'
|
||||
import { useToast } from './toast'
|
||||
import { useLightning } from './lightning'
|
||||
import { nextTip, defaultTipIncludingRandom } from './upvote'
|
||||
import { nextTip } from './upvote'
|
||||
import { ZAP_UNDO_DELAY_MS } from '@/lib/constants'
|
||||
import { usePaidMutation } from './use-paid-mutation'
|
||||
import { ACT_MUTATION } from '@/fragments/paidAction'
|
||||
@ -101,7 +101,7 @@ export default function ItemAct ({ onClose, item, down, children, abortSignal })
|
||||
return (
|
||||
<Form
|
||||
initial={{
|
||||
amount: defaultTipIncludingRandom(me?.privates) || defaultTips[0],
|
||||
amount: me?.privates?.tipDefault || defaultTips[0],
|
||||
default: false
|
||||
}}
|
||||
schema={amountSchema}
|
||||
|
@ -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>
|
||||
</Popover.Header>
|
||||
<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>
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
@ -67,20 +67,11 @@ export function DropdownItemUpVote ({ item }) {
|
||||
)
|
||||
}
|
||||
|
||||
export const defaultTipIncludingRandom = ({ tipDefault, tipRandom, tipRandomMin, tipRandomMax }) =>
|
||||
tipRandom
|
||||
? Math.floor((Math.random() * (tipRandomMax - tipRandomMin)) + tipRandomMin)
|
||||
: (tipDefault || 1)
|
||||
|
||||
export const nextTip = (meSats, { tipDefault, turboTipping, tipRandom, tipRandomMin, tipRandomMax }) => {
|
||||
export const nextTip = (meSats, { tipDefault, turboTipping }) => {
|
||||
// what should our next tip be?
|
||||
const calculatedDefault = defaultTipIncludingRandom({ tipDefault, tipRandom, tipRandomMin, tipRandomMax })
|
||||
if (!turboTipping) return (tipDefault || 1)
|
||||
|
||||
if (!turboTipping) {
|
||||
return calculatedDefault
|
||||
}
|
||||
|
||||
let sats = calculatedDefault
|
||||
let sats = tipDefault || 1
|
||||
if (turboTipping) {
|
||||
while (meSats >= sats) {
|
||||
sats *= 10
|
||||
@ -147,17 +138,11 @@ export default function UpVote ({ item, className }) {
|
||||
|
||||
// what should our next tip be?
|
||||
const sats = nextTip(meSats, { ...me?.privates })
|
||||
let overlayTextContent
|
||||
if (me) {
|
||||
overlayTextContent = me.privates?.tipRandom ? 'random' : numWithUnits(sats, { abbreviate: false })
|
||||
} else {
|
||||
overlayTextContent = 'zap it'
|
||||
}
|
||||
|
||||
return [
|
||||
meSats, overlayTextContent,
|
||||
meSats, me ? numWithUnits(sats, { abbreviate: false }) : 'zap it',
|
||||
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 = () => {
|
||||
setHover(false)
|
||||
|
@ -41,9 +41,6 @@ export const ME = gql`
|
||||
noteItemMentions
|
||||
sats
|
||||
tipDefault
|
||||
tipRandom
|
||||
tipRandomMin
|
||||
tipRandomMax
|
||||
tipPopover
|
||||
turboTipping
|
||||
zapUndos
|
||||
@ -69,9 +66,6 @@ export const SETTINGS_FIELDS = gql`
|
||||
fragment SettingsFields on User {
|
||||
privates {
|
||||
tipDefault
|
||||
tipRandom
|
||||
tipRandomMin
|
||||
tipRandomMax
|
||||
turboTipping
|
||||
zapUndos
|
||||
fiatCurrency
|
||||
|
@ -540,24 +540,8 @@ export const actSchema = object({
|
||||
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'),
|
||||
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),
|
||||
withdrawMaxFeeDefault: intValidator.required('required').positive('must be positive'),
|
||||
nostrPubkey: string().nullable()
|
||||
@ -577,8 +561,7 @@ export const settingsSchema = object().shape({
|
||||
noReferralLinks: boolean(),
|
||||
hideIsContributor: boolean(),
|
||||
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'
|
||||
export const lastAuthRemovalSchema = object({
|
||||
|
@ -112,9 +112,6 @@ export default function Settings ({ ssrData }) {
|
||||
<Form
|
||||
initial={{
|
||||
tipDefault: settings?.tipDefault || 21,
|
||||
tipRandom: settings?.tipRandom,
|
||||
tipRandomMin: settings?.tipRandomMin || 1,
|
||||
tipRandomMax: settings?.tipRandomMax || settings?.tipDefault || 21,
|
||||
turboTipping: settings?.turboTipping,
|
||||
zapUndos: settings?.zapUndos || (settings?.tipDefault ? 100 * settings.tipDefault : 2100),
|
||||
zapUndosEnabled: settings?.zapUndos !== null,
|
||||
@ -152,7 +149,7 @@ export default function Settings ({ ssrData }) {
|
||||
noReferralLinks: settings?.noReferralLinks
|
||||
}}
|
||||
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) {
|
||||
nostrPubkey = null
|
||||
} else {
|
||||
@ -169,8 +166,6 @@ export default function Settings ({ ssrData }) {
|
||||
variables: {
|
||||
settings: {
|
||||
tipDefault: Number(tipDefault),
|
||||
tipRandomMin: tipRandom ? Number(tipRandomMin) : null,
|
||||
tipRandomMax: tipRandom ? Number(tipRandomMax) : null,
|
||||
withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault),
|
||||
zapUndos: zapUndosEnabled ? Number(zapUndos) : null,
|
||||
nostrPubkey,
|
||||
@ -195,7 +190,7 @@ export default function Settings ({ ssrData }) {
|
||||
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>}
|
||||
/>
|
||||
<div className='pb-4'>
|
||||
<div className='mb-2'>
|
||||
<AccordianItem
|
||||
show={settings?.turboTipping}
|
||||
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>advanced</div>}
|
||||
@ -229,7 +224,6 @@ export default function Settings ({ ssrData }) {
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<ZapUndosField />
|
||||
<TipRandomField />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@ -1001,79 +995,31 @@ function ApiKeyDeleteObstacle ({ onClose }) {
|
||||
const ZapUndosField = () => {
|
||||
const [checkboxField] = useField({ name: 'zapUndosEnabled' })
|
||||
return (
|
||||
<>
|
||||
<Checkbox
|
||||
name='zapUndosEnabled'
|
||||
groupClassName='mb-0'
|
||||
<div className='d-flex flex-row align-items-center'>
|
||||
<Input
|
||||
name='zapUndos'
|
||||
disabled={!checkboxField.value}
|
||||
label={
|
||||
<div className='d-flex align-items-center'>
|
||||
zap undos
|
||||
<Info>
|
||||
<ul className='fw-bold'>
|
||||
<li>After every zap that exceeds or is equal to the threshold, the bolt will pulse</li>
|
||||
<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>
|
||||
</Info>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{checkboxField.value &&
|
||||
<Input
|
||||
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>
|
||||
<Checkbox
|
||||
name='zapUndosEnabled'
|
||||
groupClassName='mb-0'
|
||||
label={
|
||||
<div className='d-flex align-items-center'>
|
||||
zap undos
|
||||
<Info>
|
||||
<ul className='fw-bold'>
|
||||
<li>After every zap that exceeds or is equal to the threshold, the bolt will pulse</li>
|
||||
<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>
|
||||
</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 &&
|
||||
<>
|
||||
<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>}
|
||||
/>
|
||||
</>}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { WalletSecurityBanner } from '@/components/banners'
|
||||
import { WalletLogs } from '@/components/wallet-logger'
|
||||
@ -10,6 +10,7 @@ import Info from '@/components/info'
|
||||
import Text from '@/components/text'
|
||||
import { AutowithdrawSettings } from '@/components/autowithdraw-shared'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
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 = 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) => {
|
||||
// We still need to run over all wallet fields via reduce
|
||||
// even though we use wallet.config as the initial value
|
||||
@ -38,6 +47,8 @@ export default function WalletSettings () {
|
||||
? { validate: wallet.fieldValidation }
|
||||
: { schema: wallet.fieldValidation }
|
||||
|
||||
const available = mounted && wallet.available !== undefined ? wallet.available : wallet.isConfigured
|
||||
|
||||
return (
|
||||
<CenterLayout>
|
||||
<h2 className='pb-2'>{wallet.card.title}</h2>
|
||||
@ -73,15 +84,12 @@ export default function WalletSettings () {
|
||||
{wallet.walletType
|
||||
? <AutowithdrawSettings wallet={wallet} />
|
||||
: (
|
||||
<CheckboxGroup name='enabled'>
|
||||
<ClientCheckbox
|
||||
disabled={!wallet.isConfigured}
|
||||
initialValue={wallet.status === Status.Enabled}
|
||||
label='enabled'
|
||||
name='enabled'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
</CheckboxGroup>
|
||||
<ClientCheckbox
|
||||
disabled={!available}
|
||||
initialValue={wallet.status === Status.Enabled}
|
||||
label='enabled'
|
||||
name='enabled'
|
||||
/>
|
||||
)}
|
||||
<WalletButtonBar
|
||||
wallet={wallet} onDelete={async () => {
|
||||
|
@ -1,3 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "tipRandomMax" INTEGER,
|
||||
ADD COLUMN "tipRandomMin" INTEGER;
|
@ -30,8 +30,6 @@ model User {
|
||||
apiKeyHash String? @unique(map: "users.apikeyhash_unique") @db.Char(64)
|
||||
apiKeyEnabled Boolean @default(false)
|
||||
tipDefault Int @default(100)
|
||||
tipRandomMin Int?
|
||||
tipRandomMax Int?
|
||||
bioId Int?
|
||||
inviteId String?
|
||||
tipPopover Boolean @default(false)
|
||||
|
@ -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).
|
||||
|
||||
- `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`
|
||||
|
||||
Wallet cards are the components you can see at [/settings/wallets](https://stacker.news/settings/wallets). This property customizes this card for this wallet.
|
||||
|
@ -137,6 +137,11 @@ function useConfig (wallet) {
|
||||
...(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) => {
|
||||
if (hasLocalConfig) setLocalConfig(config)
|
||||
if (hasServerConfig) await setServerConfig(config)
|
||||
@ -260,7 +265,13 @@ export function getEnabledWallet (me) {
|
||||
const priority = 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]
|
||||
}
|
||||
|
||||
|
@ -1,18 +1,10 @@
|
||||
import { SSR } from '@/lib/constants'
|
||||
|
||||
export const name = 'webln'
|
||||
|
||||
export const fields = []
|
||||
|
||||
export const fieldValidation = ({ enabled }) => {
|
||||
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 available = SSR ? false : typeof window.webln !== 'undefined'
|
||||
|
||||
export const card = {
|
||||
title: 'WebLN',
|
||||
|
Loading…
x
Reference in New Issue
Block a user