diff --git a/frontend/package.json b/frontend/package.json index 1eb947f2..2955e380 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "boring-avatars": "^2.0.4", "framer-motion": "^12.38.0", "marked": "^17.0.5", + "motion": "^12.38.0", "next": "^14.2.5", "next-intl": "^4.8.3", "next-themes": "^0.4.6", diff --git a/frontend/src/app/(authenticated)/layout.tsx b/frontend/src/app/(authenticated)/layout.tsx index e91ad64e..45be80e9 100644 --- a/frontend/src/app/(authenticated)/layout.tsx +++ b/frontend/src/app/(authenticated)/layout.tsx @@ -5,14 +5,19 @@ import Breadcrumbs from "@/components/Breadcrumbs"; import LocaleSwitcher from "@/components/LocaleSwitcher"; import PaymentToastListener from "@/components/PaymentToastListener"; import { motion } from "framer-motion"; +import AuthGuard from "@/components/AuthGuard"; +import { useHydrateMerchantStore } from "@/lib/merchant-store"; export default function AuthenticatedLayout({ children, }: { children: React.ReactNode; }) { + useHydrateMerchantStore(); + return ( -
+ +
{/* Sidebar - fixed width for desktop layout offset */} @@ -41,5 +46,6 @@ export default function AuthenticatedLayout({
+ ); } diff --git a/frontend/src/app/(public)/login/page.tsx b/frontend/src/app/(public)/login/page.tsx index 3f2768ab..1e0fdded 100644 --- a/frontend/src/app/(public)/login/page.tsx +++ b/frontend/src/app/(public)/login/page.tsx @@ -1,6 +1,7 @@ import HeroSection from "@/components/login/HeroSection"; import LoginForm from "@/components/login/LoginForm"; import Link from "next/link"; +import GuestGuard from "@/components/GuestGuard"; export const metadata = { title: "Login - Stellar Pay", @@ -9,6 +10,7 @@ export const metadata = { export default function LoginPage() { return ( +
@@ -46,5 +48,6 @@ export default function LoginPage() {
+
); } diff --git a/frontend/src/app/(public)/page.tsx b/frontend/src/app/(public)/page.tsx index 153d023d..2654b372 100644 --- a/frontend/src/app/(public)/page.tsx +++ b/frontend/src/app/(public)/page.tsx @@ -1,5 +1,6 @@ "use client"; +import GuestGuard from "@/components/GuestGuard"; import Link from "next/link"; import { motion } from "framer-motion"; import { useState } from "react"; @@ -541,6 +542,7 @@ function Footer() { export default function Home() { return ( +
{/* subtle grid texture */}
+
); } diff --git a/frontend/src/app/(public)/register/page.tsx b/frontend/src/app/(public)/register/page.tsx index 02e8b718..b3e9c8ae 100644 --- a/frontend/src/app/(public)/register/page.tsx +++ b/frontend/src/app/(public)/register/page.tsx @@ -1,8 +1,10 @@ import RegistrationForm from "@/components/RegistrationForm"; import Link from "next/link"; +import GuestGuard from "@/components/GuestGuard"; export default function RegisterPage() { return ( +

Onboarding

@@ -25,5 +27,6 @@ export default function RegisterPage() {

+
); } diff --git a/frontend/src/app/dashboard/create/page.tsx b/frontend/src/app/dashboard/create/page.tsx deleted file mode 100644 index acfdfdc6..00000000 --- a/frontend/src/app/dashboard/create/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import CreatePaymentForm from "@/components/CreatePaymentForm"; -import Link from "next/link"; - -export const metadata = { - title: "Create Payment Link — Stellar Payment Dashboard", - description: - "Generate a shareable Stellar payment link for XLM or USDC in seconds.", -}; - -export default function CreatePaymentPage() { - return ( -
-
-

- Dashboard -

-

- Create Payment Link -

-

- Set an amount, choose an asset, and enter a Stellar recipient address - to generate a shareable payment link. -

-
- -
- -
- - -
- ); -} diff --git a/frontend/src/components/AuthGuard.tsx b/frontend/src/components/AuthGuard.tsx new file mode 100644 index 00000000..fde1baff --- /dev/null +++ b/frontend/src/components/AuthGuard.tsx @@ -0,0 +1,29 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { useRouter, usePathname } from "next/navigation"; +import { useMerchantHydrated, useMerchantSession } from "@/lib/merchant-store"; + +export default function AuthGuard({ children }: { children: React.ReactNode }) { + const hydrated = useMerchantHydrated(); + const session = useMerchantSession(); + const router = useRouter(); + const pathname = usePathname(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (mounted && hydrated && !session) { + router.push(`/login?callbackUrl=${encodeURIComponent(pathname)}`); + } + }, [mounted, hydrated, session, router, pathname]); + + if (!mounted || !hydrated || !session) { + return null; + } + + return <>{children}; +} diff --git a/frontend/src/components/CreatePaymentForm.tsx b/frontend/src/components/CreatePaymentForm.tsx index 690324d2..e690d588 100644 --- a/frontend/src/components/CreatePaymentForm.tsx +++ b/frontend/src/components/CreatePaymentForm.tsx @@ -11,7 +11,7 @@ import { useMerchantHydrated, useMerchantTrustedAddresses, } from "@/lib/merchant-store"; -import { useLocalStorage } from "@/hooks/useLocalStorage"; + const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000"; @@ -49,6 +49,11 @@ export default function CreatePaymentForm() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [created, setCreated] = useState(null); + + const [useSessionBranding, setUseSessionBranding] = useState(false); + const [branding, setBranding] = useState(DEFAULT_BRANDING); + const [selectedTrustedAddress, setSelectedTrustedAddress] = useState(""); + const apiKey = useMerchantApiKey(); const hydrated = useMerchantHydrated(); const trustedAddresses = useMerchantTrustedAddresses(); diff --git a/frontend/src/components/DevTools.tsx b/frontend/src/components/DevTools.tsx index bb61d450..44f54464 100644 --- a/frontend/src/components/DevTools.tsx +++ b/frontend/src/components/DevTools.tsx @@ -57,8 +57,8 @@ export default function DevTools() { const parsed = JSON.parse(requestBody); setRequestBody(JSON.stringify(parsed, null, 2)); setJsonError(null); - } catch (err: any) { - setJsonError(err.message || "Invalid JSON"); + } catch (err: unknown) { + setJsonError(err instanceof Error ? err.message : "Invalid JSON"); } }; @@ -77,8 +77,8 @@ export default function DevTools() { JSON.parse(requestBody); bodyData = requestBody; } - } catch (err: any) { - throw new Error(`Invalid JSON body: ${err.message}`); + } catch (err: unknown) { + throw new Error(`Invalid JSON body: ${err instanceof Error ? err.message : String(err)}`); } } @@ -115,9 +115,9 @@ export default function DevTools() { setResponseTime(Math.round(finishedAt - startedAt)); setResponseBody(formattedBody); - } catch (error: any) { + } catch (error: unknown) { setResponseStatus(0); - setResponseBody(error.message || "Request failed to send. Check network or CORS."); + setResponseBody(error instanceof Error ? error.message : "Request failed to send. Check network or CORS."); } finally { setIsRunning(false); } diff --git a/frontend/src/components/FirstApiKeyModal.tsx b/frontend/src/components/FirstApiKeyModal.tsx index 506bd2ac..c9503c01 100644 --- a/frontend/src/components/FirstApiKeyModal.tsx +++ b/frontend/src/components/FirstApiKeyModal.tsx @@ -24,7 +24,7 @@ export default function FirstApiKeyModal({ isOpen, onClose }: FirstApiKeyModalPr setApiKey(newKey); setStoreApiKey(newKey); toast.success("API Key generated successfully!"); - } catch (err: any) { + } catch (err: unknown) { const message = err instanceof Error ? err.message : "Failed to generate API Key"; toast.error(message); } finally { @@ -40,7 +40,7 @@ export default function FirstApiKeyModal({ isOpen, onClose }: FirstApiKeyModalPr

Generate your first API key

- To start accepting payments, you'll need an API key to authenticate your server-side requests. + To start accepting payments, you'll need an API key to authenticate your server-side requests.

@@ -57,7 +57,7 @@ export default function FirstApiKeyModal({ isOpen, onClose }: FirstApiKeyModalPr

Your API Key is ready!

- Copy this key and save it somewhere secure. You won't be able to see it again after closing this window. + Copy this key and save it somewhere secure. You won't be able to see it again after closing this window.

diff --git a/frontend/src/components/GuestGuard.tsx b/frontend/src/components/GuestGuard.tsx new file mode 100644 index 00000000..d2cf0a60 --- /dev/null +++ b/frontend/src/components/GuestGuard.tsx @@ -0,0 +1,30 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useMerchantHydrated, useMerchantSession } from "@/lib/merchant-store"; + +export default function GuestGuard({ children }: { children: React.ReactNode }) { + const hydrated = useMerchantHydrated(); + const session = useMerchantSession(); + const router = useRouter(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (mounted && hydrated && session) { + router.push("/dashboard"); + } + }, [mounted, hydrated, session, router]); + + // If we are definitely authenticated, hide the public UI so it doesn't flash + // before the redirect kicks in. + if (mounted && hydrated && session) { + return null; + } + + return <>{children}; +} diff --git a/frontend/src/components/RecentPayments.tsx b/frontend/src/components/RecentPayments.tsx index 64e08763..b64ec61f 100644 --- a/frontend/src/components/RecentPayments.tsx +++ b/frontend/src/components/RecentPayments.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useLocale, useTranslations } from "next-intl"; -import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; +import Skeleton from "react-loading-skeleton"; import "react-loading-skeleton/dist/skeleton.css"; import PaymentDetailModal from "@/components/PaymentDetailModal"; import ExportCsvButton from "@/components/ExportCsvButton"; @@ -107,9 +107,7 @@ export default function RecentPayments({ const [payments, setPayments] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [page, setPage] = useState(1); - // const [, setTotalPages] = useState(1); - const [_totalPages, setTotalPages] = useState(1); + const page = 1; const [totalCount, setTotalCount] = useState(0); const [selectedPayment, setSelectedPayment] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); @@ -188,6 +186,9 @@ export default function RecentPayments({ return; } + setLoading(true); + setError(null); + const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"; @@ -210,20 +211,8 @@ export default function RecentPayments({ "x-api-key": apiKey, }, signal: controller.signal, - setLoading(true); - setError(null); - - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"; - const params = buildSearchParams(filters); - params.set("page", "1"); - params.set("limit", LIMIT.toString()); - - const response = await fetch(`${apiUrl}/api/payments?${params.toString()}`, { - headers: { - "x-api-key": apiKey, - }, - signal: controller.signal, - }); + } + ); if (!response.ok) { throw new Error(t("fetchFailed")); @@ -384,7 +373,7 @@ export default function RecentPayments({ - + ); } @@ -794,50 +783,7 @@ export default function RecentPayments({ - - {payments.map((payment) => ( - handlePaymentClick(payment.id)} - > - - - {payment.status} - - - - {payment.amount} {payment.asset} - - - {payment.description || "—"} - - - {new Date(payment.created_at).toLocaleDateString()} - - - - - - + {payments.map((payment) => ( - )} ); } - -function FilterChip({ - label, - onClear, - ariaLabel, -}: { - label: string; - onClear: () => void; - ariaLabel: string; -}) { - return ( - - {label} - - - ); -} diff --git a/frontend/src/utils/csv.ts b/frontend/src/utils/csv.ts index fc7d426f..2581fa27 100644 --- a/frontend/src/utils/csv.ts +++ b/frontend/src/utils/csv.ts @@ -1,4 +1,4 @@ -export const convertToCSV = (data: any[]) => { +export const convertToCSV = (data: Record[]) => { if (!data.length) return ""; const headers = Object.keys(data[0]);