Skip to content
Closed
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
104 changes: 104 additions & 0 deletions components/dashboard/withdraw/withdrawal-confirmation-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"use client";

import { AlertTriangle, Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";

interface WithdrawalConfirmationModalProps {
amount: string | number;
currency: string;
destinationAddress: string;
onConfirm: () => void | Promise<void>;
onCancel: () => void;
isLoading?: boolean;
}

export function WithdrawalConfirmationModal({
amount,
currency,
destinationAddress,
onConfirm,
onCancel,
isLoading = false,
}: WithdrawalConfirmationModalProps) {
const formattedAmount =
typeof amount === "number"
? amount.toLocaleString()
: amount;

return (
<div className="p-6 space-y-6">
<div className="space-y-2 pt-2">
<h2 className="text-xl font-bold text-foreground">Confirm withdrawal</h2>
<p className="text-sm text-muted-foreground">
Review the destination address carefully before you submit.
</p>
</div>

<div className="rounded-xl border border-border bg-muted/30 p-5 space-y-4">
<div className="text-center border-b border-border pb-4">
<p className="text-sm text-muted-foreground mb-1">You are withdrawing</p>
<p className="text-3xl font-bold text-foreground">
{formattedAmount} {currency}
</p>
</div>

<div className="space-y-2">
<p className="text-sm text-muted-foreground">To</p>
<div className="rounded-lg border border-border bg-background/80 p-3">
<p className="text-sm font-mono text-foreground break-all whitespace-pre-wrap">
{destinationAddress.trim() || "No address provided"}
</p>
</div>
</div>
</div>

<div className="rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3">
<div className="flex gap-2">
<AlertTriangle className="size-4 mt-0.5 shrink-0 text-yellow-600 dark:text-yellow-400" />
<div className="space-y-1">
<p className="text-sm font-semibold text-yellow-700 dark:text-yellow-400">
This action cannot be undone.
</p>
<p className="text-xs text-yellow-700/90 dark:text-yellow-400">
Funds sent to an incorrect address cannot be recovered.
</p>
</div>
</div>
</div>

<div className="space-y-3 pt-2">
<button
onClick={onConfirm}
disabled={isLoading}
className={cn(
"w-full py-3.5 rounded-xl font-semibold",
"bg-primary text-primary-foreground",
"hover:bg-primary/90 active:scale-[0.98]",
"transition-all duration-200",
"flex items-center justify-center gap-2",
isLoading && "opacity-80 cursor-not-allowed"
)}
>
{isLoading ? (
<>
<Loader2 className="size-5 animate-spin" />
Processing...
</>
) : (
"Confirm Withdrawal"
)}
</button>
<button
onClick={onCancel}
disabled={isLoading}
className={cn(
"w-full py-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors",
isLoading && "opacity-50 cursor-not-allowed"
)}
>
Cancel
</button>
</div>
</div>
);
}
91 changes: 91 additions & 0 deletions components/dashboard/withdraw/withdrawal-success.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"use client";

import Link from "next/link";
import { CheckCircle2, Copy, ExternalLink, ArrowRight } from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/utils";
import { useWithdrawalStore } from "@/hooks/useWithdrawalStore";
import type { Transaction } from "@/lib/api/transactions";

interface WithdrawalSuccessProps {
transaction: Transaction;
}

export function WithdrawalSuccess({ transaction }: WithdrawalSuccessProps) {
const [copied, setCopied] = useState(false);
const resetForm = useWithdrawalStore((state) => state.resetForm);

const handleCopy = () => {
navigator.clipboard.writeText(transaction.id);
setCopied(true);
window.setTimeout(() => setCopied(false), 2000);
};

return (
<div className="p-6 space-y-6">
<div className="flex flex-col items-center pt-4 pb-2">
<div className="mb-4 flex h-20 w-20 items-center justify-center rounded-full bg-green-500/10">
<CheckCircle2 className="size-10 text-green-500" />
</div>
<h2 className="text-xl font-bold text-foreground">Withdrawal submitted</h2>
<p className="mt-1 text-center text-sm text-muted-foreground">
Your withdrawal request has been received and is now being processed.
</p>
</div>

<div className="space-y-3 rounded-xl border border-border bg-muted/30 p-5">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs uppercase tracking-wide text-muted-foreground">Transaction ID</p>
<p className="mt-1 font-mono text-sm text-foreground">{transaction.id}</p>
</div>
<button
onClick={handleCopy}
className="rounded-lg p-2 transition-colors hover:bg-background"
aria-label="Copy transaction ID"
>
<Copy className={cn("size-4", copied ? "text-green-500" : "text-muted-foreground")} />
</button>
</div>

<div className="grid gap-3 text-sm sm:grid-cols-2">
<div className="rounded-lg border border-border bg-background/70 p-3">
<p className="text-xs text-muted-foreground">Amount</p>
<p className="mt-1 font-semibold text-foreground">{transaction.amountString}</p>
</div>
<div className="rounded-lg border border-border bg-background/70 p-3">
<p className="text-xs text-muted-foreground">Currency</p>
<p className="mt-1 font-semibold text-foreground">{transaction.currency}</p>
</div>
<div className="rounded-lg border border-border bg-background/70 p-3 sm:col-span-2">
<p className="text-xs text-muted-foreground">Destination address</p>
<p className="mt-1 break-all font-mono text-sm text-foreground">
{transaction.destinationAddress || transaction.reference || "Pending verification"}
</p>
</div>
<div className="rounded-lg border border-border bg-background/70 p-3 sm:col-span-2">
<p className="text-xs text-muted-foreground">Estimated processing time</p>
<p className="mt-1 font-semibold text-foreground">1-3 business days</p>
</div>
</div>
</div>

<div className="space-y-3 pt-2">
<Link
href="/dashboard/transactions"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-primary px-4 py-3 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90"
>
View in transactions
<ArrowRight className="size-4" />
</Link>
<button
onClick={() => resetForm()}
className="flex w-full items-center justify-center gap-2 rounded-xl border border-border px-4 py-3 text-sm font-medium text-foreground transition-colors hover:bg-muted"
>
<ExternalLink className="size-4" />
Make another withdrawal
</button>
</div>
</div>
);
}
160 changes: 29 additions & 131 deletions components/dashboard/withdrawal/WithdrawalReview.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@
"use client";

import { useWithdrawalStore } from "@/hooks/useWithdrawalStore";
import { ChevronLeft, Loader2, Coins, CircleDollarSign, BadgeDollarSign } from "lucide-react";
import { cn } from "@/lib/utils";
import { createWithdrawal } from "@/lib/api/transactions";

const currencies = [
{ id: 'USDC', name: 'USD Coin', icon: <CircleDollarSign className="w-8 h-8 text-blue-500" /> },
{ id: 'ETH', name: 'Ethereum', icon: <BadgeDollarSign className="w-8 h-8 text-neutral-500" /> },
{ id: 'BNB', name: 'BNB', icon: <Coins className="w-8 h-8 text-yellow-500" /> },
];
import { createWithdrawal, type Transaction } from "@/lib/api/transactions";
import { WithdrawalConfirmationModal } from "@/components/dashboard/withdraw/withdrawal-confirmation-modal";

export function WithdrawalReview() {
const { currency, amount, walletAddress, step, setStep, setTransactionResult, close, reset } = useWithdrawalStore();
const { currency, amount, walletAddress, step, setStep, setTransactionResult, setTransactionData } = useWithdrawalStore();

const isProcessing = step === 'processing';
const selectedCurrency = currencies.find(c => c.id === currency) || currencies[0];

const truncateAddress = (addr: string) => {
if (addr.length <= 16) return addr;
return `${addr.slice(0, 8)}...${addr.slice(-6)}`;
};

const handleConfirm = async () => {
setStep('processing');
Expand All @@ -32,7 +19,21 @@ export function WithdrawalReview() {
destinationAddress: walletAddress,
});

setTransactionResult(response.transactionId, 'success');
const transaction: Transaction = {
id: response.transactionId,
type: 'Withdraw',
currency,
amount: parseFloat(amount),
amountString: `${parseFloat(amount).toLocaleString()} ${currency}`,
date: new Date().toISOString(),
status: response.status === 'success' ? 'Success' : response.status === 'failed' ? 'Failed' : 'Pending',
reference: walletAddress,
description: 'Withdrawal request submitted',
destinationAddress: walletAddress,
};

setTransactionData(transaction);
setTransactionResult(response.transactionId, response.status === 'success' ? 'success' : response.status === 'failed' ? 'failed' : 'pending');
setStep('success');
} catch (error) {
const errorMessage = error instanceof Error
Expand All @@ -43,120 +44,17 @@ export function WithdrawalReview() {
}
};

const handleCancel = () => {
if (isProcessing) return;
close();
setTimeout(() => reset(), 300);
};

return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex items-center gap-3 pt-4">
<button
onClick={() => setStep('form')}
disabled={isProcessing}
className={cn(
"p-2 -ml-2 rounded-full hover:bg-muted transition-colors",
isProcessing && "opacity-50 cursor-not-allowed"
)}
aria-label="Go back to withdrawal form"
>
<ChevronLeft className="size-5 text-muted-foreground" />
</button>
<div>
<h2 className="text-xl font-bold text-foreground">Review Withdrawal</h2>
<p className="text-sm text-muted-foreground">Confirm your withdrawal details</p>
</div>
</div>

{/* Summary Card */}
<div className="bg-muted/30 rounded-xl p-5 space-y-4 border border-border">
{/* Amount */}
<div className="text-center pb-4 border-b border-border">
<p className="text-sm text-muted-foreground mb-1">You are withdrawing</p>
<div className="flex items-center justify-center gap-3">
{selectedCurrency.icon}
<span className="text-3xl font-bold text-foreground">
{parseFloat(amount).toLocaleString()} {currency}
</span>
</div>
</div>

{/* Details */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Destination</span>
<span className="text-sm font-medium text-foreground font-mono">
{truncateAddress(walletAddress)}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Network</span>
<span className="text-sm font-medium text-foreground">
{currency === 'ETH' ? 'Ethereum' : currency === 'BNB' ? 'BSC' : 'Stellar'}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Fee</span>
<span className="text-sm font-medium text-green-500">
Free
</span>
</div>
</div>
</div>

{/* Warning */}
<div className="bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-3">
<p className="text-xs text-yellow-600 dark:text-yellow-400">
<span className="font-semibold">Important:</span> Please verify the wallet address is correct. Transactions cannot be reversed once confirmed.
</p>
</div>

{/* Actions */}
<div className="space-y-3 pt-2">
<button
onClick={handleConfirm}
disabled={isProcessing}
className={cn(
"w-full py-3.5 rounded-xl font-semibold",
"bg-primary text-primary-foreground",
"hover:bg-primary/90 active:scale-[0.98]",
"transition-all duration-200",
"flex items-center justify-center gap-2",
isProcessing && "opacity-80 cursor-not-allowed"
)}
>
{isProcessing ? (
<>
<Loader2 className="size-5 animate-spin" />
Processing...
</>
) : (
"Confirm Withdrawal"
)}
</button>
<button
onClick={() => setStep('form')}
disabled={isProcessing}
className={cn(
"w-full py-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors",
isProcessing && "opacity-50 cursor-not-allowed"
)}
>
Go Back
</button>
<button
onClick={handleCancel}
disabled={isProcessing}
className={cn(
"w-full py-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors",
isProcessing && "opacity-50 cursor-not-allowed"
)}
>
Cancel
</button>
</div>
</div>
<WithdrawalConfirmationModal
amount={amount}
currency={currency}
destinationAddress={walletAddress}
onConfirm={handleConfirm}
onCancel={() => {
if (isProcessing) return;
setStep('form');
}}
isLoading={isProcessing}
/>
);
}
Loading
Loading