diff --git a/frontend/src/__tests__/setup.ts b/frontend/src/__tests__/setup.ts index 7b0828b..3310558 100644 --- a/frontend/src/__tests__/setup.ts +++ b/frontend/src/__tests__/setup.ts @@ -1 +1,30 @@ import '@testing-library/jest-dom'; +import { vi } from 'vitest'; + +const localStorageMock = (() => { + let store: Record = {}; + return { + getItem: vi.fn((key: string) => store[key] || null), + setItem: vi.fn((key: string, value: string) => { + store[key] = value.toString(); + }), + clear: vi.fn(() => { + store = {}; + }), + removeItem: vi.fn((key: string) => { + delete store[key]; + }), + key: vi.fn((index: number) => Object.keys(store)[index] || null), + length: 0, + }; +})(); + +vi.stubGlobal('localStorage', localStorageMock); +if (typeof window !== 'undefined') { + Object.defineProperty(window, 'localStorage', { + value: localStorageMock, + configurable: true, + writable: true, + }); +} + diff --git a/frontend/src/app/streams/create/page.tsx b/frontend/src/app/streams/create/page.tsx index f1c7cfd..66164f6 100644 --- a/frontend/src/app/streams/create/page.tsx +++ b/frontend/src/app/streams/create/page.tsx @@ -8,263 +8,4 @@ export const metadata: Metadata = { export default function CreateStreamPage() { return ; -import React, { useState } from "react"; -import { - createStream, - toBaseUnits, - toDurationSeconds, - getTokenAddress, - toSorobanErrorMessage, - TOKEN_ADDRESSES -} from "@/lib/soroban"; -import { hasValidPrecision, validateAmountInput } from "@/utils/amount"; -import { isValidStellarPublicKey } from "@/lib/stellar"; -import { toast } from "react-hot-toast"; -import { useRouter } from "next/navigation"; -import Link from "next/link"; -import { ArrowLeft } from "lucide-react"; -import { useWallet } from "@/context/wallet-context"; - -const TOKEN_DECIMALS = 7; - -export default function CreateStreamPage() { - const { status, session } = useWallet(); - const router = useRouter(); - const [nowTimestamp] = useState(() => Date.now()); - const [loading, setLoading] = useState(false); - const [txState, setTxState] = useState<"idle" | "signing" | "submitted" | "confirming">("idle"); - const [formData, setFormData] = useState({ - recipient: "", - token: "XLM", - amount: "", - duration: "30", // days - }); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (status !== "connected" || !session) { - toast.error("Please connect your wallet first."); - return; - } - - // Validate recipient - if (!formData.recipient.trim()) { - toast.error("Recipient address is required"); - return; - } - if (!isValidStellarPublicKey(formData.recipient)) { - toast.error("Invalid Stellar public key format"); - return; - } - - // Validate amount - const validationError = validateAmountInput(formData.amount, TOKEN_DECIMALS); - if (validationError) { - toast.error(validationError); - return; - } - - // Validate duration - const durationNum = parseFloat(formData.duration); - if (isNaN(durationNum) || durationNum <= 0) { - toast.error("Duration must be a positive number"); - return; - } - - setLoading(true); - setTxState("signing"); - - try { - const amountBigInt = toBaseUnits(formData.amount); - const durationBigInt = toDurationSeconds(formData.duration, "days"); - const tokenAddress = getTokenAddress(formData.token); - - const result = await createStream(session, { - recipient: formData.recipient, - tokenAddress, - amount: amountBigInt, - durationSeconds: durationBigInt, - }); - - if (result.success) { - setTxState("confirming"); - toast.success("Stream created successfully!"); - // Small delay to allow indexer to catch up - setTimeout(() => { - router.push("/dashboard"); - }, 2000); - } - } catch (error) { - console.error("Stream creation failed:", error); - toast.error(toSorobanErrorMessage(error)); - } finally { - setLoading(false); - setTxState("idle"); - } - }; - - const getButtonText = () => { - if (!loading) return "Start Streaming"; - switch (txState) { - case "signing": return "Confirm in Wallet..."; - case "submitted": return "Submitting to Network..."; - case "confirming": return "Finalizing Stream..."; - default: return "Processing..."; - } - }; - - // Inline validation feedback for the amount field. validateAmountInput - // returns an error message when invalid and null when valid. Only show it - // once the user has typed something — the empty case is handled on submit. - const amountError = formData.amount - ? validateAmountInput(formData.amount, TOKEN_DECIMALS) - : null; - - const recipientError = formData.recipient - ? (!isValidStellarPublicKey(formData.recipient) ? "Invalid Stellar public key format" : null) - : null; - - const durationError = formData.duration - ? (isNaN(Number(formData.duration)) || Number(formData.duration) <= 0 - ? "Duration must be a positive number" - : null) - : null; - - return ( -
- - - Back to Dashboard - - -
-

Create New Stream

-

- Set up a real-time payment stream to any Stellar address. -

- -
-
- - setFormData({ ...formData, recipient: e.target.value })} - required - /> - {recipientError && ( -

{recipientError}

- )} -
- -
-
- - -
-
- - { - const newValue = e.target.value; - // Only allow valid number characters and check precision - if (newValue === '' || /^\d*\.?\d*$/.test(newValue)) { - if (hasValidPrecision(newValue, TOKEN_DECIMALS)) { - setFormData({ ...formData, amount: newValue }); - } - } - }} - required - /> - {amountError && ( -

{amountError}

- )} -
-
- -
- - setFormData({ ...formData, duration: e.target.value })} - required - /> - {durationError && ( -

{durationError}

- )} -
- -
-
- Streaming Rate - - {formData.amount && formData.duration && Number(formData.duration) > 0 - ? (Number(formData.amount) / (Number(formData.duration) * 86400)).toFixed(8) - : "0.00000000"} {formData.token}/sec - -
-
- Estimated End Date - - {formData.duration && Number(formData.duration) > 0 - ? new Date(nowTimestamp + Number(formData.duration) * 86400000).toLocaleDateString() - : "—"} - -
-
- - - - {status !== "connected" && ( -

- Please connect your wallet to create a stream. -

- )} -
-
-
- ); } diff --git a/frontend/src/components/dashboard/StreamDetailsModal.tsx b/frontend/src/components/dashboard/StreamDetailsModal.tsx index e960fc2..999891c 100644 --- a/frontend/src/components/dashboard/StreamDetailsModal.tsx +++ b/frontend/src/components/dashboard/StreamDetailsModal.tsx @@ -1,7 +1,8 @@ "use client"; -import React, { useEffect } from "react"; +import React from "react"; import { Button } from "@/components/ui/Button"; +import { useModalDialog } from "@/hooks/useModalDialog"; import type { Stream } from "@/lib/dashboard"; interface StreamDetailsModalProps { @@ -17,14 +18,7 @@ export const StreamDetailsModal: React.FC = ({ onCancelClick, onTopUpClick, }) => { - // Escape key support - useEffect(() => { - const handleEscape = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); - }; - window.addEventListener("keydown", handleEscape); - return () => window.removeEventListener("keydown", handleEscape); - }, [onClose]); + const dialogRef = useModalDialog({ onClose }); const progress = stream.deposited > 0 ? Math.min(100, Math.max(0, (stream.withdrawn / stream.deposited) * 100)) : 0; const remaining = stream.deposited - stream.withdrawn; @@ -32,15 +26,21 @@ export const StreamDetailsModal: React.FC = ({ return (
{ if (e.target === e.currentTarget) onClose(); }} > -
+
{/* Header */}
-

Stream Details

+

Stream Details

ID: {stream.id}