Skip to content

Commit

Permalink
feature: Modal loading while staking (#147)
Browse files Browse the repository at this point in the history
* modal loading while staking

* onClose={handlePreviewModalClose}

* loading state unbonding withdrawing

* isAwaitingWalletResponse

* remove x, change text to Awaiting wallet signature and broadcast

* general modal not wallet specific

* awaitingWalletResponse
  • Loading branch information
gbarkhatov authored Sep 16, 2024
1 parent 5baf8d4 commit 76c60ca
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 49 deletions.
8 changes: 8 additions & 0 deletions src/app/components/Delegations/Delegations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ const DelegationsContent: React.FC<DelegationsProps> = ({
const [modalMode, setModalMode] = useState<MODE>();
const { showError } = useError();
const { isApiNormal, isGeoBlocked } = useHealthCheck();
const [awaitingWalletResponse, setAwaitingWalletResponse] = useState(false);

// Local storage state for intermediate delegations (withdrawing, unbonding)
const intermediateDelegationsLocalStorageKey =
Expand Down Expand Up @@ -149,6 +150,8 @@ const DelegationsContent: React.FC<DelegationsProps> = ({
// It constructs an unbonding transaction, creates a signature for it, and submits both to the back-end API
const handleUnbond = async (id: string) => {
try {
// Prevent the modal from closing
setAwaitingWalletResponse(true);
// Sign the unbonding transaction
const { delegation } = await signUnbondingTx(
id,
Expand All @@ -171,13 +174,16 @@ const DelegationsContent: React.FC<DelegationsProps> = ({
setModalOpen(false);
setTxID("");
setModalMode(undefined);
setAwaitingWalletResponse(false);
}
};

// Handles withdrawing requests for delegations that have expired timelocks
// It constructs a withdrawal transaction, creates a signature for it, and submits it to the Bitcoin network
const handleWithdraw = async (id: string) => {
try {
// Prevent the modal from closing
setAwaitingWalletResponse(true);
// Sign the withdrawal transaction
const { delegation } = await signWithdrawalTx(
id,
Expand All @@ -203,6 +209,7 @@ const DelegationsContent: React.FC<DelegationsProps> = ({
setModalOpen(false);
setTxID("");
setModalMode(undefined);
setAwaitingWalletResponse(false);
}
};

Expand Down Expand Up @@ -343,6 +350,7 @@ const DelegationsContent: React.FC<DelegationsProps> = ({
: handleWithdraw(txID);
}}
mode={modalMode}
awaitingWalletResponse={awaitingWalletResponse}
/>
)}
</div>
Expand Down
14 changes: 11 additions & 3 deletions src/app/components/Loading/Loading.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { twJoin } from "tailwind-merge";

interface LoadingProps {
text?: string;
noBorder?: boolean;
}

export const LoadingView: React.FC<LoadingProps> = () => {
export const LoadingView: React.FC<LoadingProps> = ({ text, noBorder }) => {
return (
<div className="flex flex-1 flex-col items-center justify-center gap-4 rounded-2xl border border-neutral-content py-4 dark:border-neutral-content/20">
<div
className={twJoin(
"flex flex-1 flex-col items-center justify-center gap-4 rounded-2xl border border-neutral-content py-4 dark:border-neutral-content/20",
noBorder && "border-0",
)}
>
<span className="loading loading-spinner loading-lg text-primary" />
<p>Please wait...</p>
<p>{text || "Please wait..."}</p>
</div>
);
};
Expand Down
6 changes: 6 additions & 0 deletions src/app/components/Modals/GeneralModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ interface GeneralModalProps {
small?: boolean;
children: ReactNode;
className?: string;
closeOnOverlayClick?: boolean;
closeOnEsc?: boolean;
}

export const GeneralModal: React.FC<GeneralModalProps> = ({
Expand All @@ -16,6 +18,8 @@ export const GeneralModal: React.FC<GeneralModalProps> = ({
children,
small,
className = "",
closeOnOverlayClick,
closeOnEsc = true,
}) => {
const modalRef = useRef(null);

Expand Down Expand Up @@ -46,6 +50,8 @@ export const GeneralModal: React.FC<GeneralModalProps> = ({
}}
showCloseIcon={false}
blockScroll={false}
closeOnEsc={closeOnEsc}
closeOnOverlayClick={closeOnOverlayClick}
>
{children}
</Modal>
Expand Down
58 changes: 38 additions & 20 deletions src/app/components/Modals/PreviewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { blocksToDisplayTime } from "@/utils/blocksToDisplayTime";
import { satoshiToBtc } from "@/utils/btcConversions";
import { maxDecimals } from "@/utils/maxDecimals";

import { LoadingView } from "../Loading/Loading";

import { GeneralModal } from "./GeneralModal";

interface PreviewModalProps {
Expand All @@ -19,6 +21,7 @@ interface PreviewModalProps {
unbondingTimeBlocks: number;
confirmationDepth: number;
unbondingFeeSat: number;
awaitingWalletResponse: boolean;
}

export const PreviewModal: React.FC<PreviewModalProps> = ({
Expand All @@ -33,22 +36,30 @@ export const PreviewModal: React.FC<PreviewModalProps> = ({
feeRate,
confirmationDepth,
unbondingFeeSat,
awaitingWalletResponse,
}) => {
const cardStyles =
"card border bg-base-300 p-4 text-sm dark:border-0 dark:bg-base-200";

const { coinName } = getNetworkConfig();

return (
<GeneralModal open={open} onClose={onClose}>
<GeneralModal
open={open}
onClose={onClose}
closeOnOverlayClick={!awaitingWalletResponse}
closeOnEsc={false}
>
<div className="mb-4 flex items-center justify-between">
<h3 className="font-bold">Preview</h3>
<button
className="btn btn-circle btn-ghost btn-sm"
onClick={() => onClose(false)}
>
<IoMdClose size={24} />
</button>
{!awaitingWalletResponse && (
<button
className="btn btn-circle btn-ghost btn-sm"
onClick={() => onClose(false)}
>
<IoMdClose size={24} />
</button>
)}
</div>
<div className="flex flex-col gap-4 text-sm">
<div className="flex flex-col gap-4 md:flex-row">
Expand Down Expand Up @@ -112,19 +123,26 @@ export const PreviewModal: React.FC<PreviewModalProps> = ({
&quot;Pending&quot; stake is only accessible through the device it was
created.
</p>
<div className="flex gap-4">
<button
className="btn btn-outline flex-1"
onClick={() => {
onClose(false);
}}
>
Cancel
</button>
<button className="btn-primary btn flex-1" onClick={onSign}>
Stake
</button>
</div>
{awaitingWalletResponse ? (
<LoadingView
text="Awaiting wallet signature and broadcast"
noBorder
/>
) : (
<div className="flex gap-4">
<button
className="btn btn-outline flex-1"
onClick={() => {
onClose(false);
}}
>
Cancel
</button>
<button className="btn-primary btn flex-1" onClick={onSign}>
Stake
</button>
</div>
)}
</div>
</GeneralModal>
);
Expand Down
65 changes: 39 additions & 26 deletions src/app/components/Modals/UnbondWithdrawModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { blocksToDisplayTime } from "@/utils/blocksToDisplayTime";
import { satoshiToBtc } from "@/utils/btcConversions";
import { maxDecimals } from "@/utils/maxDecimals";

import { LoadingView } from "../Loading/Loading";

import { GeneralModal } from "./GeneralModal";

export const MODE_UNBOND = "unbond";
Expand All @@ -18,6 +20,7 @@ interface PreviewModalProps {
onClose: (value: boolean) => void;
onProceed: () => void;
mode: MODE;
awaitingWalletResponse: boolean;
}

export const UnbondWithdrawModal: React.FC<PreviewModalProps> = ({
Expand All @@ -27,6 +30,7 @@ export const UnbondWithdrawModal: React.FC<PreviewModalProps> = ({
onClose,
onProceed,
mode,
awaitingWalletResponse,
}) => {
const { coinName, networkName } = getNetworkConfig();

Expand Down Expand Up @@ -59,37 +63,46 @@ export const UnbondWithdrawModal: React.FC<PreviewModalProps> = ({
const content = mode === MODE_UNBOND ? unbondContent : withdrawContent;

return (
<GeneralModal open={open} onClose={onClose} small>
<GeneralModal
open={open}
onClose={onClose}
closeOnEsc={false}
closeOnOverlayClick={!awaitingWalletResponse}
small
>
<div className="mb-4 flex items-center justify-between">
<h3 className="font-bold">{title}</h3>
<button
className="btn btn-circle btn-ghost btn-sm"
onClick={() => onClose(false)}
>
<IoMdClose size={24} />
</button>
</div>
<div className="flex flex-col gap-4">
<p className="text-left dark:text-neutral-content">{content}</p>
<div className="flex gap-4">
<button
className="btn btn-outline flex-1"
onClick={() => {
onClose(false);
}}
>
Cancel
</button>
{!awaitingWalletResponse && (
<button
className="btn-primary btn flex-1"
onClick={() => {
onClose(false);
onProceed();
}}
className="btn btn-circle btn-ghost btn-sm"
onClick={() => onClose(false)}
>
Proceed
<IoMdClose size={24} />
</button>
</div>
)}
</div>
<div className="flex flex-col gap-4">
<p className="text-left dark:text-neutral-content">{content}</p>
{awaitingWalletResponse ? (
<LoadingView
text="Awaiting wallet signature and broadcast"
noBorder
/>
) : (
<div className="flex gap-4">
<button
className="btn btn-outline flex-1"
onClick={() => {
onClose(false);
}}
>
Cancel
</button>
<button className="btn-primary btn flex-1" onClick={onProceed}>
Proceed
</button>
</div>
)}
</div>
</GeneralModal>
);
Expand Down
7 changes: 7 additions & 0 deletions src/app/components/Staking/Staking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const Staking: React.FC<StakingProps> = ({
useState<FinalityProvider[]>();
// Selected fee rate, comes from the user input
const [selectedFeeRate, setSelectedFeeRate] = useState(0);
const [awaitingWalletResponse, setAwaitingWalletResponse] = useState(false);
const [previewModalOpen, setPreviewModalOpen] = useState(false);
const [resetFormInputs, setResetFormInputs] = useState(false);
const [feedbackModal, setFeedbackModal] = useState<{
Expand Down Expand Up @@ -226,6 +227,7 @@ export const Staking: React.FC<StakingProps> = ({
]);

const handleResetState = () => {
setAwaitingWalletResponse(false);
setFinalityProvider(undefined);
setStakingAmountSat(0);
setStakingTimeBlocks(0);
Expand All @@ -243,6 +245,8 @@ export const Staking: React.FC<StakingProps> = ({

const handleSign = async () => {
try {
// Prevent the modal from closing
setAwaitingWalletResponse(true);
// Initial validation
if (!btcWallet) throw new Error("Wallet is not connected");
if (!address) throw new Error("Address is not set");
Expand Down Expand Up @@ -292,6 +296,8 @@ export const Staking: React.FC<StakingProps> = ({
queryClient.invalidateQueries({ queryKey: [UTXO_KEY, address] });
},
});
} finally {
setAwaitingWalletResponse(false);
}
};

Expand Down Expand Up @@ -659,6 +665,7 @@ export const Staking: React.FC<StakingProps> = ({
feeRate={feeRate}
unbondingTimeBlocks={unbondingTime}
unbondingFeeSat={unbondingFeeSat}
awaitingWalletResponse={awaitingWalletResponse}
/>
)}
</div>
Expand Down

0 comments on commit 76c60ca

Please sign in to comment.