Skip to content

Commit 2e65831

Browse files
author
Ubuntu
committed
feat: connect registration to dashboard onboarding
1 parent 9d82bd9 commit 2e65831

4 files changed

Lines changed: 149 additions & 51 deletions

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
const maybeSingle = vi.fn();
4+
const eq = vi.fn(() => ({ maybeSingle }));
5+
const selectExisting = vi.fn(() => ({ eq }));
6+
const single = vi.fn();
7+
const selectInserted = vi.fn(() => ({ single }));
8+
const insert = vi.fn(() => ({ select: selectInserted }));
9+
const from = vi.fn(() => ({
10+
select: selectExisting,
11+
insert,
12+
}));
13+
14+
vi.mock("../lib/supabase.js", () => ({
15+
supabase: { from },
16+
}));
17+
18+
vi.mock("../lib/auth.js", () => ({
19+
hashPassword: vi.fn().mockResolvedValue("hashed-password"),
20+
requireApiKeyAuth: vi.fn(() => (req, res, next) => next()),
21+
requireSessionAuth: vi.fn(() => (req, res, next) => next()),
22+
}));
23+
24+
vi.mock("../lib/sep10-auth.js", () => ({
25+
generateSessionToken: vi.fn(() => "session.jwt.token"),
26+
}));
27+
28+
function createResponse() {
29+
return {
30+
status: vi.fn().mockReturnThis(),
31+
json: vi.fn(),
32+
};
33+
}
34+
35+
function getRegisterMerchantHandler(router) {
36+
const layer = router.stack.find(
37+
(entry) =>
38+
entry.route?.path === "/register-merchant" && entry.route?.methods?.post,
39+
);
40+
41+
if (!layer) {
42+
throw new Error("register-merchant route not found");
43+
}
44+
45+
return layer.route.stack[layer.route.stack.length - 1].handle;
46+
}
47+
48+
describe("POST /api/register-merchant", () => {
49+
beforeEach(() => {
50+
vi.clearAllMocks();
51+
maybeSingle.mockResolvedValue({ data: null });
52+
single.mockResolvedValue({
53+
data: {
54+
id: "merchant-123",
55+
email: "owner@example.com",
56+
business_name: "Pluto Store",
57+
notification_email: "alerts@example.com",
58+
merchant_settings: null,
59+
metadata: null,
60+
api_key: "sk_test",
61+
webhook_secret: "whsec_test",
62+
created_at: "2026-04-21T00:00:00.000Z",
63+
},
64+
error: null,
65+
});
66+
});
67+
68+
it("returns a session token with the registered merchant", async () => {
69+
const { default: createMerchantsRouter } = await import("./merchants.js");
70+
const router = createMerchantsRouter();
71+
const handler = getRegisterMerchantHandler(router);
72+
const res = createResponse();
73+
const next = vi.fn();
74+
75+
await handler(
76+
{
77+
body: {
78+
email: "owner@example.com",
79+
password: "correct horse battery staple",
80+
business_name: "Pluto Store",
81+
notification_email: "alerts@example.com",
82+
},
83+
},
84+
res,
85+
next,
86+
);
87+
88+
expect(insert).toHaveBeenCalledWith(
89+
expect.objectContaining({
90+
email: "owner@example.com",
91+
password_hash: "hashed-password",
92+
}),
93+
);
94+
expect(res.status).toHaveBeenCalledWith(201);
95+
expect(res.json).toHaveBeenCalledWith(
96+
expect.objectContaining({
97+
message: "Merchant registered successfully",
98+
token: "session.jwt.token",
99+
merchant: expect.objectContaining({
100+
id: "merchant-123",
101+
email: "owner@example.com",
102+
api_key: "sk_test",
103+
}),
104+
}),
105+
);
106+
expect(next).not.toHaveBeenCalled();
107+
});
108+
});

frontend/src/app/(authenticated)/dashboard/page.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ import {
1212
useMerchantMetadata,
1313
} from "@/lib/merchant-store";
1414
import { useTranslations } from "next-intl";
15+
import { toast } from "sonner";
1516
import FirstApiKeyModal from "@/components/FirstApiKeyModal";
1617
import FirstPaymentCelebration from "@/components/FirstPaymentCelebration";
1718

19+
const ONBOARDING_WELCOME_KEY = "merchant_onboarding_welcome";
20+
1821
export default function DashboardPage() {
1922
const t = useTranslations("dashboardPage");
2023
const [isFirstKeyModalOpen, setIsFirstKeyModalOpen] = useState(false);
@@ -38,6 +41,21 @@ export default function DashboardPage() {
3841
}
3942
}, [hydrated, loading, apiKey]);
4043

44+
useEffect(() => {
45+
if (!hydrated || loading || typeof window === "undefined") return;
46+
if (sessionStorage.getItem(ONBOARDING_WELCOME_KEY) !== "true") return;
47+
48+
sessionStorage.removeItem(ONBOARDING_WELCOME_KEY);
49+
toast.success("Welcome to your dashboard. Create your first payment when you are ready.", {
50+
action: {
51+
label: "Create payment",
52+
onClick: () => {
53+
window.location.href = "/create";
54+
},
55+
},
56+
});
57+
}, [hydrated, loading]);
58+
4159
if (!hydrated || loading) return <DashboardSkeleton />;
4260

