refactor(ui): consolidate Send and Sweep precondition alerts (#1402)

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>
This commit is contained in:
Kishore B 2026-08-11 21:50:55 +05:30 committed by GitHub
parent d9918b30b0
commit 047a8ab779
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 285 additions and 410 deletions

View file

@ -1,97 +1,10 @@
import { AlertTriangleIcon } from 'lucide-react'
import { Trans, useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import type { SweepPreconditionSummary } from '@/components/sweep/preconditions'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Balance } from '@/components/ui/jam/Balance'
import { JAM_JM_RETRIES_DOCS_URL } from '@/constants/jam'
import { routes } from '@/constants/routes'
import { shortenStringMiddle } from '@/lib/utils'
import { JarBadge } from '../earn/fidelity-bond/FidelityBondDialogParts'
import { PreconditionAlert } from '@/components/ui/jam/PreconditionAlert'
interface SendCoinjoinPreconditionAlertProps {
summary: SweepPreconditionSummary
}
const RetryLockedUtxoList = ({ summary }: SendCoinjoinPreconditionAlertProps) => {
const { t } = useTranslation()
return (
<>
<Trans
i18nKey="send.coinjoin_precondition.hint_missing_retries"
components={{
'1': (
<a
key="retries-doc-link"
className="font-semibold"
href={JAM_JM_RETRIES_DOCS_URL}
target="_blank"
rel="noopener noreferrer"
/>
),
}}
/>
<div className="mt-3 p-2">
<div className="mb-2 text-xs font-semibold">{t('global.utxos')}</div>
<ul className="list-inside list-[circle] space-y-2">
{summary.retryLockedUtxos.map((utxo) => (
<li key={utxo.utxo} className="space-x-2">
<span className="slashed-zero tabular-nums">
<JarBadge jarIndex={utxo.mixdepth} />
</span>
<span className="font-mono select-all">{shortenStringMiddle(utxo.utxo, 26)}</span>
<Balance valueString={String(utxo.value)} />
</li>
))}
</ul>
</div>
</>
)
}
export const SendCoinjoinPreconditionAlert = ({ summary }: SendCoinjoinPreconditionAlertProps) => {
const { t } = useTranslation()
if (summary.isFulfilled) {
return null
}
return (
<Alert variant="warning">
<AlertTriangleIcon />
<AlertTitle>{t('send.coinjoin_precondition.title')}</AlertTitle>
<AlertDescription>
<ul className="list-outside list-disc space-y-2">
{summary.numberOfNonFrozenFidelityBondOutputs === 0 ? null : (
<li>
<Trans
i18nKey="send.coinjoin_precondition.hint_non_frozen_fidelity_bond"
values={{ count: summary.numberOfNonFrozenFidelityBondOutputs }}
components={{
'1': <Link to={routes.walletJarsDetails} className="font-semibold" />,
}}
/>
</li>
)}
{summary.numberOfMissingUtxos === 0 ? null : (
<li>
{t('send.coinjoin_precondition.hint_missing_utxos', {
minConfirmations: summary.options.minConfirmations,
})}
</li>
)}
{summary.numberOfMissingConfirmations === 0 ? null : (
<li>
{t('send.coinjoin_precondition.hint_missing_confirmations', {
minConfirmations: summary.options.minConfirmations,
amountOfMissingConfirmations: summary.numberOfMissingConfirmations,
})}
</li>
)}
{summary.retryLockedUtxos.length === 0 ? null : <li>{<RetryLockedUtxoList summary={summary} />}</li>}
</ul>
</AlertDescription>
</Alert>
)
return <PreconditionAlert summary={summary} i18nPrefix="send.coinjoin_precondition" />
}

View file

@ -1,97 +1,10 @@
import { AlertTriangleIcon } from 'lucide-react'
import { Trans, useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import type { SweepPreconditionSummary } from '@/components/sweep/preconditions'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Balance } from '@/components/ui/jam/Balance'
import { JAM_JM_RETRIES_DOCS_URL } from '@/constants/jam'
import { routes } from '@/constants/routes'
import { shortenStringMiddle } from '@/lib/utils'
import { JarBadge } from '../earn/fidelity-bond/FidelityBondDialogParts'
import { PreconditionAlert } from '@/components/ui/jam/PreconditionAlert'
interface SweepPreconditionAlertProps {
summary: SweepPreconditionSummary
}
const RetryLockedUtxoList = ({ summary }: SweepPreconditionAlertProps) => {
const { t } = useTranslation()
return (
<>
<Trans
i18nKey="scheduler.precondition.hint_missing_retries"
components={{
'1': (
<a
key="retries-doc-link"
className="font-semibold"
href={JAM_JM_RETRIES_DOCS_URL}
target="_blank"
rel="noopener noreferrer"
/>
),
}}
/>
<div className="mt-3 p-2">
<div className="mb-2 text-xs font-semibold">{t('global.utxos')}</div>
<ul className="list-inside list-[circle] space-y-2">
{summary.retryLockedUtxos.map((utxo) => (
<li key={utxo.utxo} className="space-x-2">
<span className="slashed-zero tabular-nums">
<JarBadge jarIndex={utxo.mixdepth} />
</span>
<span className="font-mono select-all">{shortenStringMiddle(utxo.utxo, 26)}</span>
<Balance valueString={String(utxo.value)} />
</li>
))}
</ul>
</div>
</>
)
}
export const SweepPreconditionAlert = ({ summary }: SweepPreconditionAlertProps) => {
const { t } = useTranslation()
if (summary.isFulfilled) {
return null
}
return (
<Alert variant="warning">
<AlertTriangleIcon />
<AlertTitle>{t('scheduler.precondition.title')}</AlertTitle>
<AlertDescription>
<ul className="list-outside list-disc space-y-2">
{summary.numberOfNonFrozenFidelityBondOutputs === 0 ? null : (
<li>
<Trans
i18nKey="scheduler.precondition.hint_non_frozen_fidelity_bond"
values={{ count: summary.numberOfNonFrozenFidelityBondOutputs }}
components={{
'1': <Link to={routes.walletJarsDetails} className="font-semibold" />,
}}
/>
</li>
)}
{summary.numberOfMissingUtxos === 0 ? null : (
<li>
{t('scheduler.precondition.hint_missing_utxos', {
minConfirmations: summary.options.minConfirmations,
})}
</li>
)}
{summary.numberOfMissingConfirmations === 0 ? null : (
<li>
{t('scheduler.precondition.hint_missing_confirmations', {
minConfirmations: summary.options.minConfirmations,
amountOfMissingConfirmations: summary.numberOfMissingConfirmations,
})}
</li>
)}
{summary.retryLockedUtxos.length === 0 ? null : <li>{<RetryLockedUtxoList summary={summary} />}</li>}
</ul>
</AlertDescription>
</Alert>
)
return <PreconditionAlert summary={summary} i18nPrefix="scheduler.precondition" />
}

View file

@ -0,0 +1,102 @@
import { AlertTriangleIcon } from 'lucide-react'
import { Trans, useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { JarBadge } from '@/components/earn/fidelity-bond/FidelityBondDialogParts'
import type { SweepPreconditionSummary } from '@/components/sweep/preconditions'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Balance } from '@/components/ui/jam/Balance'
import { JAM_JM_RETRIES_DOCS_URL } from '@/constants/jam'
import { routes } from '@/constants/routes'
import { shortenStringMiddle } from '@/lib/utils'
export interface PreconditionAlertProps {
summary: SweepPreconditionSummary
i18nPrefix: 'send.coinjoin_precondition' | 'scheduler.precondition'
}
const RetryLockedUtxoList = ({ summary, i18nPrefix }: PreconditionAlertProps) => {
const { t } = useTranslation()
return (
<>
<Trans
i18nKey={`${i18nPrefix}.hint_missing_retries`}
components={{
'1': (
<a
key="retries-doc-link"
className="font-semibold"
href={JAM_JM_RETRIES_DOCS_URL}
target="_blank"
rel="noopener noreferrer"
/>
),
}}
/>
<div className="mt-3 p-2">
<div className="mb-2 text-xs font-semibold">{t('global.utxos')}</div>
<ul className="list-inside list-[circle] space-y-2">
{summary.retryLockedUtxos.map((utxo) => (
<li key={utxo.utxo} className="space-x-2">
<span className="slashed-zero tabular-nums">
<JarBadge jarIndex={utxo.mixdepth} />
</span>
<span className="font-mono select-all">{shortenStringMiddle(utxo.utxo, 26)}</span>
<Balance valueString={String(utxo.value)} />
</li>
))}
</ul>
</div>
</>
)
}
export const PreconditionAlert = ({ summary, i18nPrefix }: PreconditionAlertProps) => {
const { t } = useTranslation()
if (summary.isFulfilled) {
return null
}
return (
<Alert variant="warning">
<AlertTriangleIcon />
<AlertTitle>{t(`${i18nPrefix}.title`)}</AlertTitle>
<AlertDescription>
<ul className="list-outside list-disc space-y-2">
{summary.numberOfNonFrozenFidelityBondOutputs === 0 ? null : (
<li>
<Trans
i18nKey={`${i18nPrefix}.hint_non_frozen_fidelity_bond`}
values={{ count: summary.numberOfNonFrozenFidelityBondOutputs }}
components={{
'1': <Link to={routes.walletJarsDetails} className="font-semibold" />,
}}
/>
</li>
)}
{summary.numberOfMissingUtxos === 0 ? null : (
<li>
{t(`${i18nPrefix}.hint_missing_utxos`, {
minConfirmations: summary.options.minConfirmations,
})}
</li>
)}
{summary.numberOfMissingConfirmations === 0 ? null : (
<li>
{t(`${i18nPrefix}.hint_missing_confirmations`, {
minConfirmations: summary.options.minConfirmations,
amountOfMissingConfirmations: summary.numberOfMissingConfirmations,
})}
</li>
)}
{summary.retryLockedUtxos.length === 0 ? null : (
<li>
<RetryLockedUtxoList summary={summary} i18nPrefix={i18nPrefix} />
</li>
)}
</ul>
</AlertDescription>
</Alert>
)
}

View file

@ -0,0 +1,179 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import { SWEEP_PRECONDITION_DEFAULT_OPTIONS } from '@/components/sweep/preconditions'
import { PreconditionAlert } from '@/components/ui/jam/PreconditionAlert'
import type { Utxo } from '@/hooks/useQueryUtxos'
const DEFAULT_SUMMARY = Object.freeze({
isFulfilled: false,
options: SWEEP_PRECONDITION_DEFAULT_OPTIONS,
numberOfMissingUtxos: 0,
numberOfMissingConfirmations: 0,
numberOfNonFrozenFidelityBondOutputs: 0,
retryLockedUtxos: [],
})
const makeUtxo = (overrides: Partial<Utxo> = {}): Utxo =>
({
address: 'bc1qsource',
confirmations: 6,
frozen: false,
label: '',
locktime: undefined,
mixdepth: 0,
path: '',
tries_remaining: 3,
utxo: '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0',
value: 100_000,
...overrides,
}) as Utxo
const meta: Meta<typeof PreconditionAlert> = {
title: 'Jam/PreconditionAlert',
component: PreconditionAlert,
tags: ['autodocs'],
args: {
summary: DEFAULT_SUMMARY,
i18nPrefix: 'send.coinjoin_precondition',
},
}
export default meta
type Story = StoryObj<typeof PreconditionAlert>
// --- Send Flow Stories ---
export const SendDefault: Story = {
args: {
i18nPrefix: 'send.coinjoin_precondition',
},
}
export const SendMissingUtxos: Story = {
args: {
i18nPrefix: 'send.coinjoin_precondition',
summary: {
...DEFAULT_SUMMARY,
numberOfMissingUtxos: 3,
},
},
}
export const SendMissingConfirmations: Story = {
args: {
i18nPrefix: 'send.coinjoin_precondition',
summary: {
...DEFAULT_SUMMARY,
numberOfMissingConfirmations: 3,
},
},
}
export const SendNonFrozenFidelityBondsPresent: Story = {
args: {
i18nPrefix: 'send.coinjoin_precondition',
summary: {
...DEFAULT_SUMMARY,
numberOfNonFrozenFidelityBondOutputs: 3,
},
},
}
export const SendRetryLockedUtxos: Story = {
args: {
i18nPrefix: 'send.coinjoin_precondition',
summary: {
...DEFAULT_SUMMARY,
retryLockedUtxos: [
makeUtxo({ mixdepth: 0, value: 21 }),
makeUtxo({ mixdepth: 1, value: 21_000 }),
makeUtxo({ mixdepth: 2, value: 21_000_000 }),
],
},
},
}
export const SendMultiple: Story = {
args: {
i18nPrefix: 'send.coinjoin_precondition',
summary: {
...DEFAULT_SUMMARY,
numberOfMissingUtxos: 3,
numberOfMissingConfirmations: 3,
numberOfNonFrozenFidelityBondOutputs: 3,
retryLockedUtxos: [
makeUtxo({ mixdepth: 0, value: 21 }),
makeUtxo({ mixdepth: 1, value: 21_000 }),
makeUtxo({ mixdepth: 2, value: 21_000_000 }),
],
},
},
}
// --- Sweep Flow Stories ---
export const SweepDefault: Story = {
args: {
i18nPrefix: 'scheduler.precondition',
},
}
export const SweepMissingUtxos: Story = {
args: {
i18nPrefix: 'scheduler.precondition',
summary: {
...DEFAULT_SUMMARY,
numberOfMissingUtxos: 3,
},
},
}
export const SweepMissingConfirmations: Story = {
args: {
i18nPrefix: 'scheduler.precondition',
summary: {
...DEFAULT_SUMMARY,
numberOfMissingConfirmations: 3,
},
},
}
export const SweepNonFrozenFidelityBondsPresent: Story = {
args: {
i18nPrefix: 'scheduler.precondition',
summary: {
...DEFAULT_SUMMARY,
numberOfNonFrozenFidelityBondOutputs: 3,
},
},
}
export const SweepRetryLockedUtxos: Story = {
args: {
i18nPrefix: 'scheduler.precondition',
summary: {
...DEFAULT_SUMMARY,
retryLockedUtxos: [
makeUtxo({ mixdepth: 0, value: 21 }),
makeUtxo({ mixdepth: 1, value: 21_000 }),
makeUtxo({ mixdepth: 2, value: 21_000_000 }),
],
},
},
}
export const SweepMultiple: Story = {
args: {
i18nPrefix: 'scheduler.precondition',
summary: {
...DEFAULT_SUMMARY,
numberOfMissingUtxos: 3,
numberOfMissingConfirmations: 3,
numberOfNonFrozenFidelityBondOutputs: 3,
retryLockedUtxos: [
makeUtxo({ mixdepth: 0, value: 21 }),
makeUtxo({ mixdepth: 1, value: 21_000 }),
makeUtxo({ mixdepth: 2, value: 21_000_000 }),
],
},
},
}

View file

@ -1,116 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import { SendCoinjoinPreconditionAlert } from '@/components/send/SendCoinjoinPreconditionAlert'
import { SWEEP_PRECONDITION_DEFAULT_OPTIONS } from '@/components/sweep/preconditions'
import type { Utxo } from '@/hooks/useQueryUtxos'
const DEFAULT_SUMMARY = Object.freeze({
isFulfilled: false,
options: SWEEP_PRECONDITION_DEFAULT_OPTIONS,
numberOfMissingUtxos: 0,
numberOfMissingConfirmations: 0,
numberOfNonFrozenFidelityBondOutputs: 0,
retryLockedUtxos: [],
})
const makeUtxo = (overrides: Partial<Utxo> = {}): Utxo =>
({
address: 'bc1qsource',
confirmations: 6,
frozen: false,
label: '',
locktime: undefined,
mixdepth: 0,
path: '',
tries_remaining: 3,
utxo: '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0',
value: 100_000,
...overrides,
}) as Utxo
const meta: Meta<typeof SendCoinjoinPreconditionAlert> = {
title: 'Jam/SendCoinjoinPreconditionAlert',
component: SendCoinjoinPreconditionAlert,
tags: ['autodocs'],
args: {
summary: DEFAULT_SUMMARY,
},
}
export default meta
type Story = StoryObj<typeof SendCoinjoinPreconditionAlert>
export const Default: Story = {}
export const MissingUtxos: Story = {
args: {
summary: {
...DEFAULT_SUMMARY,
numberOfMissingUtxos: 3,
},
},
}
export const MissingConfirmations: Story = {
args: {
summary: {
...DEFAULT_SUMMARY,
numberOfMissingConfirmations: 3,
},
},
}
export const NonFrozenFidelityBondsPresent: Story = {
args: {
summary: {
...DEFAULT_SUMMARY,
numberOfNonFrozenFidelityBondOutputs: 3,
},
},
}
export const RetryLockedUtxos: Story = {
args: {
summary: {
...DEFAULT_SUMMARY,
retryLockedUtxos: [
makeUtxo({
mixdepth: 0,
value: 21,
}),
makeUtxo({
mixdepth: 1,
value: 21_000,
}),
makeUtxo({
mixdepth: 2,
value: 21_000_000,
}),
],
},
},
}
export const Multiple: Story = {
args: {
summary: {
...DEFAULT_SUMMARY,
numberOfMissingUtxos: 3,
numberOfMissingConfirmations: 3,
numberOfNonFrozenFidelityBondOutputs: 3,
retryLockedUtxos: [
makeUtxo({
mixdepth: 0,
value: 21,
}),
makeUtxo({
mixdepth: 1,
value: 21_000,
}),
makeUtxo({
mixdepth: 2,
value: 21_000_000,
}),
],
},
},
}

View file

@ -1,116 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import { SweepPreconditionAlert } from '@/components/sweep/SweepPreconditionAlert'
import { SWEEP_PRECONDITION_DEFAULT_OPTIONS } from '@/components/sweep/preconditions'
import type { Utxo } from '@/hooks/useQueryUtxos'
const DEFAULT_SUMMARY = Object.freeze({
isFulfilled: false,
options: SWEEP_PRECONDITION_DEFAULT_OPTIONS,
numberOfMissingUtxos: 0,
numberOfMissingConfirmations: 0,
numberOfNonFrozenFidelityBondOutputs: 0,
retryLockedUtxos: [],
})
const makeUtxo = (overrides: Partial<Utxo> = {}): Utxo =>
({
address: 'bc1qsource',
confirmations: 6,
frozen: false,
label: '',
locktime: undefined,
mixdepth: 0,
path: '',
tries_remaining: 3,
utxo: '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0',
value: 100_000,
...overrides,
}) as Utxo
const meta: Meta<typeof SweepPreconditionAlert> = {
title: 'Jam/SweepPreconditionAlert',
component: SweepPreconditionAlert,
tags: ['autodocs'],
args: {
summary: DEFAULT_SUMMARY,
},
}
export default meta
type Story = StoryObj<typeof SweepPreconditionAlert>
export const Default: Story = {}
export const MissingUtxos: Story = {
args: {
summary: {
...DEFAULT_SUMMARY,
numberOfMissingUtxos: 3,
},
},
}
export const MissingConfirmations: Story = {
args: {
summary: {
...DEFAULT_SUMMARY,
numberOfMissingConfirmations: 3,
},
},
}
export const NonFrozenFidelityBondsPresent: Story = {
args: {
summary: {
...DEFAULT_SUMMARY,
numberOfNonFrozenFidelityBondOutputs: 3,
},
},
}
export const RetryLockedUtxos: Story = {
args: {
summary: {
...DEFAULT_SUMMARY,
retryLockedUtxos: [
makeUtxo({
mixdepth: 0,
value: 21,
}),
makeUtxo({
mixdepth: 1,
value: 21_000,
}),
makeUtxo({
mixdepth: 2,
value: 21_000_000,
}),
],
},
},
}
export const Multiple: Story = {
args: {
summary: {
...DEFAULT_SUMMARY,
numberOfMissingUtxos: 3,
numberOfMissingConfirmations: 3,
numberOfNonFrozenFidelityBondOutputs: 3,
retryLockedUtxos: [
makeUtxo({
mixdepth: 0,
value: 21,
}),
makeUtxo({
mixdepth: 1,
value: 21_000,
}),
makeUtxo({
mixdepth: 2,
value: 21_000_000,
}),
],
},
},
}