From 9b2b6a8b23994ac749c28b8df1ea221b9b9e76c9 Mon Sep 17 00:00:00 2001
From: Nursca Ajah
Date: Sat, 28 Mar 2026 01:36:09 +0100
Subject: [PATCH 1/2] refactor: separate marketing home from merchant dashboard
---
frontend/package.json | 1 +
frontend/src/app/(authenticated)/layout.tsx | 8 ++++-
frontend/src/app/(public)/login/page.tsx | 3 ++
frontend/src/app/(public)/page.tsx | 3 ++
frontend/src/app/(public)/register/page.tsx | 3 ++
frontend/src/app/dashboard/create/page.tsx | 40 ---------------------
frontend/src/components/AuthGuard.tsx | 29 +++++++++++++++
frontend/src/components/GuestGuard.tsx | 30 ++++++++++++++++
8 files changed, 76 insertions(+), 41 deletions(-)
delete mode 100644 frontend/src/app/dashboard/create/page.tsx
create mode 100644 frontend/src/components/AuthGuard.tsx
create mode 100644 frontend/src/components/GuestGuard.tsx
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/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}>;
+}
From 7dc358970cb2ae7be6bf2b230410bd146a6af992 Mon Sep 17 00:00:00 2001
From: Nursca Ajah
Date: Sat, 28 Mar 2026 02:07:12 +0100
Subject: [PATCH 2/2] refactor: separate marketing home from merchant dashboard
---
frontend/src/components/CreatePaymentForm.tsx | 7 +-
frontend/src/components/DevTools.tsx | 12 +--
frontend/src/components/FirstApiKeyModal.tsx | 6 +-
frontend/src/components/RecentPayments.tsx | 98 ++-----------------
frontend/src/utils/csv.ts | 2 +-
5 files changed, 25 insertions(+), 100 deletions(-)
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/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()}
-
-
- {
- e.stopPropagation();
- handlePaymentClick(payment.id);
- }}
- className="font-mono text-xs text-mint transition-colors hover:text-glow"
- >
- View →
-
-
-
-
+
{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]);