4361
return (

frontend/src/components/RegistrationForm.tsx

Lines changed: 9 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"use client";
22

33
import { useState } from "react";
4-
import { registerMerchant, type Merchant as AuthMerchant } from "../lib/auth";
4+
import { registerMerchant } from "../lib/auth";
55
import { toast } from "sonner";
6-
import MaskedValue from "./MaskedValue";
6+
import { useRouter } from "next/navigation";
77
import zxcvbn from "zxcvbn";
88
import {
99
useSetMerchantApiKey,
@@ -14,8 +14,10 @@ import { Spinner } from "./ui/Spinner";
1414

1515
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1616
const BUSINESS_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9\s&'.,-]{1,79}$/;
17+
const ONBOARDING_WELCOME_KEY = "merchant_onboarding_welcome";
1718

1819
export default function RegistrationForm() {
20+
const router = useRouter();
1921
const setToken = useSetMerchantToken();
2022
const setApiKey = useSetMerchantApiKey();
2123
const setMerchant = useSetMerchantMetadata();
@@ -33,9 +35,6 @@ export default function RegistrationForm() {
3335
string | null
3436
>(null);
3537
const [passwordError, setPasswordError] = useState<string | null>(null);
36-
const [registeredMerchant, setRegisteredMerchant] = useState<AuthMerchant | null>(
37-
null,
38-
);
3938

4039
const businessNameTrimmed = businessName.trim();
4140
const emailTrimmed = email.trim();
@@ -115,10 +114,13 @@ export default function RegistrationForm() {
115114
setToken(data.token);
116115
}
117116

118-
setRegisteredMerchant(data.merchant);
119117
setApiKey(data.merchant.api_key);
120118
setMerchant(data.merchant);
121-
toast.success("Merchant registered successfully!");
119+
if (typeof window !== "undefined") {
120+
sessionStorage.setItem(ONBOARDING_WELCOME_KEY, "true");
121+
}
122+
toast.success("Welcome! Your merchant account is ready.");
123+
router.push("/dashboard");
122124
} catch (err: unknown) {
123125
const message =
124126
err instanceof Error ? err.message : "Failed to register merchant";
@@ -129,49 +131,6 @@ export default function RegistrationForm() {
129131
}
130132
};
131133

132-
if (registeredMerchant) {
133-
return (
134-
<div className="flex flex-col gap-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
135-
<div className="rounded-[3rem] border border-[#E8E8E8] bg-white p-12 shadow-[0_20px_60px_rgb(0,0,0,0.05)]">
136-
<div className="flex flex-col gap-4 text-center sm:text-left mb-8">
137-
<p className="font-bold text-[10px] uppercase tracking-[0.4em] text-[#6B6B6B]">
138-
Success
139-
</p>
140-
<h2 className="text-4xl font-bold text-[#0A0A0A] font-serif tracking-tight uppercase">
141-
Welcome, {registeredMerchant.business_name}
142-
</h2>
143-
<p className="text-sm font-medium text-[#6B6B6B] leading-relaxed">
144-
Your merchant account is ready. Save your API key below—you
145-
won&apos;t be able to access it again.
146-
</p>
147-
</div>
148-
149-
<div className="space-y-4">
150-
<MaskedValue
151-
label="Your API Key"
152-
value={registeredMerchant.api_key}
153-
copyText={registeredMerchant.api_key}
154-
defaultRevealed={true}
155-
/>
156-
<MaskedValue
157-
label="Webhook Secret"
158-
value={registeredMerchant.webhook_secret}
159-
copyText={registeredMerchant.webhook_secret}
160-
defaultRevealed={true}
161-
/>
162-
</div>
163-
</div>
164-
165-
<a
166-
href="/dashboard"
167-
className="text-center text-[10px] font-bold uppercase tracking-widest text-[#6B6B6B] transition-colors underline underline-offset-8 hover:text-[#0A0A0A]"
168-
>
169-
Enter Dashboard
170-
</a>
171-
</div>
172-
);
173-
}
174-
175134
return (
176135
<form onSubmit={handleSubmit} className="flex flex-col gap-8" noValidate>
177136
{error && (

frontend/src/lib/auth.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,20 @@ export async function registerMerchant(
132132
throw new Error(body.error ?? "Registration failed");
133133
}
134134

135-
return await res.json();
135+
const body = await res.json();
136+
const token = body.token;
137+
if (!token) throw new Error("No token in server response");
138+
139+
saveToken(token);
140+
141+
if (body.merchant && typeof window !== "undefined") {
142+
localStorage.setItem("merchant_metadata", JSON.stringify(body.merchant));
143+
if (body.merchant.api_key) {
144+
localStorage.setItem("merchant_api_key", body.merchant.api_key);
145+
}
146+
}
147+
148+
return body;
136149
}
137150

138151
export function logout(): void {

0 commit comments

Comments
 (0)