feat: User onboarding finished
This commit is contained in:
@@ -8,7 +8,7 @@ import { AuthService } from '../domain/auth.service';
|
|||||||
import { UserProfileRepository } from '../../user-profile/data/user-profile.repository';
|
import { UserProfileRepository } from '../../user-profile/data/user-profile.repository';
|
||||||
import { pool } from '../../../core/config/database';
|
import { pool } from '../../../core/config/database';
|
||||||
import { logger } from '../../../core/logging/logger';
|
import { logger } from '../../../core/logging/logger';
|
||||||
import { signupSchema } from './auth.validation';
|
import { signupSchema, resendVerificationPublicSchema } from './auth.validation';
|
||||||
|
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
private authService: AuthService;
|
private authService: AuthService;
|
||||||
@@ -119,4 +119,67 @@ export class AuthController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/auth/resend-verification-public
|
||||||
|
* Resend verification email by email address
|
||||||
|
* Public endpoint - no JWT required (for pre-login verification page)
|
||||||
|
*/
|
||||||
|
async resendVerificationPublic(request: FastifyRequest, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const validation = resendVerificationPublicSchema.safeParse(request.body);
|
||||||
|
if (!validation.success) {
|
||||||
|
return reply.code(400).send({
|
||||||
|
error: 'Validation error',
|
||||||
|
message: validation.error.errors[0]?.message || 'Invalid input',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { email } = validation.data;
|
||||||
|
|
||||||
|
const result = await this.authService.resendVerificationByEmail(email);
|
||||||
|
|
||||||
|
logger.info('Public resend verification requested', { email: email.substring(0, 3) + '***' });
|
||||||
|
|
||||||
|
return reply.code(200).send(result);
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Failed to resend verification email (public)', { error });
|
||||||
|
|
||||||
|
// Always return success for security (don't reveal if email exists)
|
||||||
|
return reply.code(200).send({
|
||||||
|
message: 'If an account exists with this email, a verification link will be sent.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/auth/user-status
|
||||||
|
* Get user status for routing decisions
|
||||||
|
* Protected endpoint - requires JWT
|
||||||
|
*/
|
||||||
|
async getUserStatus(request: FastifyRequest, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const userId = (request as any).user.sub;
|
||||||
|
|
||||||
|
const result = await this.authService.getUserStatus(userId);
|
||||||
|
|
||||||
|
logger.info('User status retrieved', {
|
||||||
|
userId: userId.substring(0, 8) + '...',
|
||||||
|
emailVerified: result.emailVerified,
|
||||||
|
onboardingCompleted: result.onboardingCompleted,
|
||||||
|
});
|
||||||
|
|
||||||
|
return reply.code(200).send(result);
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Failed to get user status', {
|
||||||
|
error,
|
||||||
|
userId: (request as any).user?.sub,
|
||||||
|
});
|
||||||
|
|
||||||
|
return reply.code(500).send({
|
||||||
|
error: 'Internal server error',
|
||||||
|
message: 'Failed to get user status',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,4 +27,13 @@ export const authRoutes: FastifyPluginAsync = async (
|
|||||||
preHandler: [fastify.authenticate],
|
preHandler: [fastify.authenticate],
|
||||||
handler: authController.resendVerification.bind(authController),
|
handler: authController.resendVerification.bind(authController),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// POST /api/auth/resend-verification-public - Resend verification by email (public, no JWT)
|
||||||
|
fastify.post('/auth/resend-verification-public', authController.resendVerificationPublic.bind(authController));
|
||||||
|
|
||||||
|
// GET /api/auth/user-status - Get user status for routing (requires JWT, verification exempt)
|
||||||
|
fastify.get('/auth/user-status', {
|
||||||
|
preHandler: [fastify.authenticate],
|
||||||
|
handler: authController.getUserStatus.bind(authController),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,3 +21,10 @@ export const signupSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type SignupInput = z.infer<typeof signupSchema>;
|
export type SignupInput = z.infer<typeof signupSchema>;
|
||||||
|
|
||||||
|
// Schema for public resend verification endpoint (no JWT required)
|
||||||
|
export const resendVerificationPublicSchema = z.object({
|
||||||
|
email: z.string().email('Invalid email format'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ResendVerificationPublicInput = z.infer<typeof resendVerificationPublicSchema>;
|
||||||
|
|||||||
@@ -127,4 +127,87 @@ export class AuthService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resend verification email by email address (public endpoint)
|
||||||
|
* Looks up user by email and sends verification if not verified
|
||||||
|
*/
|
||||||
|
async resendVerificationByEmail(email: string): Promise<ResendVerificationResponse> {
|
||||||
|
try {
|
||||||
|
// Look up user by email in our database
|
||||||
|
const userProfile = await this.userProfileRepository.getByEmail(email);
|
||||||
|
|
||||||
|
if (!userProfile) {
|
||||||
|
// Don't reveal if email exists - return success message regardless
|
||||||
|
logger.info('Resend verification requested for unknown email', {
|
||||||
|
email: email.substring(0, 3) + '***',
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
message: 'If an account exists with this email, a verification link will be sent.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already verified via Auth0
|
||||||
|
const verified = await auth0ManagementClient.checkEmailVerified(userProfile.auth0Sub);
|
||||||
|
|
||||||
|
if (verified) {
|
||||||
|
logger.info('Email already verified, skipping resend', { email: email.substring(0, 3) + '***' });
|
||||||
|
return {
|
||||||
|
message: 'If an account exists with this email, a verification link will be sent.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request Auth0 to resend verification email
|
||||||
|
await auth0ManagementClient.resendVerificationEmail(userProfile.auth0Sub);
|
||||||
|
|
||||||
|
logger.info('Verification email resent via public endpoint', {
|
||||||
|
email: email.substring(0, 3) + '***',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'If an account exists with this email, a verification link will be sent.',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to resend verification email by email', {
|
||||||
|
email: email.substring(0, 3) + '***',
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
// Don't reveal error details - return generic success for security
|
||||||
|
return {
|
||||||
|
message: 'If an account exists with this email, a verification link will be sent.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user status for routing decisions
|
||||||
|
* Returns email verification and onboarding completion status
|
||||||
|
*/
|
||||||
|
async getUserStatus(auth0Sub: string): Promise<{
|
||||||
|
emailVerified: boolean;
|
||||||
|
onboardingCompleted: boolean;
|
||||||
|
email: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
// Get user details from Auth0
|
||||||
|
const auth0User = await auth0ManagementClient.getUser(auth0Sub);
|
||||||
|
|
||||||
|
// Get local profile for onboarding status
|
||||||
|
const localProfile = await this.userProfileRepository.getByAuth0Sub(auth0Sub);
|
||||||
|
|
||||||
|
// Sync email verification status if needed
|
||||||
|
if (localProfile && localProfile.emailVerified !== auth0User.emailVerified) {
|
||||||
|
await this.userProfileRepository.updateEmailVerified(auth0Sub, auth0User.emailVerified);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
emailVerified: auth0User.emailVerified,
|
||||||
|
onboardingCompleted: localProfile?.onboardingCompletedAt !== null,
|
||||||
|
email: auth0User.email,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get user status', { auth0Sub, error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,26 @@ export class UserProfileRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getByEmail(email: string): Promise<UserProfile | null> {
|
||||||
|
const query = `
|
||||||
|
SELECT ${USER_PROFILE_COLUMNS}
|
||||||
|
FROM user_profiles
|
||||||
|
WHERE email = $1
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.pool.query(query, [email]);
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.mapRowToUserProfile(result.rows[0]);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error fetching user profile by email', { error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
auth0Sub: string,
|
auth0Sub: string,
|
||||||
email: string,
|
email: string,
|
||||||
|
|||||||
@@ -47,8 +47,10 @@ const AdminCommunityStationsMobileScreen = lazy(() => import('./features/admin/m
|
|||||||
// Auth pages (lazy-loaded)
|
// Auth pages (lazy-loaded)
|
||||||
const SignupPage = lazy(() => import('./features/auth/pages/SignupPage').then(m => ({ default: m.SignupPage })));
|
const SignupPage = lazy(() => import('./features/auth/pages/SignupPage').then(m => ({ default: m.SignupPage })));
|
||||||
const VerifyEmailPage = lazy(() => import('./features/auth/pages/VerifyEmailPage').then(m => ({ default: m.VerifyEmailPage })));
|
const VerifyEmailPage = lazy(() => import('./features/auth/pages/VerifyEmailPage').then(m => ({ default: m.VerifyEmailPage })));
|
||||||
|
const CallbackPage = lazy(() => import('./features/auth/pages/CallbackPage').then(m => ({ default: m.CallbackPage })));
|
||||||
const SignupMobileScreen = lazy(() => import('./features/auth/mobile/SignupMobileScreen').then(m => ({ default: m.SignupMobileScreen })));
|
const SignupMobileScreen = lazy(() => import('./features/auth/mobile/SignupMobileScreen').then(m => ({ default: m.SignupMobileScreen })));
|
||||||
const VerifyEmailMobileScreen = lazy(() => import('./features/auth/mobile/VerifyEmailMobileScreen').then(m => ({ default: m.VerifyEmailMobileScreen })));
|
const VerifyEmailMobileScreen = lazy(() => import('./features/auth/mobile/VerifyEmailMobileScreen').then(m => ({ default: m.VerifyEmailMobileScreen })));
|
||||||
|
const CallbackMobileScreen = lazy(() => import('./features/auth/mobile/CallbackMobileScreen').then(m => ({ default: m.CallbackMobileScreen })));
|
||||||
|
|
||||||
// Onboarding pages (lazy-loaded)
|
// Onboarding pages (lazy-loaded)
|
||||||
const OnboardingPage = lazy(() => import('./features/onboarding/pages/OnboardingPage').then(m => ({ default: m.OnboardingPage })));
|
const OnboardingPage = lazy(() => import('./features/onboarding/pages/OnboardingPage').then(m => ({ default: m.OnboardingPage })));
|
||||||
@@ -469,13 +471,19 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCallbackRoute) {
|
// Callback route requires authentication - handled by CallbackPage component
|
||||||
|
if (isCallbackRoute && isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={md3Theme}>
|
<ThemeProvider theme={md3Theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
|
<React.Suspense fallback={
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="text-lg">Processing login...</div>
|
<div className="text-lg">Processing login...</div>
|
||||||
</div>
|
</div>
|
||||||
|
}>
|
||||||
|
{mobileMode ? <CallbackMobileScreen /> : <CallbackPage />}
|
||||||
|
</React.Suspense>
|
||||||
|
<DebugInfo />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -507,11 +515,8 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
// Verify email is public - shown after signup before user can login
|
||||||
return <Navigate to="/" replace />;
|
// (Auth0 blocks unverified users from logging in)
|
||||||
}
|
|
||||||
|
|
||||||
// Verify email and onboarding routes require authentication but not full initialization
|
|
||||||
if (isVerifyEmailRoute) {
|
if (isVerifyEmailRoute) {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={md3Theme}>
|
<ThemeProvider theme={md3Theme}>
|
||||||
@@ -528,6 +533,10 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Navigate to="/" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
if (isOnboardingRoute) {
|
if (isOnboardingRoute) {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={md3Theme}>
|
<ThemeProvider theme={md3Theme}>
|
||||||
|
|||||||
@@ -26,8 +26,12 @@ export const Auth0Provider: React.FC<Auth0ProviderProps> = ({ children }) => {
|
|||||||
|
|
||||||
const onRedirectCallback = (appState?: { returnTo?: string }) => {
|
const onRedirectCallback = (appState?: { returnTo?: string }) => {
|
||||||
console.log('[Auth0Provider] Redirect callback triggered', { appState, returnTo: appState?.returnTo });
|
console.log('[Auth0Provider] Redirect callback triggered', { appState, returnTo: appState?.returnTo });
|
||||||
const target = appState?.returnTo || '/garage';
|
// Route to callback page which will check user status and redirect appropriately
|
||||||
navigate(target, { replace: true });
|
// Pass the intended destination as state for after status check
|
||||||
|
navigate('/callback', {
|
||||||
|
replace: true,
|
||||||
|
state: { returnTo: appState?.returnTo || '/garage' },
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
25
frontend/src/core/auth/useUserStatus.ts
Normal file
25
frontend/src/core/auth/useUserStatus.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* @ai-summary React Query hook for user status (email verification + onboarding)
|
||||||
|
* @ai-context Used by CallbackPage to determine routing after Auth0 callback
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useAuth0 } from '@auth0/auth0-react';
|
||||||
|
import { authApi } from '../../features/auth/api/auth.api';
|
||||||
|
import { UserStatusResponse } from '../../features/auth/types/auth.types';
|
||||||
|
|
||||||
|
interface UseUserStatusOptions {
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserStatus = (options?: UseUserStatusOptions) => {
|
||||||
|
const { isAuthenticated, isLoading: isAuthLoading } = useAuth0();
|
||||||
|
|
||||||
|
return useQuery<UserStatusResponse>({
|
||||||
|
queryKey: ['userStatus'],
|
||||||
|
queryFn: authApi.getUserStatus,
|
||||||
|
enabled: (options?.enabled !== false) && isAuthenticated && !isAuthLoading,
|
||||||
|
retry: 2,
|
||||||
|
staleTime: 30000, // Cache for 30 seconds
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -9,11 +9,13 @@ import {
|
|||||||
SignupResponse,
|
SignupResponse,
|
||||||
VerifyStatusResponse,
|
VerifyStatusResponse,
|
||||||
ResendVerificationResponse,
|
ResendVerificationResponse,
|
||||||
|
ResendVerificationPublicRequest,
|
||||||
|
UserStatusResponse,
|
||||||
} from '../types/auth.types';
|
} from '../types/auth.types';
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api';
|
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({
|
const unauthenticatedClient = axios.create({
|
||||||
baseURL: API_BASE_URL,
|
baseURL: API_BASE_URL,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
@@ -37,4 +39,22 @@ export const authApi = {
|
|||||||
const response = await apiClient.post('/auth/resend-verification');
|
const response = await apiClient.post('/auth/resend-verification');
|
||||||
return response.data;
|
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) => {
|
const handleSubmit = (data: SignupRequest) => {
|
||||||
signup(data, {
|
signup(data, {
|
||||||
onSuccess: () => {
|
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 React, { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { useAuth0 } from '@auth0/auth0-react';
|
||||||
import { MobileContainer } from '../../../shared-minimal/components/mobile/MobileContainer';
|
import { MobileContainer } from '../../../shared-minimal/components/mobile/MobileContainer';
|
||||||
import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard';
|
import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard';
|
||||||
import { Button } from '../../../shared-minimal/components/Button';
|
import { Button } from '../../../shared-minimal/components/Button';
|
||||||
import { useVerifyStatus, useResendVerification } from '../hooks/useVerifyStatus';
|
import { useResendVerificationPublic } from '../hooks/useVerifyStatus';
|
||||||
|
|
||||||
export const VerifyEmailMobileScreen: React.FC = () => {
|
export const VerifyEmailMobileScreen: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const location = useLocation();
|
||||||
const { data: verifyStatus, isLoading } = useVerifyStatus({
|
const { loginWithRedirect, isAuthenticated, isLoading: isAuthLoading } = useAuth0();
|
||||||
enablePolling: true,
|
const { mutate: resendVerification, isPending: isResending } = useResendVerificationPublic();
|
||||||
});
|
|
||||||
const { mutate: resendVerification, isPending: isResending } = useResendVerification();
|
|
||||||
|
|
||||||
|
// 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(() => {
|
useEffect(() => {
|
||||||
if (verifyStatus?.emailVerified) {
|
if (isAuthLoading) return;
|
||||||
navigate('/onboarding');
|
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 = () => {
|
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 (
|
return (
|
||||||
<MobileContainer>
|
<MobileContainer>
|
||||||
<div className="flex-1 flex items-center justify-center">
|
<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>
|
</div>
|
||||||
</MobileContainer>
|
</MobileContainer>
|
||||||
);
|
);
|
||||||
@@ -59,20 +138,32 @@ export const VerifyEmailMobileScreen: React.FC = () => {
|
|||||||
<p className="text-slate-600">
|
<p className="text-slate-600">
|
||||||
We've sent a verification link to
|
We've sent a verification link to
|
||||||
</p>
|
</p>
|
||||||
|
{email && (
|
||||||
<p className="text-primary-600 font-medium mt-1 break-words px-4">
|
<p className="text-primary-600 font-medium mt-1 break-words px-4">
|
||||||
{verifyStatus?.email}
|
{email}
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GlassCard>
|
<GlassCard>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="bg-slate-50 rounded-lg p-4 text-sm text-slate-700">
|
<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 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>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Button
|
||||||
|
onClick={handleBackToLogin}
|
||||||
|
variant="primary"
|
||||||
|
className="w-full min-h-[44px]"
|
||||||
|
>
|
||||||
|
Back to Login
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{email && (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-slate-600 mb-3">Didn't receive the email?</p>
|
<p className="text-sm text-slate-600 mb-2">Didn't receive the email?</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleResend}
|
onClick={handleResend}
|
||||||
loading={isResending}
|
loading={isResending}
|
||||||
@@ -82,6 +173,8 @@ export const VerifyEmailMobileScreen: React.FC = () => {
|
|||||||
Resend Verification Email
|
Resend Verification Email
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</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) => {
|
const handleSubmit = (data: SignupRequest) => {
|
||||||
signup(data, {
|
signup(data, {
|
||||||
onSuccess: () => {
|
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 React, { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useVerifyStatus, useResendVerification } from '../hooks/useVerifyStatus';
|
import { useAuth0 } from '@auth0/auth0-react';
|
||||||
|
import { useResendVerificationPublic } from '../hooks/useVerifyStatus';
|
||||||
import { Button } from '../../../shared-minimal/components/Button';
|
import { Button } from '../../../shared-minimal/components/Button';
|
||||||
|
|
||||||
export const VerifyEmailPage: React.FC = () => {
|
export const VerifyEmailPage: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const location = useLocation();
|
||||||
const { data: verifyStatus, isLoading } = useVerifyStatus({
|
const { loginWithRedirect, isAuthenticated, isLoading: isAuthLoading } = useAuth0();
|
||||||
enablePolling: true,
|
const { mutate: resendVerification, isPending: isResending } = useResendVerificationPublic();
|
||||||
});
|
|
||||||
const { mutate: resendVerification, isPending: isResending } = useResendVerification();
|
|
||||||
|
|
||||||
|
// 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(() => {
|
useEffect(() => {
|
||||||
if (verifyStatus?.emailVerified) {
|
if (isAuthLoading) return;
|
||||||
navigate('/onboarding');
|
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 = () => {
|
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 (
|
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="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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -56,19 +135,31 @@ export const VerifyEmailPage: React.FC = () => {
|
|||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
We've sent a verification link to
|
We've sent a verification link to
|
||||||
</p>
|
</p>
|
||||||
|
{email && (
|
||||||
<p className="text-primary-600 font-medium mt-1">
|
<p className="text-primary-600 font-medium mt-1">
|
||||||
{verifyStatus?.email}
|
{email}
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="bg-slate-50 rounded-lg p-4 text-sm text-gray-700">
|
<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 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>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Button
|
||||||
|
onClick={handleBackToLogin}
|
||||||
|
variant="primary"
|
||||||
|
className="w-full min-h-[44px]"
|
||||||
|
>
|
||||||
|
Back to Login
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{email && (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-gray-600 mb-3">Didn't receive the email?</p>
|
<p className="text-sm text-gray-600 mb-2">Didn't receive the email?</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleResend}
|
onClick={handleResend}
|
||||||
loading={isResending}
|
loading={isResending}
|
||||||
@@ -78,6 +169,8 @@ export const VerifyEmailPage: React.FC = () => {
|
|||||||
Resend Verification Email
|
Resend Verification Email
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 text-center text-sm text-gray-500">
|
<div className="mt-6 text-center text-sm text-gray-500">
|
||||||
|
|||||||
@@ -21,3 +21,13 @@ export interface VerifyStatusResponse {
|
|||||||
export interface ResendVerificationResponse {
|
export interface ResendVerificationResponse {
|
||||||
message: string;
|
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 () => {
|
const handleComplete = async () => {
|
||||||
try {
|
try {
|
||||||
await completeOnboarding.mutateAsync();
|
await completeOnboarding.mutateAsync();
|
||||||
navigate('/vehicles');
|
navigate('/garage');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error is handled by the mutation hook
|
// Error is handled by the mutation hook
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const OnboardingPage: React.FC = () => {
|
|||||||
const handleComplete = async () => {
|
const handleComplete = async () => {
|
||||||
try {
|
try {
|
||||||
await completeOnboarding.mutateAsync();
|
await completeOnboarding.mutateAsync();
|
||||||
navigate('/vehicles');
|
navigate('/garage');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error is handled by the mutation hook
|
// Error is handled by the mutation hook
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user