Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 56 additions & 2 deletions src/components/ProfileWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Check, Logout as LogoutIcon } from '@mui/icons-material';
import {
Alert,
Box,
Container,
Paper,
Expand All @@ -17,6 +18,7 @@ import {
} from '@mui/material';
import { signOut } from 'next-auth/react';
import { useEffect, useState } from 'react';
import type { FieldErrors } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form';
import { z } from 'zod';

Expand Down Expand Up @@ -136,6 +138,35 @@ interface ProfileWizardProps {
| undefined;
}

/**
* A friendly, field-aware explanation for why a full-profile submit was
* blocked. Prevents the "button looks active but does nothing" dead end by
* turning silent react-hook-form validation failures into visible guidance.
*/
export function profileSubmitBlockMessage(
errors: FieldErrors<ProfileFormData>
): string {
if (errors.profilePicture) {
return 'Please add a profile photo before you finish.';
}
if (errors.address || errors.parsedAddress) {
return 'Please pick your address from the suggestions so we can verify it.';
}
if (errors.name) {
return 'Please enter your name.';
}
if (
errors.agreedToHouseholdGoods ||
errors.agreedToTrustAndCare ||
errors.agreedToCommunityValues ||
errors.agreedToAgeRestrictions ||
errors.agreedToTerms
) {
return 'Please accept the community agreements to continue.';
}
return 'Please complete the highlighted fields to continue.';
}

export function ProfileWizard({
onComplete,
initialData,
Expand All @@ -144,6 +175,7 @@ export function ProfileWizard({
user,
}: ProfileWizardProps) {
const [activeStep, setActiveStep] = useState(initialStep ?? 0);
const [submitError, setSubmitError] = useState<string | null>(null);
const [profilePicturePreviewUrl, setProfilePicturePreviewUrl] = useState<
string | null
>(null);
Expand Down Expand Up @@ -251,11 +283,13 @@ export function ProfileWizard({
const isStepValid = await trigger(fieldsToValidate);

if (isStepValid) {
setSubmitError(null);
setActiveStep((prev) => prev + 1);
}
};

const handleBack = () => {
setSubmitError(null);
setActiveStep((prev) => prev - 1);
};

Expand All @@ -268,10 +302,14 @@ export function ProfileWizard({
!data.agreedToAgeRestrictions ||
!data.agreedToTerms
) {
// This shouldn't happen due to UI logic, but just in case
// The agreements live on Step 1; surface the reason rather than
// silently no-op'ing (which reads as a dead "Complete Profile" button).
setSubmitError('Please accept the community agreements to continue.');
return;
}

setSubmitError(null);

// Clear user-specific draft
const draftKey = user?.email
? `profile-wizard-draft-${user.email}`
Expand All @@ -283,6 +321,12 @@ export function ProfileWizard({
onComplete(data, 'full');
};

// react-hook-form calls this when zod validation blocks the submit. Without
// it the failure is silent and the button appears to do nothing.
const handleInvalid = (errors: FieldErrors<ProfileFormData>) => {
setSubmitError(profileSubmitBlockMessage(errors));
};

// Minimal entry: name + agreements only. Bypasses the full zod schema
// (which requires address + photo); UI gating in Step 1 enforces the
// agreements via canSubmitMinimal.
Expand Down Expand Up @@ -434,9 +478,19 @@ export function ProfileWizard({
<Box
component="form"
onSubmit={handleSubmit(
handleComplete as (data: ProfileFormData) => void
handleComplete as (data: ProfileFormData) => void,
handleInvalid
)}
>
{submitError && (
<Alert
severity="warning"
onClose={() => setSubmitError(null)}
sx={{ mb: 3, borderRadius: 2 }}
>
{submitError}
</Alert>
)}
{CurrentStepComponent && (
<CurrentStepComponent
onNext={handleNext}
Expand Down
31 changes: 31 additions & 0 deletions src/components/__tests__/profile-submit-block.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, it, expect } from 'vitest';

import { profileSubmitBlockMessage } from '../ProfileWizard';

describe('profileSubmitBlockMessage', () => {
it('points at the photo when profilePicture is missing', () => {
expect(
profileSubmitBlockMessage({
profilePicture: { type: 'required', message: 'x' },
})
).toMatch(/photo/i);
});

it('points at the address when it is not verified', () => {
expect(
profileSubmitBlockMessage({ address: { type: 'required', message: 'x' } })
).toMatch(/address/i);
});

it('points at the agreements when one is unchecked', () => {
expect(
profileSubmitBlockMessage({
agreedToTerms: { type: 'required', message: 'x' },
})
).toMatch(/agreement/i);
});

it('never returns an empty string, even with no known field', () => {
expect(profileSubmitBlockMessage({}).length).toBeGreaterThan(0);
});
});
Loading