Skip to content

feat: redesign new user onboarding flow#14

Merged
KazanderDad merged 2 commits intomainfrom
codex/implement-new-user-onboarding-flow
Nov 10, 2025
Merged

feat: redesign new user onboarding flow#14
KazanderDad merged 2 commits intomainfrom
codex/implement-new-user-onboarding-flow

Conversation

@KazanderDad
Copy link
Member

Summary

  • replace the /new-user form with a guided three-step flow that gathers the display name, captures a photo, and connects the wallet while showing the generated Cubid ID
  • upload profile photos to the Supabase "profile-pictures" bucket from either file uploads or image links and surface the resulting preview in the UI
  • add Vitest coverage for the onboarding journey and provide a jsqr stub/alias so tests and builds run without missing module errors

Testing

  • pnpm exec eslint src/app/new-user/page.tsx tests/new-user-page.test.tsx
  • pnpm --filter frontend test -- tests/new-user-page.test.tsx
  • pnpm --filter frontend build

Codex Task

Copilot AI review requested due to automatic review settings November 10, 2025 03:24
@vercel
Copy link

vercel bot commented Nov 10, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
peer-mapper-frontend Ready Ready Preview Comment Nov 10, 2025 5:39am

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 78 to 90
useEffect(() => {
if (!session?.user?.email) {
return;
}
if (profile?.cubid_id || hasRequestedCubidId.current) {
return;
}

hasRequestedCubidId.current = true;
requestCubidId(session.user.email)
.then((cubid) => {
setForm((prev) => ({ ...prev, cubidId: cubid }));
setStatus("Cubid ID prepared");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid generating Cubid ID before profile loads

The effect that auto-generates a Cubid ID runs as soon as a session email is present, regardless of whether the user profile has finished loading. AuthProvider sets session before completing the profile fetch, so there is a render where session exists but profile is still null. In that window the effect calls requestCubidId and later overwrites the form state with the newly generated ID (setForm…cubidId: cubid). If the subsequently fetched profile already has a cubid_id, the promise resolution replaces it and the user may submit with a different ID than the one stored on the server, unintentionally changing their identifier. The effect should wait until the profile is known to be missing an ID or cancel pending updates once the profile arrives.

Useful? React with 👍 / 👎.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the new user onboarding flow into a multi-step wizard (name → photo → wallet) with enhanced photo upload capabilities and automatic Cubid ID generation. The changes improve user experience by breaking down the onboarding process into digestible steps while adding support for both file uploads and URL-based photo imports.

Key Changes:

  • Implemented a 3-step onboarding wizard with step indicators and navigation
  • Added photo upload functionality supporting both file uploads and URL imports
  • Automated Cubid ID generation on component mount
  • Added test infrastructure for jsqr library mocking

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
frontend/src/app/new-user/page.tsx Major refactoring to implement multi-step onboarding flow with photo upload, URL cleanup, and automatic Cubid ID generation
frontend/__tests__/new-user-page.test.tsx New test file covering the happy path through all three onboarding steps
frontend/__mocks__/jsqr.ts Mock implementation for jsqr library to support testing
frontend/vitest.setup.ts Added jsqr mock setup using vi.mock()
frontend/vitest.config.ts Added alias configuration to redirect jsqr imports to mock file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

await uploadPhotoFromLinkOrFile();
setStep(2);
} catch (err) {
const message = err instanceof Error ? err.message : "We couldn't save your photo";
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error message should not contain HTML entities like '. These entities are meant for JSX rendering and will be displayed literally in error messages shown to users. Use a regular apostrophe instead: "We couldn't save your photo"

Suggested change
const message = err instanceof Error ? err.message : "We couldn't save your photo";
const message = err instanceof Error ? err.message : "We couldn't save your photo";

Copilot uses AI. Check for mistakes.
const extension = inferExtensionFromSource(photoLink, contentType);
const blob = await response.blob();
fileToUpload = new File([blob], `linked.${extension}`, { type: contentType });
updatePhotoPreview(URL.createObjectURL(blob), true);
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a preview object URL for the fetched blob is unnecessary here since the photo is being uploaded immediately afterward and won't be displayed before the upload completes. This creates an object URL that will be revoked shortly after, adding no value. Consider removing this line to simplify the code flow.

Suggested change
updatePhotoPreview(URL.createObjectURL(blob), true);

Copilot uses AI. Check for mistakes.
Comment on lines 257 to 263
<div className="flex gap-2 text-xs uppercase tracking-widest text-muted-foreground">
<span className={step === 0 ? "font-semibold text-blue-600" : ""}>Name</span>
<span>›</span>
<span className={step === 1 ? "font-semibold text-blue-600" : step > 1 ? "text-blue-600" : ""}>Photo</span>
<span>›</span>
<span className={step === 2 ? "font-semibold text-blue-600" : ""}>Wallet</span>
</div>
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The step indicator (Name › Photo › Wallet) lacks semantic structure and ARIA attributes for screen readers. Users with assistive technology won't know which step is current or how many total steps exist. Consider wrapping this in a <nav> element with aria-label="Onboarding progress" and adding aria-current="step" to the current step indicator.

Suggested change
<div className="flex gap-2 text-xs uppercase tracking-widest text-muted-foreground">
<span className={step === 0 ? "font-semibold text-blue-600" : ""}>Name</span>
<span></span>
<span className={step === 1 ? "font-semibold text-blue-600" : step > 1 ? "text-blue-600" : ""}>Photo</span>
<span></span>
<span className={step === 2 ? "font-semibold text-blue-600" : ""}>Wallet</span>
</div>
<nav aria-label="Onboarding progress">
<ol className="flex gap-2 text-xs uppercase tracking-widest text-muted-foreground list-none p-0 m-0">
<li>
<span
className={step === 0 ? "font-semibold text-blue-600" : ""}
{...(step === 0 ? { "aria-current": "step" } : {})}
>
Name
</span>
</li>
<li aria-hidden="true"></li>
<li>
<span
className={step === 1 ? "font-semibold text-blue-600" : step > 1 ? "text-blue-600" : ""}
{...(step === 1 ? { "aria-current": "step" } : {})}
>
Photo
</span>
</li>
<li aria-hidden="true"></li>
<li>
<span
className={step === 2 ? "font-semibold text-blue-600" : ""}
{...(step === 2 ? { "aria-current": "step" } : {})}
>
Wallet
</span>
</li>
</ol>
</nav>

Copilot uses AI. Check for mistakes.
<button
className="rounded-full border border-neutral-300 px-8 py-3 text-lg font-semibold transition hover:bg-neutral-100 dark:border-neutral-700 dark:hover:bg-neutral-900"
onClick={() => {
setStatus(null);
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When navigating back, only status is cleared but not error. This is inconsistent with the "Back" button on step 1 (line 332-334) which also only clears status. For better user experience, both error and status messages should be cleared when navigating between steps to avoid showing stale messages from previous steps. Consider adding setError(null) here and at line 333.

Suggested change
setStatus(null);
setStatus(null);
setError(null);

Copilot uses AI. Check for mistakes.
} else if (photoLink) {
const response = await fetch(photoLink);
if (!response.ok) {
throw new Error("We couldn&apos;t fetch that image link");
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error messages should not contain HTML entities like &apos;. These entities are meant for JSX rendering and will be displayed literally in error messages shown to users. Use regular apostrophes instead:

  • "We couldn't fetch that image link"
  • "We couldn't save your photo"
Suggested change
throw new Error("We couldn&apos;t fetch that image link");
throw new Error("We couldn't fetch that image link");

Copilot uses AI. Check for mistakes.
Comment on lines 4 to 7

vi.mock("jsqr", () => ({
default: vi.fn(),
}));
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The jsqr library is being mocked in two different ways: here in vitest.setup.ts using vi.mock() and in vitest.config.ts using an alias to __mocks__/jsqr.ts. This dual approach is redundant and could lead to confusion about which mock is being used. Consider removing one of these mocking strategies for clarity. Since the alias approach in vitest.config.ts points to an actual mock file, the vi.mock() call here may not be necessary.

Suggested change
vi.mock("jsqr", () => ({
default: vi.fn(),
}));

Copilot uses AI. Check for mistakes.
Comment on lines 104 to 106
const globalUrl = globalThis.URL as unknown as Record<string, unknown>;
globalUrl.createObjectURL = createObjectURLMock;
globalUrl.revokeObjectURL = revokeObjectURLMock;
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the afterEach hook, URL.createObjectURL and URL.revokeObjectURL are being set back to the mock functions instead of restoring them to their original values. This is likely incorrect - they should be restored to their original implementations before the test started. Consider storing the original implementations in beforeEach and restoring them in afterEach, similar to how originalFetch is handled.

Copilot uses AI. Check for mistakes.
if (urlExtension && /^[a-z0-9]+$/i.test(urlExtension)) {
return urlExtension;
}
const mimeExtension = contentType.split("/")[1];
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MIME type extension parsing is incomplete. For content types like image/jpeg, this will correctly extract jpeg, but for more complex MIME types with parameters (e.g., image/svg+xml; charset=utf-8), it will return svg+xml; charset=utf-8. Consider using contentType.split("/")[1]?.split(";")[0]?.trim() to properly extract the MIME subtype.

Suggested change
const mimeExtension = contentType.split("/")[1];
const mimeExtension = contentType.split("/")[1]?.split(";")[0]?.trim();

Copilot uses AI. Check for mistakes.
setUser(updated);
setWalletAddress(address);
setStatus("Wallet linked");
setStep(2);
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This setStep(2) call is redundant because the user is already on step 2 when they click the "Connect wallet" button. The wallet connection button is only visible when step === 2, so setting the step to 2 again has no effect. This line can be safely removed.

Suggested change
setStep(2);

Copilot uses AI. Check for mistakes.
@KazanderDad KazanderDad merged commit dead796 into main Nov 10, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant