feat: User onboarding finished
This commit is contained in:
@@ -9,11 +9,13 @@ import {
|
||||
SignupResponse,
|
||||
VerifyStatusResponse,
|
||||
ResendVerificationResponse,
|
||||
ResendVerificationPublicRequest,
|
||||
UserStatusResponse,
|
||||
} from '../types/auth.types';
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api';
|
||||
|
||||
// Create unauthenticated client for public signup endpoint
|
||||
// Create unauthenticated client for public endpoints (no JWT required)
|
||||
const unauthenticatedClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: 10000,
|
||||
@@ -37,4 +39,22 @@ export const authApi = {
|
||||
const response = await apiClient.post('/auth/resend-verification');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Resend verification email by email address (public, no auth required)
|
||||
* Used on the "Check Your Email" page before user can login
|
||||
*/
|
||||
resendVerificationPublic: async (data: ResendVerificationPublicRequest): Promise<ResendVerificationResponse> => {
|
||||
const response = await unauthenticatedClient.post('/auth/resend-verification-public', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get user status for routing decisions (requires auth)
|
||||
* Returns email verification and onboarding completion status
|
||||
*/
|
||||
getUserStatus: async (): Promise<UserStatusResponse> => {
|
||||
const response = await apiClient.get('/auth/user-status');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -52,3 +52,21 @@ export const useResendVerification = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Public resend verification - no authentication required
|
||||
* Used on the "Check Your Email" page before user can login
|
||||
*/
|
||||
export const useResendVerificationPublic = () => {
|
||||
return useMutation({
|
||||
mutationFn: (email: string) => authApi.resendVerificationPublic({ email }),
|
||||
onSuccess: (data) => {
|
||||
toast.success(data.message || 'If an account exists, a verification link will be sent.');
|
||||
},
|
||||
onError: (error: ApiError) => {
|
||||
// Always show success message for security (don't reveal if email exists)
|
||||
toast.success('If an account exists, a verification link will be sent.');
|
||||
console.error('Resend verification error:', error);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
86
frontend/src/features/auth/mobile/CallbackMobileScreen.tsx
Normal file
86
frontend/src/features/auth/mobile/CallbackMobileScreen.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @ai-summary Mobile Auth0 callback handler - routes users based on their status
|
||||
* @ai-context Fetches user status after Auth0 callback and redirects appropriately
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { MobileContainer } from '../../../shared-minimal/components/mobile/MobileContainer';
|
||||
import { useUserStatus } from '../../../core/auth/useUserStatus';
|
||||
|
||||
export const CallbackMobileScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { data: userStatus, isLoading, error } = useUserStatus();
|
||||
|
||||
// Get returnTo from location state (passed from Auth0Provider onRedirectCallback)
|
||||
const returnTo = (location.state as { returnTo?: string })?.returnTo || '/garage';
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
|
||||
if (error) {
|
||||
console.error('[CallbackMobileScreen] Error fetching user status:', error);
|
||||
// On error, redirect to garage and let normal auth flow handle it
|
||||
navigate('/garage', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (userStatus) {
|
||||
// Note: Unverified users should never reach this page if Auth0 action is configured
|
||||
// But as a safety check, redirect to verify-email if not verified
|
||||
if (!userStatus.emailVerified) {
|
||||
console.log('[CallbackMobileScreen] User not verified, redirecting to verify-email');
|
||||
navigate('/verify-email', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if onboarding is completed
|
||||
if (!userStatus.onboardingCompleted) {
|
||||
console.log('[CallbackMobileScreen] User not onboarded, redirecting to onboarding');
|
||||
navigate('/onboarding', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// User is verified and onboarded - go to requested destination
|
||||
console.log('[CallbackMobileScreen] User verified and onboarded, redirecting to:', returnTo);
|
||||
navigate(returnTo, { replace: true });
|
||||
}
|
||||
}, [userStatus, isLoading, error, navigate, returnTo]);
|
||||
|
||||
return (
|
||||
<MobileContainer>
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center px-4">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary-100 mb-4">
|
||||
<svg
|
||||
className="w-8 h-8 text-primary-600 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-slate-800 mb-2">
|
||||
Setting up your session...
|
||||
</h2>
|
||||
<p className="text-slate-600">Please wait while we prepare your garage.</p>
|
||||
</div>
|
||||
</div>
|
||||
</MobileContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallbackMobileScreen;
|
||||
@@ -17,7 +17,11 @@ export const SignupMobileScreen: React.FC = () => {
|
||||
const handleSubmit = (data: SignupRequest) => {
|
||||
signup(data, {
|
||||
onSuccess: () => {
|
||||
navigate('/verify-email');
|
||||
// Store email in localStorage for post-verification login_hint
|
||||
// (navigation state is lost when Auth0 redirects after verification)
|
||||
localStorage.setItem('pendingVerificationEmail', data.email);
|
||||
// Pass email to verify-email page for resend functionality
|
||||
navigate('/verify-email', { state: { email: data.email } });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,36 +1,115 @@
|
||||
/**
|
||||
* @ai-summary Mobile email verification screen with polling and resend
|
||||
* @ai-summary Mobile "Check Your Email" screen - handles both pre-verification and post-verification states
|
||||
* @ai-context No authentication required - auto-triggers login when verification success is detected
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useAuth0 } from '@auth0/auth0-react';
|
||||
import { MobileContainer } from '../../../shared-minimal/components/mobile/MobileContainer';
|
||||
import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard';
|
||||
import { Button } from '../../../shared-minimal/components/Button';
|
||||
import { useVerifyStatus, useResendVerification } from '../hooks/useVerifyStatus';
|
||||
import { useResendVerificationPublic } from '../hooks/useVerifyStatus';
|
||||
|
||||
export const VerifyEmailMobileScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { data: verifyStatus, isLoading } = useVerifyStatus({
|
||||
enablePolling: true,
|
||||
});
|
||||
const { mutate: resendVerification, isPending: isResending } = useResendVerification();
|
||||
const location = useLocation();
|
||||
const { loginWithRedirect, isAuthenticated, isLoading: isAuthLoading } = useAuth0();
|
||||
const { mutate: resendVerification, isPending: isResending } = useResendVerificationPublic();
|
||||
|
||||
// Track if auto-login is in progress
|
||||
const [isAutoLoginTriggered, setIsAutoLoginTriggered] = useState(false);
|
||||
|
||||
// Get email from navigation state (passed from signup page) or localStorage
|
||||
// localStorage is used when Auth0 redirects after verification (state is lost)
|
||||
const stateEmail = (location.state as { email?: string })?.email || '';
|
||||
const storedEmail = localStorage.getItem('pendingVerificationEmail') || '';
|
||||
const email = stateEmail || storedEmail;
|
||||
|
||||
// Parse URL search params for verification success detection
|
||||
// Auth0 redirects with ?message=Your%20email%20was%20verified... on success
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const message = searchParams.get('message');
|
||||
const verificationSuccessful = message && message.toLowerCase().includes('verified');
|
||||
|
||||
// Auto-login effect: triggers when verification success is detected
|
||||
useEffect(() => {
|
||||
if (verifyStatus?.emailVerified) {
|
||||
navigate('/onboarding');
|
||||
if (isAuthLoading) return;
|
||||
if (isAuthenticated) return;
|
||||
|
||||
if (verificationSuccessful && !isAutoLoginTriggered) {
|
||||
setIsAutoLoginTriggered(true);
|
||||
console.log('[VerifyEmailMobileScreen] Verification success detected, triggering auto-login with email:', email);
|
||||
|
||||
// Clear the stored email after successful verification
|
||||
localStorage.removeItem('pendingVerificationEmail');
|
||||
|
||||
// Redirect to Auth0 login with email pre-filled
|
||||
loginWithRedirect({
|
||||
appState: { returnTo: '/callback' },
|
||||
authorizationParams: {
|
||||
login_hint: email || undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [verifyStatus, navigate]);
|
||||
}, [verificationSuccessful, isAuthenticated, isAuthLoading, isAutoLoginTriggered, loginWithRedirect, email]);
|
||||
|
||||
const handleResend = () => {
|
||||
resendVerification();
|
||||
if (email) {
|
||||
resendVerification(email);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
const handleBackToLogin = () => {
|
||||
loginWithRedirect();
|
||||
};
|
||||
|
||||
// Show loading state when auto-login is in progress
|
||||
if (isAutoLoginTriggered || (verificationSuccessful && !isAuthLoading)) {
|
||||
return (
|
||||
<MobileContainer>
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-lg text-slate-600">Loading...</div>
|
||||
<div className="text-center px-4">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-green-100 mb-4">
|
||||
<svg
|
||||
className="w-8 h-8 text-green-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-slate-800 mb-2">
|
||||
Email Verified!
|
||||
</h2>
|
||||
<p className="text-slate-600 mb-4">Logging you in automatically...</p>
|
||||
<div className="inline-flex items-center justify-center w-8 h-8">
|
||||
<svg
|
||||
className="w-8 h-8 text-primary-600 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MobileContainer>
|
||||
);
|
||||
@@ -59,28 +138,42 @@ export const VerifyEmailMobileScreen: React.FC = () => {
|
||||
<p className="text-slate-600">
|
||||
We've sent a verification link to
|
||||
</p>
|
||||
<p className="text-primary-600 font-medium mt-1 break-words px-4">
|
||||
{verifyStatus?.email}
|
||||
</p>
|
||||
{email && (
|
||||
<p className="text-primary-600 font-medium mt-1 break-words px-4">
|
||||
{email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<GlassCard>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-slate-50 rounded-lg p-4 text-sm text-slate-700">
|
||||
<p className="mb-2">Click the link in the email to verify your account.</p>
|
||||
<p>Once verified, you'll be automatically redirected to complete your profile.</p>
|
||||
<p>Once verified, you can log in to complete your profile setup.</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-slate-600 mb-3">Didn't receive the email?</p>
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
onClick={handleResend}
|
||||
loading={isResending}
|
||||
variant="secondary"
|
||||
onClick={handleBackToLogin}
|
||||
variant="primary"
|
||||
className="w-full min-h-[44px]"
|
||||
>
|
||||
Resend Verification Email
|
||||
Back to Login
|
||||
</Button>
|
||||
|
||||
{email && (
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-slate-600 mb-2">Didn't receive the email?</p>
|
||||
<Button
|
||||
onClick={handleResend}
|
||||
loading={isResending}
|
||||
variant="secondary"
|
||||
className="w-full min-h-[44px]"
|
||||
>
|
||||
Resend Verification Email
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
83
frontend/src/features/auth/pages/CallbackPage.tsx
Normal file
83
frontend/src/features/auth/pages/CallbackPage.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @ai-summary Auth0 callback handler page - routes users based on their status
|
||||
* @ai-context Fetches user status after Auth0 callback and redirects appropriately
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useUserStatus } from '../../../core/auth/useUserStatus';
|
||||
|
||||
export const CallbackPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { data: userStatus, isLoading, error } = useUserStatus();
|
||||
|
||||
// Get returnTo from location state (passed from Auth0Provider onRedirectCallback)
|
||||
const returnTo = (location.state as { returnTo?: string })?.returnTo || '/garage';
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
|
||||
if (error) {
|
||||
console.error('[CallbackPage] Error fetching user status:', error);
|
||||
// On error, redirect to garage and let normal auth flow handle it
|
||||
navigate('/garage', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (userStatus) {
|
||||
// Note: Unverified users should never reach this page if Auth0 action is configured
|
||||
// But as a safety check, redirect to verify-email if not verified
|
||||
if (!userStatus.emailVerified) {
|
||||
console.log('[CallbackPage] User not verified, redirecting to verify-email');
|
||||
navigate('/verify-email', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if onboarding is completed
|
||||
if (!userStatus.onboardingCompleted) {
|
||||
console.log('[CallbackPage] User not onboarded, redirecting to onboarding');
|
||||
navigate('/onboarding', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// User is verified and onboarded - go to requested destination
|
||||
console.log('[CallbackPage] User verified and onboarded, redirecting to:', returnTo);
|
||||
navigate(returnTo, { replace: true });
|
||||
}
|
||||
}, [userStatus, isLoading, error, navigate, returnTo]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-rose-50 flex items-center justify-center p-4">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary-100 mb-4">
|
||||
<svg
|
||||
className="w-8 h-8 text-primary-600 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-slate-800 mb-2">
|
||||
Setting up your session...
|
||||
</h2>
|
||||
<p className="text-slate-600">Please wait while we prepare your garage.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallbackPage;
|
||||
@@ -15,7 +15,11 @@ export const SignupPage: React.FC = () => {
|
||||
const handleSubmit = (data: SignupRequest) => {
|
||||
signup(data, {
|
||||
onSuccess: () => {
|
||||
navigate('/verify-email');
|
||||
// Store email in localStorage for post-verification login_hint
|
||||
// (navigation state is lost when Auth0 redirects after verification)
|
||||
localStorage.setItem('pendingVerificationEmail', data.email);
|
||||
// Pass email to verify-email page for resend functionality
|
||||
navigate('/verify-email', { state: { email: data.email } });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,33 +1,112 @@
|
||||
/**
|
||||
* @ai-summary Desktop email verification page with polling and resend functionality
|
||||
* @ai-summary Desktop "Check Your Email" page - handles both pre-verification and post-verification states
|
||||
* @ai-context No authentication required - auto-triggers login when verification success is detected
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useVerifyStatus, useResendVerification } from '../hooks/useVerifyStatus';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useAuth0 } from '@auth0/auth0-react';
|
||||
import { useResendVerificationPublic } from '../hooks/useVerifyStatus';
|
||||
import { Button } from '../../../shared-minimal/components/Button';
|
||||
|
||||
export const VerifyEmailPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { data: verifyStatus, isLoading } = useVerifyStatus({
|
||||
enablePolling: true,
|
||||
});
|
||||
const { mutate: resendVerification, isPending: isResending } = useResendVerification();
|
||||
const location = useLocation();
|
||||
const { loginWithRedirect, isAuthenticated, isLoading: isAuthLoading } = useAuth0();
|
||||
const { mutate: resendVerification, isPending: isResending } = useResendVerificationPublic();
|
||||
|
||||
// Track if auto-login is in progress
|
||||
const [isAutoLoginTriggered, setIsAutoLoginTriggered] = useState(false);
|
||||
|
||||
// Get email from navigation state (passed from signup page) or localStorage
|
||||
// localStorage is used when Auth0 redirects after verification (state is lost)
|
||||
const stateEmail = (location.state as { email?: string })?.email || '';
|
||||
const storedEmail = localStorage.getItem('pendingVerificationEmail') || '';
|
||||
const email = stateEmail || storedEmail;
|
||||
|
||||
// Parse URL search params for verification success detection
|
||||
// Auth0 redirects with ?message=Your%20email%20was%20verified... on success
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const message = searchParams.get('message');
|
||||
const verificationSuccessful = message && message.toLowerCase().includes('verified');
|
||||
|
||||
// Auto-login effect: triggers when verification success is detected
|
||||
useEffect(() => {
|
||||
if (verifyStatus?.emailVerified) {
|
||||
navigate('/onboarding');
|
||||
if (isAuthLoading) return;
|
||||
if (isAuthenticated) return;
|
||||
|
||||
if (verificationSuccessful && !isAutoLoginTriggered) {
|
||||
setIsAutoLoginTriggered(true);
|
||||
console.log('[VerifyEmailPage] Verification success detected, triggering auto-login with email:', email);
|
||||
|
||||
// Clear the stored email after successful verification
|
||||
localStorage.removeItem('pendingVerificationEmail');
|
||||
|
||||
// Redirect to Auth0 login with email pre-filled
|
||||
loginWithRedirect({
|
||||
appState: { returnTo: '/callback' },
|
||||
authorizationParams: {
|
||||
login_hint: email || undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [verifyStatus, navigate]);
|
||||
}, [verificationSuccessful, isAuthenticated, isAuthLoading, isAutoLoginTriggered, loginWithRedirect, email]);
|
||||
|
||||
const handleResend = () => {
|
||||
resendVerification();
|
||||
if (email) {
|
||||
resendVerification(email);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
const handleBackToLogin = () => {
|
||||
loginWithRedirect();
|
||||
};
|
||||
|
||||
// Show loading state when auto-login is in progress
|
||||
if (isAutoLoginTriggered || (verificationSuccessful && !isAuthLoading)) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-rose-50 flex items-center justify-center p-4">
|
||||
<div className="text-lg text-gray-600">Loading...</div>
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-green-100 mb-4">
|
||||
<svg
|
||||
className="w-8 h-8 text-green-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-slate-800 mb-2">
|
||||
Email Verified!
|
||||
</h2>
|
||||
<p className="text-slate-600 mb-4">Logging you in automatically...</p>
|
||||
<div className="inline-flex items-center justify-center w-8 h-8">
|
||||
<svg
|
||||
className="w-8 h-8 text-primary-600 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -56,27 +135,41 @@ export const VerifyEmailPage: React.FC = () => {
|
||||
<p className="text-gray-600">
|
||||
We've sent a verification link to
|
||||
</p>
|
||||
<p className="text-primary-600 font-medium mt-1">
|
||||
{verifyStatus?.email}
|
||||
</p>
|
||||
{email && (
|
||||
<p className="text-primary-600 font-medium mt-1">
|
||||
{email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-slate-50 rounded-lg p-4 text-sm text-gray-700">
|
||||
<p className="mb-2">Click the link in the email to verify your account.</p>
|
||||
<p>Once verified, you'll be automatically redirected to complete your profile.</p>
|
||||
<p>Once verified, you can log in to complete your profile setup.</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-600 mb-3">Didn't receive the email?</p>
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
onClick={handleResend}
|
||||
loading={isResending}
|
||||
variant="secondary"
|
||||
onClick={handleBackToLogin}
|
||||
variant="primary"
|
||||
className="w-full min-h-[44px]"
|
||||
>
|
||||
Resend Verification Email
|
||||
Back to Login
|
||||
</Button>
|
||||
|
||||
{email && (
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-600 mb-2">Didn't receive the email?</p>
|
||||
<Button
|
||||
onClick={handleResend}
|
||||
loading={isResending}
|
||||
variant="secondary"
|
||||
className="w-full min-h-[44px]"
|
||||
>
|
||||
Resend Verification Email
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -21,3 +21,13 @@ export interface VerifyStatusResponse {
|
||||
export interface ResendVerificationResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ResendVerificationPublicRequest {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface UserStatusResponse {
|
||||
emailVerified: boolean;
|
||||
onboardingCompleted: boolean;
|
||||
email: string;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export const OnboardingMobileScreen: React.FC = () => {
|
||||
const handleComplete = async () => {
|
||||
try {
|
||||
await completeOnboarding.mutateAsync();
|
||||
navigate('/vehicles');
|
||||
navigate('/garage');
|
||||
} catch (error) {
|
||||
// Error is handled by the mutation hook
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export const OnboardingPage: React.FC = () => {
|
||||
const handleComplete = async () => {
|
||||
try {
|
||||
await completeOnboarding.mutateAsync();
|
||||
navigate('/vehicles');
|
||||
navigate('/garage');
|
||||
} catch (error) {
|
||||
// Error is handled by the mutation hook
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user