mirror of
https://github.com/joinmarket-webui/jam.git
synced 2026-08-17 13:07:34 +02:00
ui(send): add warn sign to selected jar on precondition violations (#648)
* ui(send): move precondition violation alert below jar selector * ui(send): display warn sign in selectable jar on precondition violations * ui(send): improve sending-in-progress screen * ui(send): add checkmark to payment success alert
This commit is contained in:
parent
7a6b920fd2
commit
77c3ec91ed
4 changed files with 86 additions and 51 deletions
|
|
@ -240,6 +240,9 @@
|
|||
<symbol id="info" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="7.25" r="1.25"></circle><path d="M11 10h2v8h-2z"></path>
|
||||
</symbol>
|
||||
<symbol id="warn" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="16.75" r="1.25"></circle><path d="M11 6h2v8h-2z"></path>
|
||||
</symbol>
|
||||
<symbol id="lock" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<rect width="14" height="10" x="5" y="10.968" rx="2"></rect>
|
||||
<path d="M15.486 10.984V7.243a1.5 1.5 0 00-1.5-1.5h-3.972a1.5 1.5 0 00-1.5 1.5v3.74"></path>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
|
@ -58,11 +58,6 @@ function SweepAccordionToggle({ eventKey }: SweepAccordionToggleProps) {
|
|||
)
|
||||
}
|
||||
|
||||
type Alert = {
|
||||
variant: 'success' | 'danger'
|
||||
message: string
|
||||
}
|
||||
|
||||
type SendProps = {
|
||||
wallet: CurrentWallet
|
||||
}
|
||||
|
|
@ -78,7 +73,7 @@ export default function Send({ wallet }: SendProps) {
|
|||
const isCoinjoinInProgress = useMemo(() => serviceInfo && serviceInfo.coinjoinInProgress, [serviceInfo])
|
||||
const isMakerRunning = useMemo(() => serviceInfo && serviceInfo.makerRunning, [serviceInfo])
|
||||
|
||||
const [alert, setAlert] = useState<Alert>()
|
||||
const [alert, setAlert] = useState<SimpleAlert>()
|
||||
const [isSending, setIsSending] = useState(false)
|
||||
const [isCoinjoin, setIsCoinjoin] = useState(IS_COINJOIN_DEFAULT_VAL)
|
||||
const [minNumCollaborators, setMinNumCollaborators] = useState(MINIMUM_MAKERS_DEFAULT_VAL)
|
||||
|
|
@ -92,7 +87,7 @@ export default function Send({ wallet }: SendProps) {
|
|||
const [showFeeConfigModal, setShowFeeConfigModal] = useState(false)
|
||||
|
||||
const [waitForUtxosToBeSpent, setWaitForUtxosToBeSpent] = useState([])
|
||||
const [paymentSuccessfulInfoAlert, setPaymentSuccessfulInfoAlert] = useState<Alert>()
|
||||
const [paymentSuccessfulInfoAlert, setPaymentSuccessfulInfoAlert] = useState<SimpleAlert>()
|
||||
|
||||
const isOperationDisabled = useMemo(
|
||||
() => isCoinjoinInProgress || isMakerRunning || waitForUtxosToBeSpent.length > 0,
|
||||
|
|
@ -143,7 +138,7 @@ export default function Send({ wallet }: SendProps) {
|
|||
return walletInfo.data.utxos.utxos.filter((it) => it.mixdepth === sourceJarIndex)
|
||||
}, [walletInfo, sourceJarIndex])
|
||||
|
||||
const coinjoinPreconditionSummary = useMemo(() => {
|
||||
const sourceJarCoinjoinPreconditionSummary = useMemo(() => {
|
||||
if (sourceJarUtxos === null) return null
|
||||
return buildCoinjoinRequirementSummary(sourceJarUtxos)
|
||||
}, [sourceJarUtxos])
|
||||
|
|
@ -160,23 +155,24 @@ export default function Send({ wallet }: SendProps) {
|
|||
const submitButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const submitButtonOptions = useMemo(() => {
|
||||
if (!isLoading) {
|
||||
if (!isCoinjoin)
|
||||
if (!isCoinjoin) {
|
||||
return {
|
||||
variant: 'danger',
|
||||
text: t('send.button_send_without_improved_privacy'),
|
||||
}
|
||||
if (coinjoinPreconditionSummary && !coinjoinPreconditionSummary.isFulfilled)
|
||||
} else if (sourceJarCoinjoinPreconditionSummary?.isFulfilled === false) {
|
||||
return {
|
||||
variant: 'warning',
|
||||
text: t('send.button_send_despite_warning'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
variant: 'dark',
|
||||
text: t('send.button_send'),
|
||||
}
|
||||
}, [isLoading, isCoinjoin, coinjoinPreconditionSummary, t])
|
||||
}, [isLoading, isCoinjoin, sourceJarCoinjoinPreconditionSummary, t])
|
||||
|
||||
const formIsValid = useMemo(() => {
|
||||
return (
|
||||
|
|
@ -593,25 +589,35 @@ export default function Send({ wallet }: SendProps) {
|
|||
<rb.Row className="align-items-center">
|
||||
<rb.Col>{t('send.text_maker_running')}</rb.Col>
|
||||
<rb.Col xs="auto">
|
||||
<Sprite symbol="caret-right" width="24px" height="24px" />
|
||||
<Sprite symbol="caret-right" width="24" height="24" />
|
||||
</rb.Col>
|
||||
</rb.Row>
|
||||
</rb.Alert>
|
||||
</Link>
|
||||
)}
|
||||
{isCoinjoinInProgress && (
|
||||
<rb.Alert variant="info" className="mb-4 d-flex align-items-center">
|
||||
{t('send.text_coinjoin_already_running')}
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="d-flex align-items-center justify-content-center mb-2">
|
||||
<div className="d-flex align-items-center justify-content-center alert alert-success rounded-circle p-3">
|
||||
<Sprite symbol="clock" width="32" height="32" />
|
||||
</div>
|
||||
</div>
|
||||
<rb.Alert variant="success" className="d-flex align-items-center">
|
||||
{t('send.text_coinjoin_already_running')}
|
||||
<Sprite className="ms-auto" symbol="joining" width="20" height="20" />
|
||||
</rb.Alert>
|
||||
<rb.Button
|
||||
variant={'outline-light'}
|
||||
className="ms-auto"
|
||||
variant="none"
|
||||
className="w-100 mb-4"
|
||||
disabled={showConfirmAbortModal}
|
||||
onClick={() => abortCoinjoin()}
|
||||
>
|
||||
{t('global.abort')}
|
||||
<div className="d-flex justify-content-center align-items-center">
|
||||
<Sprite symbol="cancel" width="24" height="24" className="me-1" />
|
||||
{t('global.abort')}
|
||||
</div>
|
||||
</rb.Button>
|
||||
</rb.Alert>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</rb.Fade>
|
||||
|
|
@ -621,22 +627,17 @@ export default function Send({ wallet }: SendProps) {
|
|||
</rb.Alert>
|
||||
)}
|
||||
{paymentSuccessfulInfoAlert && (
|
||||
<rb.Alert className="small slashed-zeroes break-word" variant={paymentSuccessfulInfoAlert.variant}>
|
||||
{paymentSuccessfulInfoAlert.message}
|
||||
</rb.Alert>
|
||||
)}
|
||||
{!isLoading &&
|
||||
!isOperationDisabled &&
|
||||
isCoinjoin &&
|
||||
coinjoinPreconditionSummary &&
|
||||
!coinjoinPreconditionSummary.isFulfilled && (
|
||||
<div className="mb-4">
|
||||
<CoinjoinPreconditionViolationAlert
|
||||
summary={coinjoinPreconditionSummary}
|
||||
i18nPrefix="send.coinjoin_precondition."
|
||||
/>
|
||||
<>
|
||||
<div className="d-flex align-items-center justify-content-center mb-2">
|
||||
<div className="d-flex align-items-center justify-content-center alert alert-success rounded-circle p-3">
|
||||
<Sprite symbol="checkmark" width="24" height="24" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<rb.Alert className="small slashed-zeroes break-word" variant={paymentSuccessfulInfoAlert.variant}>
|
||||
{paymentSuccessfulInfoAlert.message}
|
||||
</rb.Alert>
|
||||
</>
|
||||
)}
|
||||
{!isLoading && walletInfo && (
|
||||
<JarSelectorModal
|
||||
isShown={destinationJarPickerShown}
|
||||
|
|
@ -692,12 +693,32 @@ export default function Send({ wallet }: SendProps) {
|
|||
it.calculatedTotalBalanceInSats,
|
||||
walletInfo.balanceSummary.calculatedTotalBalanceInSats
|
||||
)}
|
||||
variant={
|
||||
it.accountIndex === sourceJarIndex &&
|
||||
isCoinjoin &&
|
||||
sourceJarCoinjoinPreconditionSummary?.isFulfilled === false
|
||||
? 'warning'
|
||||
: undefined
|
||||
}
|
||||
onClick={(jarIndex) => setSourceJarIndex(jarIndex)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</rb.Form.Group>
|
||||
|
||||
{!isLoading &&
|
||||
!isOperationDisabled &&
|
||||
isCoinjoin &&
|
||||
sourceJarCoinjoinPreconditionSummary?.isFulfilled === false && (
|
||||
<div className="mb-4">
|
||||
<CoinjoinPreconditionViolationAlert
|
||||
summary={sourceJarCoinjoinPreconditionSummary}
|
||||
i18nPrefix="send.coinjoin_precondition."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<rb.Form.Group className="mb-4" controlId="destination">
|
||||
<rb.Form.Label>{t('send.label_recipient')}</rb.Form.Label>
|
||||
<div className="position-relative">
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@
|
|||
--bs-code-color: var(--bs-blue);
|
||||
color: var(--bs-blue);
|
||||
font-size: 0.75rem;
|
||||
min-height: 1rem;
|
||||
}
|
||||
|
||||
.selectableJarContainer {
|
||||
|
|
@ -77,21 +78,20 @@
|
|||
cursor: none;
|
||||
}
|
||||
|
||||
.selectableJarContainer > .selectionCircle {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
margin-top: 0.8rem;
|
||||
.selectableJarContainer .selectionCircle {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
border: 1px solid var(--bs-body-color);
|
||||
}
|
||||
|
||||
.selectableJarContainer:not(.selectable) > .selectionCircle {
|
||||
.selectableJarContainer:not(.selectable) .selectionCircle {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.selectableJarContainer.selected > .selectionCircle {
|
||||
.selectableJarContainer.selected .selectionCircle {
|
||||
visibility: visible !important;
|
||||
background-color: var(--bs-body-color);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ interface JarProps {
|
|||
interface SelectableJarProps {
|
||||
isSelectable: boolean
|
||||
isSelected: boolean
|
||||
variant?: 'default' | 'warning'
|
||||
onClick: (index: JarIndex) => void
|
||||
}
|
||||
|
||||
|
|
@ -127,16 +128,18 @@ const Jar = ({ index, balance, frozenBalance, fillLevel, isOpen = false }: JarPr
|
|||
<div className={`${styles.jarBalance} jar-balance-container-hook`}>
|
||||
<Balance valueString={balance.toString()} convertToUnit={settings.unit} showBalance={settings.showBalance} />
|
||||
</div>
|
||||
{frozenBalance && frozenBalance > 0 ? (
|
||||
<div className={`${styles.jarBalance} ${styles.frozen} small jar-balance-container-hook`}>
|
||||
<Sprite symbol="snowflake" width="14" height="14" />
|
||||
<Balance
|
||||
valueString={frozenBalance.toString()}
|
||||
convertToUnit={settings.unit}
|
||||
showBalance={settings.showBalance}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={`${styles.jarBalance} ${styles.frozen} jar-balance-container-hook`}>
|
||||
{frozenBalance && frozenBalance > 0 ? (
|
||||
<>
|
||||
<Sprite symbol="snowflake" width="14" height="14" />
|
||||
<Balance
|
||||
valueString={frozenBalance.toString()}
|
||||
convertToUnit={settings.unit}
|
||||
showBalance={settings.showBalance}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -153,6 +156,7 @@ const SelectableJar = ({
|
|||
isSelectable,
|
||||
isSelected,
|
||||
onClick,
|
||||
variant = 'default',
|
||||
}: JarProps & SelectableJarProps) => {
|
||||
return (
|
||||
<div
|
||||
|
|
@ -163,7 +167,14 @@ const SelectableJar = ({
|
|||
onClick={() => isSelectable && onClick(index)}
|
||||
>
|
||||
<Jar index={index} balance={balance} frozenBalance={frozenBalance} fillLevel={fillLevel} />
|
||||
<div className={styles.selectionCircle}></div>
|
||||
<div className="d-flex justify-content-center align-items-center gap-1 mt-2 position-relative">
|
||||
<div className={styles.selectionCircle} />
|
||||
{variant === 'warning' && (
|
||||
<div className="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning text-dark p-0">
|
||||
<Sprite symbol="warn" width="20" height="20" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue