Skip to content

Commit dead796

Browse files
authored
Merge pull request #14 from Cubid-Me/codex/implement-new-user-onboarding-flow
feat: redesign new user onboarding flow
2 parents 640c8d2 + 3d2e2e9 commit dead796

File tree

4 files changed

+508
-76
lines changed

4 files changed

+508
-76
lines changed

frontend/__mocks__/jsqr.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function mockJsqr() {
2+
return null;
3+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import type { Session } from "@supabase/supabase-js";
2+
import { act, render, screen, waitFor } from "@testing-library/react";
3+
import userEvent from "@testing-library/user-event";
4+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5+
6+
import NewUserPage from "../src/app/new-user/page";
7+
import { useUserStore } from "../src/lib/store";
8+
9+
const {
10+
pushMock,
11+
requestCubidIdMock,
12+
ensureWalletMock,
13+
upsertMyProfileMock,
14+
uploadMock,
15+
getPublicUrlMock,
16+
createObjectURLMock,
17+
revokeObjectURLMock,
18+
} = vi.hoisted(() => ({
19+
pushMock: vi.fn(),
20+
requestCubidIdMock: vi.fn<[], Promise<string>>(),
21+
ensureWalletMock: vi.fn<[], Promise<string>>(),
22+
upsertMyProfileMock: vi.fn(),
23+
uploadMock: vi.fn(),
24+
getPublicUrlMock: vi.fn(),
25+
createObjectURLMock: vi.fn(() => "blob:preview"),
26+
revokeObjectURLMock: vi.fn(),
27+
}));
28+
let originalFetch: typeof globalThis.fetch;
29+
let originalCreateObjectURL: typeof URL.createObjectURL | undefined;
30+
let originalRevokeObjectURL: typeof URL.revokeObjectURL | undefined;
31+
32+
vi.mock("next/navigation", () => ({
33+
useRouter: () => ({
34+
push: pushMock,
35+
}),
36+
}));
37+
38+
vi.mock("../src/lib/cubid", async () => {
39+
const actual = await vi.importActual<typeof import("../src/lib/cubid")>("../src/lib/cubid");
40+
return {
41+
...actual,
42+
requestCubidId: requestCubidIdMock,
43+
};
44+
});
45+
46+
vi.mock("../src/lib/onboarding", () => ({
47+
useRestrictToIncompleteOnboarding: () => ({
48+
ready: true,
49+
session: {
50+
user: { id: "user-1", email: "user@example.com" },
51+
} as unknown as Session,
52+
profile: { user_id: "user-1" },
53+
}),
54+
}));
55+
56+
vi.mock("../src/lib/profile", () => ({
57+
upsertMyProfile: (...args: unknown[]) => upsertMyProfileMock(...args),
58+
}));
59+
60+
vi.mock("../src/lib/wallet", () => ({
61+
ensureWallet: (...args: unknown[]) => ensureWalletMock(...args),
62+
}));
63+
64+
vi.mock("../src/lib/supabaseClient", () => ({
65+
getSupabaseClient: () => ({
66+
storage: {
67+
from: () => ({
68+
upload: uploadMock,
69+
getPublicUrl: getPublicUrlMock,
70+
}),
71+
},
72+
}),
73+
}));
74+
75+
describe("NewUserPage", () => {
76+
beforeEach(() => {
77+
pushMock.mockReset();
78+
requestCubidIdMock.mockResolvedValue("cubid_testabcd");
79+
ensureWalletMock.mockResolvedValue("0xwallet");
80+
upsertMyProfileMock.mockImplementation(async (payload) => ({
81+
user_id: "user-1",
82+
...payload,
83+
}));
84+
uploadMock.mockResolvedValue({ error: null });
85+
getPublicUrlMock.mockImplementation((path: string) => ({
86+
data: { publicUrl: `https://supabase.test/${path}` },
87+
}));
88+
89+
originalFetch = globalThis.fetch;
90+
globalThis.fetch = vi.fn() as unknown as typeof globalThis.fetch;
91+
92+
createObjectURLMock.mockReset();
93+
createObjectURLMock.mockReturnValue("blob:preview");
94+
revokeObjectURLMock.mockReset();
95+
const globalUrl = globalThis.URL as unknown as Record<string, unknown>;
96+
originalCreateObjectURL = globalUrl.createObjectURL as typeof URL.createObjectURL | undefined;
97+
originalRevokeObjectURL = globalUrl.revokeObjectURL as typeof URL.revokeObjectURL | undefined;
98+
globalUrl.createObjectURL = createObjectURLMock;
99+
globalUrl.revokeObjectURL = revokeObjectURLMock;
100+
101+
act(() => {
102+
useUserStore.getState().reset();
103+
});
104+
});
105+
106+
afterEach(() => {
107+
globalThis.fetch = originalFetch;
108+
const globalUrl = globalThis.URL as unknown as Record<string, unknown>;
109+
if (originalCreateObjectURL) {
110+
globalUrl.createObjectURL = originalCreateObjectURL;
111+
} else {
112+
delete globalUrl.createObjectURL;
113+
}
114+
if (originalRevokeObjectURL) {
115+
globalUrl.revokeObjectURL = originalRevokeObjectURL;
116+
} else {
117+
delete globalUrl.revokeObjectURL;
118+
}
119+
upsertMyProfileMock.mockReset();
120+
requestCubidIdMock.mockReset();
121+
ensureWalletMock.mockReset();
122+
uploadMock.mockReset();
123+
getPublicUrlMock.mockReset();
124+
pushMock.mockReset();
125+
act(() => {
126+
useUserStore.getState().reset();
127+
});
128+
});
129+
130+
it("guides the user through the onboarding steps", async () => {
131+
const user = userEvent.setup();
132+
133+
render(<NewUserPage />);
134+
135+
await waitFor(() => expect(requestCubidIdMock).toHaveBeenCalled());
136+
137+
const nameInput = screen.getByPlaceholderText("Casey Rivers");
138+
await user.type(nameInput, "Maple Leaf");
139+
140+
await user.click(screen.getByRole("button", { name: /next/i }));
141+
142+
await screen.findByText(/share a photo/i);
143+
144+
const fileInput = screen.getByLabelText(/upload a photo/i) as HTMLInputElement;
145+
const file = new File(["avatar"], "avatar.png", { type: "image/png" });
146+
await user.upload(fileInput, file);
147+
148+
await user.click(screen.getByRole("button", { name: /next/i }));
149+
150+
await waitFor(() => expect(uploadMock).toHaveBeenCalled());
151+
expect(uploadMock.mock.calls[0][0]).toMatch(/cubid_testabcd/);
152+
153+
await screen.findByText(/connect your wallet/i);
154+
155+
await user.click(screen.getByRole("button", { name: /connect wallet/i }));
156+
157+
await waitFor(() => expect(ensureWalletMock).toHaveBeenCalled());
158+
await waitFor(() => expect(upsertMyProfileMock).toHaveBeenCalledWith({ evm_address: "0xwallet" }));
159+
160+
const cubidInput = await screen.findByDisplayValue("cubid_testabcd");
161+
expect(cubidInput).toHaveAttribute("readonly");
162+
163+
await user.click(screen.getByRole("button", { name: /finish/i }));
164+
165+
await waitFor(() =>
166+
expect(upsertMyProfileMock).toHaveBeenCalledWith({
167+
cubid_id: "cubid_testabcd",
168+
display_name: "Maple Leaf",
169+
photo_url: expect.stringContaining("https://supabase.test/"),
170+
}),
171+
);
172+
await waitFor(() => expect(pushMock).toHaveBeenCalledWith("/circle"));
173+
});
174+
});

0 commit comments

Comments
 (0)