Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 20 additions & 44 deletions src/app/farm/[poolId]/PoolDetailClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useEffect, useMemo, useRef, useState } from "react";
import NextLink from "next/link";
import TransactionConfirmModal from "@/components/TransactionConfirmModal/TransactionConfirmModal";
import {
Alert,
AlertIcon,
Expand Down Expand Up @@ -209,6 +210,11 @@ export default function PoolDetailClient({ poolId }: { poolId: string }) {
const displayAmount = parseFloat(rawAmount);
const amountValid = Number.isFinite(displayAmount) && displayAmount > 0;
const isPending = isDepositPending(flow.step);
const lockPeriodLabel = pool
? pool.minLockPeriod >= 86400
? `${Math.floor(pool.minLockPeriod / 86400)} day${Math.floor(pool.minLockPeriod / 86400) > 1 ? "s" : ""}`
: `${Math.floor(pool.minLockPeriod / 3600)} hour${Math.floor(pool.minLockPeriod / 3600) > 1 ? "s" : ""}`
: "";

if (error) {
return (
Expand Down Expand Up @@ -507,50 +513,6 @@ export default function PoolDetailClient({ poolId }: { poolId: string }) {
)}
</Button>

{showConfirmation && (
<Alert status="warning" borderRadius="2xl" bg="app.warningBg" color="app.warningFg" fontSize="sm" p={4}>
<Flex direction="column" gap={3} w="full">
<Flex align="flex-start" gap={2}>
<AlertIcon color="app.warningFg" mt={0.5} />
<Box flex="1">
<Text fontWeight="semibold" mb={1}>
Confirm Lock Transaction
</Text>
<Text fontSize="xs">
You are about to lock {displayAmount} {pool?.asset.code ?? ""} for a minimum period of{" "}
{pool
? pool.minLockPeriod >= 86400
? `${Math.floor(pool.minLockPeriod / 86400)} day${Math.floor(pool.minLockPeriod / 86400) > 1 ? "s" : ""}`
: `${Math.floor(pool.minLockPeriod / 3600)} hour${Math.floor(pool.minLockPeriod / 3600) > 1 ? "s" : ""}`
: ""}
. This action cannot be reversed until the lock period expires.
</Text>
</Box>
</Flex>
<Flex gap={2} justify="flex-end">
<Button
size="sm"
variant="ghost"
onClick={handleCancelLock}
isDisabled={isPending}
>
Cancel
</Button>
<Button
size="sm"
bg="app.accent"
color="app.onAccent"
_hover={{ opacity: 0.9 }}
onClick={handleConfirmLock}
isDisabled={isPending}
>
Confirm Lock
</Button>
</Flex>
</Flex>
</Alert>
)}

{flow.step === "error" && (
<Button variant="ghost" size="sm" color="app.muted" onClick={flow.reset}>
Try again
Expand All @@ -559,6 +521,20 @@ export default function PoolDetailClient({ poolId }: { poolId: string }) {
</Flex>
</ModalBody>
</ModalContent>
<TransactionConfirmModal
isOpen={showConfirmation}
onClose={handleCancelLock}
onConfirm={handleConfirmLock}
title="Confirm Lock"
description="Review the details below. You'll be asked to sign this transaction in Freighter."
details={[
{ label: "Pool", value: pool?.asset.code ?? poolId },
{ label: "Lock amount", value: `${displayAmount} ${pool?.asset.code ?? ""}` },
{ label: "Minimum lock period", value: lockPeriodLabel },
]}
warning={`This action cannot be reversed until the ${lockPeriodLabel} lock period expires.`}
confirmLabel="Confirm Lock"
/>
</Modal>
</Flex>
);
Expand Down
28 changes: 27 additions & 1 deletion src/app/farm/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ConnectWalletButton from "@/components/ConnectWalletButton/ConnectWalletB
import UnlockModal from "@/components/UnlockModal/UnlockModal";
import BoostModal from "@/components/BoostModal/BoostModal";
import Breadcrumbs from "@/components/Breadcrumbs/Breadcrumbs";
import TransactionConfirmModal from "@/components/TransactionConfirmModal/TransactionConfirmModal";
import { EarningRow, MetricColumn } from "@/app/farm/EarningRow";
import { FarmPoolRow } from "@/app/farm/FarmPoolRow";
import {
Expand Down Expand Up @@ -95,6 +96,7 @@ function DepositModal({
}) {
const { publicKey, walletApi, isConnected, isNetworkMismatch } = useStellarWallet();
const [amount, setAmount] = useState("");
const [isConfirmOpen, setIsConfirmOpen] = useState(false);

const selectedContractAddress = farm?.contractAddress || farm?.id || "";
const balanceQuery = useStellarBalance(publicKey ?? undefined);
Expand Down Expand Up @@ -156,6 +158,7 @@ function DepositModal({
useEffect(() => {
if (isOpen) {
setAmount("");
setIsConfirmOpen(false);
flow.reset();
}
// Reset only when the modal opens or the selected pool changes.
Expand All @@ -178,6 +181,11 @@ function DepositModal({
await flow.execute(numericAmount);
};

const handleConfirmDeposit = () => {
setIsConfirmOpen(false);
void handleSubmit();
};

// Auto-close the modal after a successful deposit (#416).
useEffect(() => {
if (flow.step === "success") {
Expand Down Expand Up @@ -361,7 +369,7 @@ function DepositModal({
color="app.onAccent"
_hover={{ opacity: isPending ? 1 : 0.9 }}
isDisabled={!canSubmit}
onClick={() => void handleSubmit()}
onClick={() => setIsConfirmOpen(true)}
w="full"
>
{isPending ? (
Expand Down Expand Up @@ -389,6 +397,24 @@ function DepositModal({
</Flex>
</ModalBody>
</ModalContent>
<TransactionConfirmModal
isOpen={isConfirmOpen}
onClose={() => setIsConfirmOpen(false)}
onConfirm={handleConfirmDeposit}
title="Confirm Deposit"
description="Review the details below. You'll be asked to sign this transaction in Freighter."
details={[
{ label: "Pool", value: farm.name },
{ label: "Deposit amount", value: `${trimmedAmount} ${farm.symbol}` },
{ label: "Minimum lock period", value: lockPeriod },
{
label: "Estimated Soroban fee",
value: feePreview.data ? `${feePreview.data.feePreview} stroops` : "Unavailable",
},
]}
warning={`Deposited funds stay locked for at least ${lockPeriod} and can't be withdrawn before then.`}
confirmLabel="Confirm Deposit"
/>
</Modal>
);
}
Expand Down
25 changes: 24 additions & 1 deletion src/components/BoostModal/BoostModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
poolContractId,
stellarNetwork,
} from "@/config";
import TransactionConfirmModal from "@/components/TransactionConfirmModal/TransactionConfirmModal";
import { useStellarWallet } from "@/context/StellarWalletContext";
import { useSetBoost } from "@/hooks/useSorobanQuery";
import { trackEvent } from "@/lib/analytics";
Expand Down Expand Up @@ -46,6 +47,7 @@ export default function BoostModal() {
const [allocation, setAllocation] = useState(currentBoost);
const [step, setStep] = useState<BoostStep>("idle");
const [error, setError] = useState<string | null>(null);
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const sliderThumbRef = useRef<HTMLDivElement>(null);

const selectedPoolContractId = position?.contractAddress || poolContractId;
Expand All @@ -55,6 +57,7 @@ export default function BoostModal() {
setAllocation(position.boostAllocation ?? 0);
setStep("idle");
setError(null);
setIsConfirmOpen(false);
}
}, [isBoost, position]);

Expand All @@ -69,6 +72,11 @@ export default function BoostModal() {
const allocationChanged = allocation !== currentBoost;
const isProcessing = step === "submitting";

const handleConfirmBoost = () => {
setIsConfirmOpen(false);
void handleSetBoost();
};

const handleSetBoost = async () => {
if (!publicKey) {
setError("Connect your Freighter wallet to set boost.");
Expand Down Expand Up @@ -263,7 +271,7 @@ export default function BoostModal() {
color="app.onAccent"
_hover={{ opacity: 0.9 }}
isDisabled={!hasStake || !allocationChanged || isProcessing || isNetworkMismatch}
onClick={() => void handleSetBoost()}
onClick={() => setIsConfirmOpen(true)}
w="full"
>
{isProcessing ? (
Expand All @@ -279,6 +287,21 @@ export default function BoostModal() {
)}
</ModalBody>
</ModalContent>
<TransactionConfirmModal
isOpen={isConfirmOpen}
onClose={() => setIsConfirmOpen(false)}
onConfirm={handleConfirmBoost}
title="Confirm Boost Change"
description="Review the details below. You'll be asked to sign this transaction in Freighter."
details={[
{ label: "Pool", value: position.name },
{ label: "Current boost", value: `${currentBoost}%` },
{ label: "New boost", value: `${allocation}%` },
{ label: "Your stake", value: `${position.stake} ${position.symbol}` },
]}
warning="This allocation applies to all future credit emissions until you change it."
confirmLabel="Confirm Boost"
/>
</Modal>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { ChakraProvider } from "@chakra-ui/react";
import { describe, expect, it, vi } from "vitest";
import TransactionConfirmModal, {
type TransactionConfirmModalProps,
} from "./TransactionConfirmModal";

function renderModal(overrides: Partial<TransactionConfirmModalProps> = {}) {
const props: TransactionConfirmModalProps = {
isOpen: true,
onClose: vi.fn(),
onConfirm: vi.fn(),
title: "Confirm Deposit",
details: [
{ label: "Pool", value: "XLM Pool" },
{ label: "Deposit amount", value: "25 XLM" },
],
warning: "Funds stay locked for 7 days.",
confirmLabel: "Confirm Deposit",
...overrides,
};
render(
<ChakraProvider>
<TransactionConfirmModal {...props} />
</ChakraProvider>,
);
return props;
}

describe("TransactionConfirmModal", () => {
it("renders the transaction details and warning", () => {
renderModal();

expect(screen.getByRole("alertdialog")).toBeTruthy();
expect(screen.getByText("XLM Pool")).toBeTruthy();
expect(screen.getByText("25 XLM")).toBeTruthy();
expect(screen.getByText("Funds stay locked for 7 days.")).toBeTruthy();
});

it("calls onConfirm when the confirm button is clicked", () => {
const props = renderModal();

fireEvent.click(screen.getByRole("button", { name: "Confirm Deposit" }));

expect(props.onConfirm).toHaveBeenCalledTimes(1);
expect(props.onClose).not.toHaveBeenCalled();
});

it("calls onClose without confirming when cancelled", () => {
const props = renderModal();

fireEvent.click(screen.getByRole("button", { name: "Cancel" }));

expect(props.onClose).toHaveBeenCalledTimes(1);
expect(props.onConfirm).not.toHaveBeenCalled();
});

it("renders nothing when closed", () => {
renderModal({ isOpen: false });

expect(screen.queryByRole("alertdialog")).toBeNull();
});
});
Loading
Loading