From 2762d170e73750bf1cd1ad82fb6aede178eda3f3 Mon Sep 17 00:00:00 2001 From: Tybravo Date: Tue, 2 Jun 2026 12:43:09 +0100 Subject: [PATCH] feat: add public payment invoice flow --- src/app/payment/[id]/page.tsx | 108 ++++++++++++++++++ .../payment/InvalidInvoiceState.tsx | 33 ++++++ .../payment/PaymentInvoiceSummary.tsx | 61 ++++++++++ .../payment/PublicPaymentLayout.tsx | 28 +++++ 4 files changed, 230 insertions(+) create mode 100644 src/app/payment/[id]/page.tsx create mode 100644 src/components/payment/InvalidInvoiceState.tsx create mode 100644 src/components/payment/PaymentInvoiceSummary.tsx create mode 100644 src/components/payment/PublicPaymentLayout.tsx diff --git a/src/app/payment/[id]/page.tsx b/src/app/payment/[id]/page.tsx new file mode 100644 index 0000000..c853056 --- /dev/null +++ b/src/app/payment/[id]/page.tsx @@ -0,0 +1,108 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { useParams } from "next/navigation"; +import { PublicPaymentLayout } from "@/components/payment/PublicPaymentLayout"; +import { + PaymentInvoiceSummary, + type InvoiceData, +} from "@/components/payment/PaymentInvoiceSummary"; +import { InvalidInvoiceState } from "@/components/payment/InvalidInvoiceState"; +import { Button } from "@/components/ui/button"; +import { Loader2 } from "lucide-react"; + +// Mock API service to fetch invoice by ID +const fetchMockInvoice = async (id: string): Promise => { + return new Promise((resolve, reject) => { + setTimeout(() => { + if (id === "invalid") { + resolve({ + id, + merchantName: "Unknown", + description: "", + amount: 0, + tokenSymbol: "USDC", + status: "invalid", + }); + } else if (id === "expired") { + resolve({ + id, + merchantName: "Acme Corp", + description: "Software Services", + amount: 150.0, + tokenSymbol: "USDC", + status: "expired", + }); + } else { + // Default valid mock invoice + resolve({ + id, + merchantName: "DripWave Studios", + description: "Freelance Design Work", + amount: 50.0, + tokenSymbol: "USDC", + status: "valid", + }); + } + }, 1500); // 1.5s simulated network delay + }); +}; + +export default function PublicPaymentPage() { + const params = useParams(); + const invoiceId = params?.id as string; + + const [invoice, setInvoice] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!invoiceId) return; + + setLoading(true); + fetchMockInvoice(invoiceId) + .then((data) => { + setInvoice(data); + }) + .catch(() => { + setInvoice(null); + }) + .finally(() => { + setLoading(false); + }); + }, [invoiceId]); + + return ( + + {loading ? ( +
+ +

+ Loading invoice details... +

+
+ ) : !invoice || invoice.status === "invalid" || invoice.status === "expired" ? ( + + ) : ( +
+ + + {/* Placeholder for actual payment mechanism */} +
+

+ Select Payment Method +

+ +
+
+ )} +
+ ); +} diff --git a/src/components/payment/InvalidInvoiceState.tsx b/src/components/payment/InvalidInvoiceState.tsx new file mode 100644 index 0000000..85c2a1c --- /dev/null +++ b/src/components/payment/InvalidInvoiceState.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { AlertCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; + +interface InvalidInvoiceStateProps { + title?: string; + message?: string; +} + +export function InvalidInvoiceState({ + title = "Invoice Not Available", + message = "This payment link is invalid or has expired.", +}: InvalidInvoiceStateProps) { + return ( +
+
+ +
+
+

+ {title} +

+

{message}

+
+
+ +
+
+ ); +} diff --git a/src/components/payment/PaymentInvoiceSummary.tsx b/src/components/payment/PaymentInvoiceSummary.tsx new file mode 100644 index 0000000..13ab93f --- /dev/null +++ b/src/components/payment/PaymentInvoiceSummary.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import Image from "next/image"; + +export interface InvoiceData { + id: string; + merchantName: string; + merchantLogo?: string; + description: string; + amount: number; + tokenSymbol: string; + status: "valid" | "expired" | "invalid"; +} + +interface PaymentInvoiceSummaryProps { + invoice: InvoiceData; +} + +export function PaymentInvoiceSummary({ invoice }: PaymentInvoiceSummaryProps) { + return ( +
+
+ {invoice.merchantLogo ? ( +
+ {invoice.merchantName} +
+ ) : ( +
+ + {invoice.merchantName.charAt(0).toUpperCase()} + +
+ )} + +
+

+ {invoice.merchantName} +

+

{invoice.description}

+
+ +
+
+ {invoice.amount.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + {invoice.tokenSymbol} + +
+
+
+
+ ); +} diff --git a/src/components/payment/PublicPaymentLayout.tsx b/src/components/payment/PublicPaymentLayout.tsx new file mode 100644 index 0000000..40d6673 --- /dev/null +++ b/src/components/payment/PublicPaymentLayout.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import Image from "next/image"; + +export function PublicPaymentLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+
+
+ Shade Logo + + Shade + +
+
+
{children}
+
+ ); +}