From 3402e98da601599f06ce2e9b21ea7b412a462b13 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Sat, 14 Oct 2023 19:59:12 +0200 Subject: [PATCH 1/8] wip(renew): add renew button to expired fb --- src/components/Earn.tsx | 32 +++++++++++++++++----------- src/i18n/locales/en/translation.json | 3 ++- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/components/Earn.tsx b/src/components/Earn.tsx index 9b5da7bc..b8704c7b 100644 --- a/src/components/Earn.tsx +++ b/src/components/Earn.tsx @@ -424,6 +424,7 @@ export default function Earn({ wallet }: EarnProps) { }, [currentWalletInfo]) const [moveToJarFidelityBondId, setMoveToJarFidelityBondId] = useState() + const [renewFidelityBondId, setRenewFidelityBondId] = useState() const startMakerService = useCallback( (values: EarnFormValues) => { @@ -633,18 +634,25 @@ export default function Earn({ wallet }: EarnProps) { return ( {actionsEnabled && ( -
-
- setMoveToJarFidelityBondId(fidelityBond.utxo)} - > - - {t('earn.fidelity_bond.existing.button_spend')} - -
+
+ setMoveToJarFidelityBondId(fidelityBond.utxo)} + > + + {t('earn.fidelity_bond.existing.button_spend')} + + setRenewFidelityBondId(fidelityBond.utxo)} + > + + {t('earn.fidelity_bond.existing.button_renew')} +
)} diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 05b0a228..68116b76 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -515,7 +515,8 @@ "label_locked_until": "Locked until", "label_expired_on": "Expired on", "label_address": "Timelocked address", - "button_spend": "Unlock Funds" + "button_spend": "Unlock Funds", + "button_renew": "Renew Bond" }, "move": { "title": "Unlock Funds", From 960100b7bd181a5fd2d5577d5dabb43a0f323dee Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Sat, 14 Oct 2023 23:03:34 +0200 Subject: [PATCH 2/8] wip(renew): ability to renew specific fidelity bond --- src/components/Earn.tsx | 16 +- src/components/fb/CreateFidelityBond.tsx | 4 +- src/components/fb/FidelityBondSteps.tsx | 12 +- src/components/fb/LockdateForm.tsx | 9 +- src/components/fb/SpendFidelityBondModal.tsx | 455 ++++++++++++++++--- src/i18n/locales/en/translation.json | 12 + 6 files changed, 442 insertions(+), 66 deletions(-) diff --git a/src/components/Earn.tsx b/src/components/Earn.tsx index b8704c7b..64b10007 100644 --- a/src/components/Earn.tsx +++ b/src/components/Earn.tsx @@ -14,7 +14,7 @@ import PageTitle from './PageTitle' import SegmentedTabs from './SegmentedTabs' import { CreateFidelityBond } from './fb/CreateFidelityBond' import { ExistingFidelityBond } from './fb/ExistingFidelityBond' -import { SpendFidelityBondModal } from './fb/SpendFidelityBondModal' +import { RenewFidelityBondModal, SpendFidelityBondModal } from './fb/SpendFidelityBondModal' import { EarnReportOverlay } from './EarnReport' import { OrderbookOverlay } from './Orderbook' import Balance from './Balance' @@ -620,6 +620,20 @@ export default function Earn({ wallet }: EarnProps) { }} /> )} + {currentWalletInfo && renewFidelityBondId && ( + { + setRenewFidelityBondId(undefined) + if (mustReload) { + reloadFidelityBonds({ delay: 0 }) + } + }} + /> + )} {fidelityBonds.map((fidelityBond, index) => { const isExpired = !fb.utxo.isLocked(fidelityBond) const actionsEnabled = diff --git a/src/components/fb/CreateFidelityBond.tsx b/src/components/fb/CreateFidelityBond.tsx index bd21685c..c83c55ca 100644 --- a/src/components/fb/CreateFidelityBond.tsx +++ b/src/components/fb/CreateFidelityBond.tsx @@ -254,8 +254,8 @@ const CreateFidelityBond = ({ otherFidelityBondExists, wallet, walletInfo, onDon return ( setLockDate(date)} + yearsRange={yearsRange} + onChange={(date) => setLockDate(date)} /> ) case steps.selectJar: diff --git a/src/components/fb/FidelityBondSteps.tsx b/src/components/fb/FidelityBondSteps.tsx index 1dfd3174..bc6f3fcc 100644 --- a/src/components/fb/FidelityBondSteps.tsx +++ b/src/components/fb/FidelityBondSteps.tsx @@ -10,17 +10,15 @@ import { SelectableJar, jarInitial, jarFillLevel } from '../jars/Jar' import Sprite from '../Sprite' import Balance from '../Balance' import { CopyButton } from '../CopyButton' -import LockdateForm from './LockdateForm' +import LockdateForm, { LockdateFormProps } from './LockdateForm' import * as fb from './utils' import styles from './FidelityBondSteps.module.css' const cx = classnamesBind.bind(styles) -interface SelectDateProps { +type SelectDateProps = { description: string - selectableYearsRange: fb.YearsRange - onDateSelected: (lockdate: Api.Lockdate | null) => void -} +} & LockdateFormProps interface SelectJarProps { description: string @@ -70,11 +68,11 @@ interface CreatedFidelityBondProps { frozenUtxos: Array } -const SelectDate = ({ description, selectableYearsRange, onDateSelected }: SelectDateProps) => { +const SelectDate = ({ description, yearsRange, disabled, onChange }: SelectDateProps) => { return (
{description}
- onDateSelected(date)} yearsRange={selectableYearsRange} /> +
) } diff --git a/src/components/fb/LockdateForm.tsx b/src/components/fb/LockdateForm.tsx index 5f6005df..748d2f27 100644 --- a/src/components/fb/LockdateForm.tsx +++ b/src/components/fb/LockdateForm.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import * as rb from 'react-bootstrap' import { Trans, useTranslation } from 'react-i18next' @@ -58,13 +58,14 @@ export const _selectableYears = (yearsRange: fb.YearsRange, now = new Date()): n .map((_, index) => index + now.getUTCFullYear() + extra) } -interface LockdateFormProps { +export interface LockdateFormProps { onChange: (lockdate: Api.Lockdate | null) => void yearsRange?: fb.YearsRange now?: Date + disabled?: boolean } -const LockdateForm = ({ onChange, now, yearsRange }: LockdateFormProps) => { +const LockdateForm = ({ onChange, now, yearsRange, disabled }: LockdateFormProps) => { const { i18n } = useTranslation() const _now = useMemo(() => now || new Date(), [now]) const _yearsRange = useMemo(() => yearsRange || fb.DEFAULT_TIMELOCK_YEARS_RANGE, [yearsRange]) @@ -115,6 +116,7 @@ const LockdateForm = ({ onChange, now, yearsRange }: LockdateFormProps) => { onChange={(e) => setLockdateYear(parseInt(e.target.value, 10))} required isInvalid={!isLockdateYearValid} + disabled={disabled} data-testid="select-lockdate-year" > {selectableYears.map((year) => ( @@ -135,6 +137,7 @@ const LockdateForm = ({ onChange, now, yearsRange }: LockdateFormProps) => { onChange={(e) => setLockdateMonth(parseInt(e.target.value, 10) as Month)} required isInvalid={!isLockdateMonthValid} + disabled={disabled} data-testid="select-lockdate-month" > {selectableMonths.map((it) => ( diff --git a/src/components/fb/SpendFidelityBondModal.tsx b/src/components/fb/SpendFidelityBondModal.tsx index 741cebc8..874d44de 100644 --- a/src/components/fb/SpendFidelityBondModal.tsx +++ b/src/components/fb/SpendFidelityBondModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import * as rb from 'react-bootstrap' import { TFunction } from 'i18next' import { useTranslation } from 'react-i18next' @@ -7,12 +7,14 @@ import * as Api from '../../libs/JmWalletApi' import * as fb from './utils' import Alert from '../Alert' import Sprite from '../Sprite' -import { SelectJar } from './FidelityBondSteps' +import { SelectDate, SelectJar } from './FidelityBondSteps' import { PaymentConfirmModal } from '../PaymentConfirmModal' import { jarInitial } from '../jars/Jar' import { useFeeConfigValues } from '../../hooks/Fees' import styles from './SpendFidelityBondModal.module.css' +import { isDebugFeatureEnabled } from '../../constants/debugFeatures' +import { CopyButton } from '../CopyButton' type Input = { outpoint: Api.UtxoId @@ -138,6 +140,394 @@ const spendUtxosWithDirectSend = async ( } } +type SendFidelityBondToAddressProps = { + fidelityBond: Utxo | undefined + destination: Api.BitcoinAddress + wallet: CurrentWallet + t: TFunction +} + +const sendFidelityBondToAddress = async ({ fidelityBond, destination, wallet, t }: SendFidelityBondToAddressProps) => { + if (!fidelityBond || fb.utxo.isLocked(fidelityBond)) { + throw new Error(t('earn.fidelity_bond.move.error_fidelity_bond_still_locked')) + } + + const abortCtrl = new AbortController() + const requestContext = { ...wallet, signal: abortCtrl.signal } + + return await spendUtxosWithDirectSend( + requestContext, + { + destination, + sourceJarIndex: fidelityBond.mixdepth, + utxos: [fidelityBond], + }, + { + onReloadWalletError: (res) => + Api.Helper.throwResolved(res, errorResolver(t, 'global.errors.error_reloading_wallet_failed')), + onFreezeUtxosError: (res) => + Api.Helper.throwResolved(res, errorResolver(t, 'earn.fidelity_bond.move.error_freezing_utxos')), + onUnfreezeUtxosError: (res) => + Api.Helper.throwResolved(res, errorResolver(t, 'earn.fidelity_bond.move.error_unfreezing_fidelity_bond')), + onSendError: (res) => + Api.Helper.throwResolved(res, errorResolver(t, 'earn.fidelity_bond.move.error_spending_fidelity_bond')), + }, + ) +} + +type SendFidelityBondToJarProps = { + fidelityBond: Utxo | undefined + targetJarIndex: JarIndex + wallet: CurrentWallet + t: TFunction +} + +const sendFidelityBondToJar = async ({ fidelityBond, targetJarIndex, wallet, t }: SendFidelityBondToJarProps) => { + if (!fidelityBond || fb.utxo.isLocked(fidelityBond)) { + throw new Error(t('earn.fidelity_bond.move.error_fidelity_bond_still_locked')) + } + + const abortCtrl = new AbortController() + const requestContext = { ...wallet, signal: abortCtrl.signal } + + const destination = await Api.getAddressNew({ ...requestContext, mixdepth: targetJarIndex }) + .then((res) => { + if (res.ok) return res.json() + return Api.Helper.throwResolved(res, errorResolver(t, 'earn.fidelity_bond.move.error_loading_address')) + }) + .then((data) => data.address as Api.BitcoinAddress) + + return await sendFidelityBondToAddress({ destination, fidelityBond, wallet, t }) +} + +const Done = ({ text }: { text: string }) => { + return ( +
+
+ +
+
{text}
+
+ ) +} + +type RenewFidelityBondModalProps = { + fidelityBondId: Api.UtxoId + wallet: CurrentWallet + walletInfo: WalletInfo + onClose: (result: Result) => void +} & Omit + +const RenewFidelityBondModal = ({ + fidelityBondId, + wallet, + walletInfo, + onClose, + ...modalProps +}: RenewFidelityBondModalProps) => { + const { t } = useTranslation() + const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() + const feeConfigValues = useFeeConfigValues()[0] + + const [alert, setAlert] = useState() + + const [txInfo, setTxInfo] = useState() + const [waitForUtxosToBeSpent, setWaitForUtxosToBeSpent] = useState([]) + + const [lockDate, setLockDate] = useState() + const [timelockedAddress, setTimelockedAddress] = useState() + + const [parentMustReload, setParentMustReload] = useState(false) + const [isSending, setIsSending] = useState(false) + const [isLoadingTimelockedAddress, setIsLoadingTimelockAddress] = useState(false) + const isLoading = useMemo(() => isSending || waitForUtxosToBeSpent.length > 0, [isSending, waitForUtxosToBeSpent]) + + const [showConfirmSendModal, setShowConfirmSendModal] = useState(false) + + const submitButtonRef = useRef(null) + + const fidelityBond = useMemo(() => { + return walletInfo.data.utxos.utxos.find((utxo) => utxo.utxo === fidelityBondId) + }, [walletInfo, fidelityBondId]) + + // This callback is responsible for updating the loading state when the + // the payment is made. The wallet needs some time after a tx is sent + // to reflect the changes internally. All outputs in + // `waitForUtxosToBeSpent` must have been removed from the wallet + // for the payment to be considered done. + useEffect(() => { + if (waitForUtxosToBeSpent.length === 0) return + + const abortCtrl = new AbortController() + + // Delaying the poll requests gives the wallet some time to synchronize + // the utxo set and reduces amount of http requests + const initialDelayInMs = 250 + const timer = setTimeout(() => { + if (abortCtrl.signal.aborted) return + + reloadCurrentWalletInfo + .reloadUtxos({ signal: abortCtrl.signal }) + .then((res) => { + if (abortCtrl.signal.aborted) return + + const outputs = res.utxos.map((it) => it.utxo) + const utxosStillPresent = waitForUtxosToBeSpent.filter((it) => outputs.includes(it)) + setWaitForUtxosToBeSpent([...utxosStillPresent]) + }) + .catch((err) => { + if (abortCtrl.signal.aborted) return + + // Stop waiting for wallet synchronization on errors, but inform + // the user that loading the wallet info failed + setWaitForUtxosToBeSpent([]) + + const message = t('global.errors.error_reloading_wallet_failed', { + reason: err.message || t('global.errors.reason_unknown'), + }) + setAlert({ variant: 'danger', message }) + }) + }, initialDelayInMs) + + return () => { + abortCtrl.abort() + clearTimeout(timer) + } + }, [waitForUtxosToBeSpent, reloadCurrentWalletInfo, t]) + + const yearsRange = useMemo(() => { + if (isDebugFeatureEnabled('allowCreatingExpiredFidelityBond')) { + return fb.toYearsRange(-1, fb.DEFAULT_MAX_TIMELOCK_YEARS) + } + return fb.toYearsRange(0, fb.DEFAULT_MAX_TIMELOCK_YEARS) + }, []) + + const loadTimeLockedAddress = useCallback( + (lockdate: Api.Lockdate, signal: AbortSignal) => { + setIsLoadingTimelockAddress(true) + setAlert(undefined) + + return ( + Api.getAddressTimelockNew({ + ...wallet, + lockdate, + signal, + }) + .then((res) => { + return res.ok ? res.json() : Api.Helper.throwError(res, t('earn.fidelity_bond.error_loading_address')) + }) + // show the loader a little longer to avoid flickering + .then((result) => new Promise((r) => setTimeout(() => r(result), 221))) + .then((data: any) => { + if (signal.aborted) return + setTimelockedAddress(data.address) + setIsLoadingTimelockAddress(false) + }) + .catch((err) => { + if (signal.aborted) return + setAlert({ variant: 'danger', message: err.message }) + setIsLoadingTimelockAddress(false) + }) + ) + }, + [wallet, t], + ) + + useEffect(() => { + if (!lockDate) return + const abortCtrl = new AbortController() + loadTimeLockedAddress(lockDate, abortCtrl.signal) + return () => abortCtrl.abort() + }, [loadTimeLockedAddress, lockDate]) + + const primaryButtonContent = useMemo(() => { + if (isSending) { + return ( + <> +
@@ -421,4 +770,4 @@ const SpendFidelityBondModal = ({ ) } -export { SpendFidelityBondModal } +export { RenewFidelityBondModal, SpendFidelityBondModal } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 68116b76..4965e2c4 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -8,6 +8,7 @@ "close": "Close", "abort": "Abort", "cancel": "Cancel", + "done": "Done", "table": { "pagination": { "items_per_page": { @@ -518,6 +519,17 @@ "button_spend": "Unlock Funds", "button_renew": "Renew Bond" }, + "renew": { + "title": "Renew Bond", + "text_loading": "Loading...", + "text_sending": "Renewing...", + "text_button_submit": "Renew Bond", + "success_text": "Fidelity Bond renewed successfully!", + "error_renewing_fidelity_bond": "Error while renewing expired fidelity bond.", + "confirm_send_modal": { + "title": "Confirm renewing expired Fidelity Bond" + } + }, "move": { "title": "Unlock Funds", "success_text": "Fidelity Bond unlocked successfully!", From 9118ee28fa4910fda4cc90a135bc2fc9d9d2124a Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Wed, 18 Oct 2023 11:34:41 +0200 Subject: [PATCH 3/8] feat(fb): show mining fee settings on fb creation/renewal --- src/components/PaymentConfirmModal.tsx | 12 ++- src/components/fb/CreateFidelityBond.tsx | 79 ++++++++++++++------ src/components/fb/FidelityBondSteps.tsx | 2 +- src/components/fb/SpendFidelityBondModal.tsx | 9 ++- 4 files changed, 71 insertions(+), 31 deletions(-) diff --git a/src/components/PaymentConfirmModal.tsx b/src/components/PaymentConfirmModal.tsx index 6554ce4a..57aaab2d 100644 --- a/src/components/PaymentConfirmModal.tsx +++ b/src/components/PaymentConfirmModal.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { PropsWithChildren, useMemo } from 'react' import { Trans, useTranslation } from 'react-i18next' import * as rb from 'react-bootstrap' import Sprite from './Sprite' @@ -6,10 +6,10 @@ import Balance from './Balance' import { useSettings } from '../context/SettingsContext' import { FeeValues, TxFee, useEstimatedMaxCollaboratorFee } from '../hooks/Fees' import { ConfirmModal, ConfirmModalProps } from './Modal' -import styles from './PaymentConfirmModal.module.css' import { AmountSats } from '../libs/JmWalletApi' import { jarInitial } from './jars/Jar' import { isValidNumber } from '../utils' +import styles from './PaymentConfirmModal.module.css' const feeRange: (txFee: TxFee, txFeeFactor: number) => [number, number] = (txFee, txFeeFactor) => { if (txFee.unit !== 'sats/kilo-vbyte') { @@ -81,8 +81,9 @@ export function PaymentConfirmModal({ feeConfigValues, showPrivacyInfo = true, }, + children, ...confirmModalProps -}: PaymentConfirmModalProps) { +}: PropsWithChildren) { const { t } = useTranslation() const settings = useSettings() @@ -206,6 +207,11 @@ export function PaymentConfirmModal({ )} + {children && ( + + {children} + + )} ) diff --git a/src/components/fb/CreateFidelityBond.tsx b/src/components/fb/CreateFidelityBond.tsx index c83c55ca..f0c72a85 100644 --- a/src/components/fb/CreateFidelityBond.tsx +++ b/src/components/fb/CreateFidelityBond.tsx @@ -5,7 +5,6 @@ import { Trans, useTranslation } from 'react-i18next' import { CurrentWallet, Utxo, Utxos, WalletInfo, useReloadCurrentWalletInfo } from '../../context/WalletContext' import Alert from '../Alert' import Sprite from '../Sprite' -import { ConfirmModal } from '../Modal' import { SelectJar, SelectUtxos, @@ -18,9 +17,33 @@ import { import * as fb from './utils' import { isDebugFeatureEnabled } from '../../constants/debugFeatures' import styles from './CreateFidelityBond.module.css' +import { PaymentConfirmModal } from '../PaymentConfirmModal' +import { useFeeConfigValues } from '../../hooks/Fees' const TIMEOUT_RELOAD_UTXOS_AFTER_FB_CREATE_MS = 2_500 +export const LockInfoAlert = ({ lockDate, className }: { lockDate: Api.Lockdate; className?: string }) => { + const { t, i18n } = useTranslation() + + return ( + + {t('earn.fidelity_bond.confirm_modal.body', { + date: new Date(fb.lockdate.toTimestamp(lockDate)).toUTCString(), + humanReadableDuration: fb.time.humanReadableDuration({ + to: fb.lockdate.toTimestamp(lockDate), + locale: i18n.resolvedLanguage || i18n.language, + }), + })} + + } + /> + ) +} + const steps = { selectDate: 0, selectJar: 1, @@ -41,8 +64,9 @@ interface CreateFidelityBondProps { } const CreateFidelityBond = ({ otherFidelityBondExists, wallet, walletInfo, onDone }: CreateFidelityBondProps) => { + const { t } = useTranslation() const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() - const { t, i18n } = useTranslation() + const feeConfigValues = useFeeConfigValues()[0] const [isExpanded, setIsExpanded] = useState(false) const [isLoading, setIsLoading] = useState(false) @@ -53,17 +77,19 @@ const CreateFidelityBond = ({ otherFidelityBondExists, wallet, walletInfo, onDon const [lockDate, setLockDate] = useState(null) const [selectedJar, setSelectedJar] = useState() const [selectedUtxos, setSelectedUtxos] = useState([]) - const [timelockedAddress, setTimelockedAddress] = useState(null) + const [timelockedAddress, setTimelockedAddress] = useState() const [utxoIdsToBeSpent, setUtxoIdsToBeSpent] = useState([]) const [createdFidelityBondUtxo, setCreatedFidelityBondUtxo] = useState() const [frozenUtxos, setFrozenUtxos] = useState([]) - const allUtxosSelected = useMemo(() => { - return ( - walletInfo.balanceSummary.calculatedTotalBalanceInSats === - selectedUtxos.map((it) => it.value).reduce((prev, curr) => prev + curr, 0) - ) - }, [walletInfo, selectedUtxos]) + const selectedUtxosTotalValue = useMemo( + () => selectedUtxos.map((it) => it.value).reduce((prev, curr) => prev + curr, 0), + [selectedUtxos], + ) + const allUtxosSelected = useMemo( + () => walletInfo.balanceSummary.calculatedTotalBalanceInSats === selectedUtxosTotalValue, + [walletInfo, selectedUtxosTotalValue], + ) const yearsRange = useMemo(() => { if (isDebugFeatureEnabled('allowCreatingExpiredFidelityBond')) { @@ -79,7 +105,7 @@ const CreateFidelityBond = ({ otherFidelityBondExists, wallet, walletInfo, onDon setSelectedJar(undefined) setSelectedUtxos([]) setLockDate(null) - setTimelockedAddress(null) + setTimelockedAddress(undefined) setAlert(undefined) setCreatedFidelityBondUtxo(undefined) setFrozenUtxos([]) @@ -307,7 +333,7 @@ const CreateFidelityBond = ({ otherFidelityBondExists, wallet, walletInfo, onDon ) } - if (timelockedAddress === null) { + if (!timelockedAddress) { return
{t('earn.fidelity_bond.error_loading_address')}
} @@ -386,7 +412,7 @@ const CreateFidelityBond = ({ otherFidelityBondExists, wallet, walletInfo, onDon return t('earn.fidelity_bond.freeze_utxos.text_primary_button') case steps.reviewInputs: - if (timelockedAddress === null) return t('earn.fidelity_bond.review_inputs.text_primary_button_error') + if (!timelockedAddress) return t('earn.fidelity_bond.review_inputs.text_primary_button_error') if (!onlyCjOutOrFbUtxosSelected()) { return t('earn.fidelity_bond.review_inputs.text_primary_button_unsafe') @@ -522,7 +548,7 @@ const CreateFidelityBond = ({ otherFidelityBondExists, wallet, walletInfo, onDon loadTimeLockedAddress(lockDate!) } - if (step === steps.reviewInputs && timelockedAddress === null) { + if (step === steps.reviewInputs && !timelockedAddress) { loadTimeLockedAddress(lockDate!) return } @@ -568,26 +594,31 @@ const CreateFidelityBond = ({ otherFidelityBondExists, wallet, walletInfo, onDon return (
{alert && setAlert(undefined)} />} - {lockDate && ( - setShowConfirmInputsModal(false)} onConfirm={() => { setStep(steps.createFidelityBond) setShowConfirmInputsModal(false) - directSweepToFidelityBond(selectedJar!, timelockedAddress!) + directSweepToFidelityBond(selectedJar, timelockedAddress) + }} + data={{ + sourceJarIndex: undefined, // dont show a source jar - might be confusing in this context + destination: timelockedAddress, + amount: selectedUtxosTotalValue, + isSweep: true, + isCoinjoin: false, // not sent as collaborative transaction + numCollaborators: undefined, + feeConfigValues, + showPrivacyInfo: false, }} > - {t('earn.fidelity_bond.confirm_modal.body', { - date: new Date(fb.lockdate.toTimestamp(lockDate)).toUTCString(), - humanReadableDuration: fb.time.humanReadableDuration({ - to: fb.lockdate.toTimestamp(lockDate), - locale: i18n.resolvedLanguage || i18n.language, - }), - })} - + + )} +
setIsExpanded(!isExpanded)}>
diff --git a/src/components/fb/FidelityBondSteps.tsx b/src/components/fb/FidelityBondSteps.tsx index bc6f3fcc..de41423b 100644 --- a/src/components/fb/FidelityBondSteps.tsx +++ b/src/components/fb/FidelityBondSteps.tsx @@ -60,7 +60,7 @@ interface ReviewInputsProps { jar: JarIndex utxos: Array selectedUtxos: Array - timelockedAddress: string + timelockedAddress: Api.BitcoinAddress } interface CreatedFidelityBondProps { diff --git a/src/components/fb/SpendFidelityBondModal.tsx b/src/components/fb/SpendFidelityBondModal.tsx index 874d44de..745fca24 100644 --- a/src/components/fb/SpendFidelityBondModal.tsx +++ b/src/components/fb/SpendFidelityBondModal.tsx @@ -15,6 +15,7 @@ import { useFeeConfigValues } from '../../hooks/Fees' import styles from './SpendFidelityBondModal.module.css' import { isDebugFeatureEnabled } from '../../constants/debugFeatures' import { CopyButton } from '../CopyButton' +import { LockInfoAlert } from './CreateFidelityBond' type Input = { outpoint: Api.UtxoId @@ -501,9 +502,9 @@ const RenewFidelityBondModal = ({
- {showConfirmSendModal && fidelityBond && timelockedAddress !== undefined && ( + {lockDate && fidelityBond && timelockedAddress !== undefined && ( { setShowConfirmSendModal(false) @@ -522,7 +523,9 @@ const RenewFidelityBondModal = ({ feeConfigValues, showPrivacyInfo: false, }} - /> + > + + )} ) From 6f24a4f3945a4f617f2dbeea7d475cc6ac5421f6 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Thu, 26 Oct 2023 12:30:30 +0200 Subject: [PATCH 4/8] wip(renew): loading indicator for timelocked address --- src/components/PaymentConfirmModal.tsx | 4 +- src/components/fb/SpendFidelityBondModal.tsx | 114 ++++++++++--------- 2 files changed, 64 insertions(+), 54 deletions(-) diff --git a/src/components/PaymentConfirmModal.tsx b/src/components/PaymentConfirmModal.tsx index 57aaab2d..68c46672 100644 --- a/src/components/PaymentConfirmModal.tsx +++ b/src/components/PaymentConfirmModal.tsx @@ -6,7 +6,7 @@ import Balance from './Balance' import { useSettings } from '../context/SettingsContext' import { FeeValues, TxFee, useEstimatedMaxCollaboratorFee } from '../hooks/Fees' import { ConfirmModal, ConfirmModalProps } from './Modal' -import { AmountSats } from '../libs/JmWalletApi' +import { AmountSats, BitcoinAddress } from '../libs/JmWalletApi' import { jarInitial } from './jars/Jar' import { isValidNumber } from '../utils' import styles from './PaymentConfirmModal.module.css' @@ -57,7 +57,7 @@ const useMiningFeeText = ({ tx_fees, tx_fees_factor }: Pick { if (abortCtrl.signal.aborted) return @@ -305,42 +304,49 @@ const RenewFidelityBondModal = ({ const loadTimeLockedAddress = useCallback( (lockdate: Api.Lockdate, signal: AbortSignal) => { + return Api.getAddressTimelockNew({ + ...wallet, + lockdate, + signal, + }).then((res) => { + return res.ok ? res.json() : Api.Helper.throwError(res, t('earn.fidelity_bond.error_loading_address')) + }) + }, + [wallet, t], + ) + + useEffect( + function loadTimelockedAddressOnLockDateChange() { + if (!lockDate) return + const abortCtrl = new AbortController() + setIsLoadingTimelockAddress(true) setAlert(undefined) - return ( - Api.getAddressTimelockNew({ - ...wallet, - lockdate, - signal, - }) - .then((res) => { - return res.ok ? res.json() : Api.Helper.throwError(res, t('earn.fidelity_bond.error_loading_address')) - }) - // show the loader a little longer to avoid flickering - .then((result) => new Promise((r) => setTimeout(() => r(result), 221))) - .then((data: any) => { - if (signal.aborted) return - setTimelockedAddress(data.address) - setIsLoadingTimelockAddress(false) - }) - .catch((err) => { - if (signal.aborted) return - setAlert({ variant: 'danger', message: err.message }) - setIsLoadingTimelockAddress(false) - }) + const timer = setTimeout( + () => + loadTimeLockedAddress(lockDate, abortCtrl.signal) + .then((data: any) => { + if (abortCtrl.signal.aborted) return + setTimelockedAddress(data.address) + setIsLoadingTimelockAddress(false) + }) + .catch((err) => { + if (abortCtrl.signal.aborted) return + setAlert({ variant: 'danger', message: err.message }) + setIsLoadingTimelockAddress(false) + }), + 250, ) + + return () => { + clearTimeout(timer) + abortCtrl.abort() + } }, - [wallet, t], + [loadTimeLockedAddress, lockDate], ) - useEffect(() => { - if (!lockDate) return - const abortCtrl = new AbortController() - loadTimeLockedAddress(lockDate, abortCtrl.signal) - return () => abortCtrl.abort() - }, [loadTimeLockedAddress, lockDate]) - const primaryButtonContent = useMemo(() => { if (isSending) { return ( @@ -383,28 +389,32 @@ const RenewFidelityBondModal = ({ - {timelockedAddress && ( - <> -
-
- } - successText={} - value={timelockedAddress} - /> -
-
{t('earn.fidelity_bond.review_inputs.label_address')}
-
- {timelockedAddress} -
-
+ + <> +
+
+ } + successText={} + value={timelockedAddress || ''} + /> +
+
{t('earn.fidelity_bond.review_inputs.label_address')}
+ {!isLoading && !isLoadingTimelockedAddress ? ( +
{timelockedAddress}
+ ) : ( + + + + )}
- - )} +
+
) }, [ @@ -505,7 +515,7 @@ const RenewFidelityBondModal = ({ {lockDate && fidelityBond && timelockedAddress !== undefined && ( { setShowConfirmSendModal(false) onClose({ txInfo, mustReload: parentMustReload }) @@ -584,7 +594,7 @@ const SpendFidelityBondModal = ({ // Delaying the poll requests gives the wallet some time to synchronize // the utxo set and reduces amount of http requests - const initialDelayInMs = 250 + const initialDelayInMs = 1_000 const timer = setTimeout(() => { if (abortCtrl.signal.aborted) return From 5aca376bb8e64664ac5d0ccd8d1ce2e622910a05 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Thu, 26 Oct 2023 14:21:03 +0200 Subject: [PATCH 5/8] refactor: add hook WaitForUtxosToBeSpent --- src/components/Send/index.tsx | 68 +++-------- src/components/fb/SpendFidelityBondModal.tsx | 117 +++++-------------- src/hooks/WaitForUtxosToBeSpent.ts | 64 ++++++++++ 3 files changed, 112 insertions(+), 137 deletions(-) create mode 100644 src/hooks/WaitForUtxosToBeSpent.ts diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 553b14b0..3fa33fd5 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -6,18 +6,20 @@ import * as rb from 'react-bootstrap' import * as Api from '../../libs/JmWalletApi' import PageTitle from '../PageTitle' import Sprite from '../Sprite' +import { SendForm, SendFormValues } from './SendForm' import { ConfirmModal } from '../Modal' +import { scrollToTop } from '../../utils' import { PaymentConfirmModal } from '../PaymentConfirmModal' import FeeConfigModal, { FeeConfigSectionKey } from '../settings/FeeConfigModal' import { FeeValues, TxFee, useFeeConfigValues } from '../../hooks/Fees' import { useReloadCurrentWalletInfo, useCurrentWalletInfo, CurrentWallet } from '../../context/WalletContext' import { useServiceInfo, useReloadServiceInfo } from '../../context/ServiceInfoContext' import { useLoadConfigValue } from '../../context/ServiceConfigContext' +import { useWaitForUtxosToBeSpent } from '../../hooks/WaitForUtxosToBeSpent' import { routes } from '../../constants/routes' import { JM_MINIMUM_MAKERS_DEFAULT } from '../../constants/config' -import { scrollToTop } from '../../utils' + import { initialNumCollaborators } from './helpers' -import { SendForm, SendFormValues } from './SendForm' const INITIAL_DESTINATION = null const INITIAL_SOURCE_JAR_INDEX = null @@ -105,7 +107,7 @@ export default function Send({ wallet }: SendProps) { [feeConfigValues], ) - const [waitForUtxosToBeSpent, setWaitForUtxosToBeSpent] = useState([]) + const [waitForUtxosToBeSpent, setWaitForUtxosToBeSpent] = useState([]) const [paymentSuccessfulInfoAlert, setPaymentSuccessfulInfoAlert] = useState() const isOperationDisabled = useMemo( @@ -160,54 +162,22 @@ export default function Send({ wallet }: SendProps) { [wallet, setAlert, t], ) - // This callback is responsible for updating `waitForUtxosToBeSpent` while - // the wallet is synchronizing. The wallet needs some time after a tx is sent - // to reflect the changes internally. In order to show the actual balance, - // all outputs in `waitForUtxosToBeSpent` must have been removed from the - // wallet's utxo set. - useEffect( - function updateWaitForUtxosToBeSpentHook() { - if (waitForUtxosToBeSpent.length === 0) return - - const abortCtrl = new AbortController() - - // Delaying the poll requests gives the wallet some time to synchronize - // the utxo set and reduces amount of http requests - const initialDelayInMs = 250 - const timer = setTimeout(() => { - if (abortCtrl.signal.aborted) return - - reloadCurrentWalletInfo - .reloadUtxos({ signal: abortCtrl.signal }) - .then((res) => { - if (abortCtrl.signal.aborted) return - const outputs = res.utxos.map((it) => it.utxo) - const utxosStillPresent = waitForUtxosToBeSpent.filter((it) => outputs.includes(it)) - setWaitForUtxosToBeSpent([...utxosStillPresent]) - }) - - .catch((err) => { - if (abortCtrl.signal.aborted) return - - // Stop waiting for wallet synchronization on errors, but inform - // the user that loading the wallet info failed - setWaitForUtxosToBeSpent([]) - - const message = t('global.errors.error_reloading_wallet_failed', { - reason: err.message || t('global.errors.reason_unknown'), - }) - setAlert({ variant: 'danger', message }) - }) - }, initialDelayInMs) - - return () => { - abortCtrl.abort() - clearTimeout(timer) - } - }, - [waitForUtxosToBeSpent, reloadCurrentWalletInfo, t], + const waitForUtxosToBeSpentContext = useMemo( + () => ({ + waitForUtxosToBeSpent, + setWaitForUtxosToBeSpent, + onError: (error: any) => { + const message = t('global.errors.error_reloading_wallet_failed', { + reason: error.message || t('global.errors.reason_unknown'), + }) + setAlert({ variant: 'danger', message }) + }, + }), + [waitForUtxosToBeSpent, t], ) + useWaitForUtxosToBeSpent(waitForUtxosToBeSpentContext) + useEffect( function initialize() { if (isOperationDisabled) { diff --git a/src/components/fb/SpendFidelityBondModal.tsx b/src/components/fb/SpendFidelityBondModal.tsx index edc3c3d4..b3ad30ad 100644 --- a/src/components/fb/SpendFidelityBondModal.tsx +++ b/src/components/fb/SpendFidelityBondModal.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import * as rb from 'react-bootstrap' import { TFunction } from 'i18next' import { useTranslation } from 'react-i18next' -import { CurrentWallet, useReloadCurrentWalletInfo, Utxo, Utxos, WalletInfo } from '../../context/WalletContext' +import { CurrentWallet, Utxo, Utxos, WalletInfo } from '../../context/WalletContext' import * as Api from '../../libs/JmWalletApi' import * as fb from './utils' import Alert from '../Alert' @@ -14,6 +14,7 @@ import { useFeeConfigValues } from '../../hooks/Fees' import { isDebugFeatureEnabled } from '../../constants/debugFeatures' import { CopyButton } from '../CopyButton' import { LockInfoAlert } from './CreateFidelityBond' +import { useWaitForUtxosToBeSpent } from '../../hooks/WaitForUtxosToBeSpent' import styles from './SpendFidelityBondModal.module.css' type Input = { @@ -226,7 +227,6 @@ const RenewFidelityBondModal = ({ ...modalProps }: RenewFidelityBondModalProps) => { const { t } = useTranslation() - const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() const feeConfigValues = useFeeConfigValues()[0] const [alert, setAlert] = useState() @@ -250,50 +250,21 @@ const RenewFidelityBondModal = ({ return walletInfo.data.utxos.utxos.find((utxo) => utxo.utxo === fidelityBondId) }, [walletInfo, fidelityBondId]) - // This callback is responsible for updating the loading state when the - // the payment is made. The wallet needs some time after a tx is sent - // to reflect the changes internally. All outputs in - // `waitForUtxosToBeSpent` must have been removed from the wallet - // for the payment to be considered done. - useEffect(() => { - if (waitForUtxosToBeSpent.length === 0) return - - const abortCtrl = new AbortController() - - // Delaying the poll requests gives the wallet some time to synchronize - // the utxo set and reduces amount of http requests - const initialDelayInMs = 1_000 - const timer = setTimeout(() => { - if (abortCtrl.signal.aborted) return - - reloadCurrentWalletInfo - .reloadUtxos({ signal: abortCtrl.signal }) - .then((res) => { - if (abortCtrl.signal.aborted) return - - const outputs = res.utxos.map((it) => it.utxo) - const utxosStillPresent = waitForUtxosToBeSpent.filter((it) => outputs.includes(it)) - setWaitForUtxosToBeSpent([...utxosStillPresent]) + const waitForUtxosToBeSpentContext = useMemo( + () => ({ + waitForUtxosToBeSpent, + setWaitForUtxosToBeSpent, + onError: (error: any) => { + const message = t('global.errors.error_reloading_wallet_failed', { + reason: error.message || t('global.errors.reason_unknown'), }) - .catch((err) => { - if (abortCtrl.signal.aborted) return - - // Stop waiting for wallet synchronization on errors, but inform - // the user that loading the wallet info failed - setWaitForUtxosToBeSpent([]) - - const message = t('global.errors.error_reloading_wallet_failed', { - reason: err.message || t('global.errors.reason_unknown'), - }) - setAlert({ variant: 'danger', message }) - }) - }, initialDelayInMs) + setAlert({ variant: 'danger', message }) + }, + }), + [waitForUtxosToBeSpent, t], + ) - return () => { - abortCtrl.abort() - clearTimeout(timer) - } - }, [waitForUtxosToBeSpent, reloadCurrentWalletInfo, t]) + useWaitForUtxosToBeSpent(waitForUtxosToBeSpentContext) const yearsRange = useMemo(() => { if (isDebugFeatureEnabled('allowCreatingExpiredFidelityBond')) { @@ -361,7 +332,7 @@ const RenewFidelityBondModal = ({ return <>{t('earn.fidelity_bond.renew.text_button_submit')} }, [isSending, txInfo, t]) - const onSelectedDateChanged = useCallback((date) => { + const onSelectedDateChanged = useCallback((date: Api.Lockdate | null) => { setTimelockedAddress(undefined) setLockDate(date ?? undefined) }, []) @@ -558,7 +529,6 @@ const SpendFidelityBondModal = ({ ...modalProps }: SpendFidelityBondModalProps) => { const { t } = useTranslation() - const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() const feeConfigValues = useFeeConfigValues()[0] const [alert, setAlert] = useState() @@ -582,50 +552,21 @@ const SpendFidelityBondModal = ({ return walletInfo.data.utxos.utxos.find((utxo) => utxo.utxo === fidelityBondId) }, [walletInfo, fidelityBondId]) - // This callback is responsible for updating the loading state when the - // the payment is made. The wallet needs some time after a tx is sent - // to reflect the changes internally. All outputs in - // `waitForUtxosToBeSpent` must have been removed from the wallet - // for the payment to be considered done. - useEffect(() => { - if (waitForUtxosToBeSpent.length === 0) return - - const abortCtrl = new AbortController() - - // Delaying the poll requests gives the wallet some time to synchronize - // the utxo set and reduces amount of http requests - const initialDelayInMs = 1_000 - const timer = setTimeout(() => { - if (abortCtrl.signal.aborted) return - - reloadCurrentWalletInfo - .reloadUtxos({ signal: abortCtrl.signal }) - .then((res) => { - if (abortCtrl.signal.aborted) return - - const outputs = res.utxos.map((it) => it.utxo) - const utxosStillPresent = waitForUtxosToBeSpent.filter((it) => outputs.includes(it)) - setWaitForUtxosToBeSpent([...utxosStillPresent]) + const waitForUtxosToBeSpentContext = useMemo( + () => ({ + waitForUtxosToBeSpent, + setWaitForUtxosToBeSpent, + onError: (error: any) => { + const message = t('global.errors.error_reloading_wallet_failed', { + reason: error.message || t('global.errors.reason_unknown'), }) - .catch((err) => { - if (abortCtrl.signal.aborted) return - - // Stop waiting for wallet synchronization on errors, but inform - // the user that loading the wallet info failed - setWaitForUtxosToBeSpent([]) - - const message = t('global.errors.error_reloading_wallet_failed', { - reason: err.message || t('global.errors.reason_unknown'), - }) - setAlert({ variant: 'danger', message }) - }) - }, initialDelayInMs) + setAlert({ variant: 'danger', message }) + }, + }), + [waitForUtxosToBeSpent, t], + ) - return () => { - abortCtrl.abort() - clearTimeout(timer) - } - }, [waitForUtxosToBeSpent, reloadCurrentWalletInfo, t]) + useWaitForUtxosToBeSpent(waitForUtxosToBeSpentContext) const onPrimaryButtonClicked = () => { if (isLoading) return diff --git a/src/hooks/WaitForUtxosToBeSpent.ts b/src/hooks/WaitForUtxosToBeSpent.ts new file mode 100644 index 00000000..b54da5cd --- /dev/null +++ b/src/hooks/WaitForUtxosToBeSpent.ts @@ -0,0 +1,64 @@ +import { useEffect } from 'react' +import { useReloadCurrentWalletInfo } from '../context/WalletContext' +import { UtxoId } from '../libs/JmWalletApi' + +// Delaying the poll requests gives the wallet some time to synchronize +// the utxo set and reduces amount of http requests +const DEFAUL_DELAY: Milliseconds = 1_000 + +interface WaitForUtxosToBeSpentArgs { + waitForUtxosToBeSpent: UtxoId[] + setWaitForUtxosToBeSpent: (utxos: UtxoId[]) => void + onError: (error: any) => void + delay?: Milliseconds + resetOnErrors?: boolean +} + +// This callback is responsible for updating the utxo array when a +// payment is made. The wallet needs some time after a tx is sent +// to reflect the changes internally. All outputs given in +// `waitForUtxosToBeSpent` must have been removed from the wallet +// for a payment to be considered done. +export const useWaitForUtxosToBeSpent = ({ + waitForUtxosToBeSpent, + setWaitForUtxosToBeSpent, + onError, + delay = DEFAUL_DELAY, + resetOnErrors = true, +}: WaitForUtxosToBeSpentArgs): void => { + const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() + return useEffect(() => { + if (waitForUtxosToBeSpent.length === 0) return + + const abortCtrl = new AbortController() + + const timer = setTimeout(() => { + if (abortCtrl.signal.aborted) return + + reloadCurrentWalletInfo + .reloadUtxos({ signal: abortCtrl.signal }) + .then((res) => { + if (abortCtrl.signal.aborted) return + + const outputs = res.utxos.map((it) => it.utxo) + const utxosStillPresent = waitForUtxosToBeSpent.filter((it) => outputs.includes(it)) + + // updating the utxos array will trigger a recursive call + setWaitForUtxosToBeSpent([...utxosStillPresent]) + }) + .catch((error: any) => { + if (abortCtrl.signal.aborted) return + if (resetOnErrors) { + // Stop waiting for wallet synchronization on errors + setWaitForUtxosToBeSpent([]) + } + onError(error) + }) + }, delay) + + return () => { + abortCtrl.abort() + clearTimeout(timer) + } + }, [waitForUtxosToBeSpent, setWaitForUtxosToBeSpent, resetOnErrors, onError, delay, reloadCurrentWalletInfo]) +} From 3885c5f147a9dbbf2b5b8d8344d5f8050ba806e7 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Thu, 26 Oct 2023 15:34:54 +0200 Subject: [PATCH 6/8] wip(renew): distinct error alert when failing to load timelocked address --- src/components/fb/SpendFidelityBondModal.tsx | 71 +++++++++++++------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/src/components/fb/SpendFidelityBondModal.tsx b/src/components/fb/SpendFidelityBondModal.tsx index b3ad30ad..6194939a 100644 --- a/src/components/fb/SpendFidelityBondModal.tsx +++ b/src/components/fb/SpendFidelityBondModal.tsx @@ -67,10 +67,15 @@ const spendUtxosWithDirectSend = async ( request: UtxoDirectSendRequest, hooks: UtxoDirectSendHook, ) => { + if (request.utxos.length === 0) { + // this is a programming error (no translation needed) + throw new Error('Precondition failed: No UTXO(s) provided.') + } + const utxosFromSameJar = request.utxos.every((it) => it.mixdepth === request.sourceJarIndex) - if (!utxosFromSameJar || request.utxos.length === 0) { + if (!utxosFromSameJar) { // this is a programming error (no translation needed) - throw new Error('Precondition failed: UTXOs must be from the same jar') + throw new Error('Precondition failed: UTXOs must be from the same jar.') } const spendableUtxoIds = request.utxos.map((it) => it.utxo) @@ -84,6 +89,10 @@ const spendUtxosWithDirectSend = async ( const utxosToSpend = utxosFromSourceJar.filter((it) => spendableUtxoIds.includes(it.utxo)) + if (spendableUtxoIds.length !== utxosToSpend.length) { + throw new Error('Precondition failed: Specified UTXO(s) cannot be used for this payment.') + } + const utxosToFreeze = utxosFromSourceJar .filter((it) => !it.frozen) .filter((it) => !spendableUtxoIds.includes(it.utxo)) @@ -236,10 +245,12 @@ const RenewFidelityBondModal = ({ const [lockDate, setLockDate] = useState() const [timelockedAddress, setTimelockedAddress] = useState() + const [isLoadingTimelockedAddress, setIsLoadingTimelockAddress] = useState(false) + const [timelockedAddressAlert, setTimelockedAddressAlert] = useState() const [parentMustReload, setParentMustReload] = useState(false) const [isSending, setIsSending] = useState(false) - const [isLoadingTimelockedAddress, setIsLoadingTimelockAddress] = useState(false) + const isLoading = useMemo(() => isSending || waitForUtxosToBeSpent.length > 0, [isSending, waitForUtxosToBeSpent]) const [showConfirmSendModal, setShowConfirmSendModal] = useState(false) @@ -292,7 +303,7 @@ const RenewFidelityBondModal = ({ const abortCtrl = new AbortController() setIsLoadingTimelockAddress(true) - setAlert(undefined) + setTimelockedAddressAlert(undefined) const timer = setTimeout( () => @@ -304,8 +315,9 @@ const RenewFidelityBondModal = ({ }) .catch((err) => { if (abortCtrl.signal.aborted) return - setAlert({ variant: 'danger', message: err.message }) setIsLoadingTimelockAddress(false) + setTimelockedAddress(undefined) + setTimelockedAddressAlert({ variant: 'danger', message: err.message }) }), 250, ) @@ -364,28 +376,36 @@ const RenewFidelityBondModal = ({ onChange={onSelectedDateChanged} /> - <> -
-
- } - successText={} - value={timelockedAddress || ''} +
+
+ {timelockedAddressAlert ? ( + setTimelockedAddressAlert(undefined)} /> -
-
{t('earn.fidelity_bond.review_inputs.label_address')}
- {!isLoading && !isLoadingTimelockedAddress ? ( -
{timelockedAddress}
- ) : ( - - - - )} -
-
+ ) : ( + <> + } + successText={} + value={timelockedAddress || ''} + /> +
+
{t('earn.fidelity_bond.review_inputs.label_address')}
+ {!isLoading && !isLoadingTimelockedAddress ? ( +
{timelockedAddress || '...'}
+ ) : ( + + + + )} +
+ + )}
- +
) }, [ @@ -394,6 +414,7 @@ const RenewFidelityBondModal = ({ yearsRange, timelockedAddress, isLoadingTimelockedAddress, + timelockedAddressAlert, txInfo, onSelectedDateChanged, t, From f2027251b52b5e3fea9315c5068717d3096eb602 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Sun, 21 Jan 2024 16:13:10 +0100 Subject: [PATCH 7/8] ui: hide cancel button on successful renewal --- src/components/fb/CreateFidelityBond.tsx | 1 + src/components/fb/SpendFidelityBondModal.tsx | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/fb/CreateFidelityBond.tsx b/src/components/fb/CreateFidelityBond.tsx index f0c72a85..0a299a88 100644 --- a/src/components/fb/CreateFidelityBond.tsx +++ b/src/components/fb/CreateFidelityBond.tsx @@ -597,6 +597,7 @@ const CreateFidelityBond = ({ otherFidelityBondExists, wallet, walletInfo, onDon {lockDate && timelockedAddress && selectedJar !== undefined && ( setShowConfirmInputsModal(false)} onConfirm={() => { diff --git a/src/components/fb/SpendFidelityBondModal.tsx b/src/components/fb/SpendFidelityBondModal.tsx index 6194939a..47b8e15a 100644 --- a/src/components/fb/SpendFidelityBondModal.tsx +++ b/src/components/fb/SpendFidelityBondModal.tsx @@ -484,14 +484,16 @@ const RenewFidelityBondModal = ({
- onClose({ txInfo, mustReload: parentMustReload })} - className="flex-1 d-flex justify-content-center align-items-center" - > - {t('global.cancel')} - + {!txInfo && ( + onClose({ txInfo, mustReload: parentMustReload })} + className="flex-1 d-flex justify-content-center align-items-center" + > + {t('global.cancel')} + + )} {lockDate && fidelityBond && timelockedAddress !== undefined && ( { From 96d9c53859cb6d2c358b7bcca766f06896c28309 Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Sat, 27 Jan 2024 15:10:22 +0100 Subject: [PATCH 8/8] ui(fb-renew): handle modal size on small screens --- src/components/fb/SpendFidelityBondModal.tsx | 2 +- src/index.css | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/fb/SpendFidelityBondModal.tsx b/src/components/fb/SpendFidelityBondModal.tsx index 47b8e15a..d9f92681 100644 --- a/src/components/fb/SpendFidelityBondModal.tsx +++ b/src/components/fb/SpendFidelityBondModal.tsx @@ -392,7 +392,7 @@ const RenewFidelityBondModal = ({ successText={} value={timelockedAddress || ''} /> -
+
{t('earn.fidelity_bond.review_inputs.label_address')}
{!isLoading && !isLoadingTimelockedAddress ? (
{timelockedAddress || '...'}
diff --git a/src/index.css b/src/index.css index c7c16525..748ffb2a 100644 --- a/src/index.css +++ b/src/index.css @@ -532,6 +532,10 @@ h2 { color: inherit; } +.modal { + --bs-modal-width: 800px; +} + .modal-header { background-color: var(--bs-gray-800); color: var(--bs-white);