From 5a7202cf5b17d332fe624ad8e82fecc0f380e43f Mon Sep 17 00:00:00 2001 From: Andrei Date: Thu, 2 Oct 2025 05:46:57 +0000 Subject: [PATCH] Add biometric login button to login page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add biometric authentication button with Face ID/Touch ID/Windows Hello support - Check WebAuthn support and platform authenticator availability on mount - Handle biometric login flow with proper error handling - Show biometric button only when device supports it - Add loading states and user-friendly error messages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- maternal-web/app/(auth)/login/page.tsx | 76 +++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/maternal-web/app/(auth)/login/page.tsx b/maternal-web/app/(auth)/login/page.tsx index 41c573e..d9c154f 100644 --- a/maternal-web/app/(auth)/login/page.tsx +++ b/maternal-web/app/(auth)/login/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { Box, @@ -15,7 +15,7 @@ import { CircularProgress, Link as MuiLink, } from '@mui/material'; -import { Visibility, VisibilityOff, Google, Apple } from '@mui/icons-material'; +import { Visibility, VisibilityOff, Google, Apple, Fingerprint } from '@mui/icons-material'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { motion } from 'framer-motion'; @@ -23,6 +23,8 @@ import * as z from 'zod'; import { useAuth } from '@/lib/auth/AuthContext'; import { MFAVerificationDialog } from '@/components/auth/MFAVerificationDialog'; import { tokenStorage } from '@/lib/utils/tokenStorage'; +import { biometricApi } from '@/lib/api/biometric'; +import { startAuthentication } from '@simplewebauthn/browser'; import Link from 'next/link'; const loginSchema = z.object({ @@ -36,6 +38,8 @@ export default function LoginPage() { const [showPassword, setShowPassword] = useState(false); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [isBiometricLoading, setIsBiometricLoading] = useState(false); + const [isBiometricSupported, setIsBiometricSupported] = useState(false); const [mfaRequired, setMfaRequired] = useState(false); const [mfaData, setMfaData] = useState<{ userId: string; mfaMethod: 'totp' | 'email' } | null>(null); const { login } = useAuth(); @@ -45,10 +49,64 @@ export default function LoginPage() { register, handleSubmit, formState: { errors }, + watch, } = useForm({ resolver: zodResolver(loginSchema), }); + const email = watch('email'); + + // Check biometric support on mount + useEffect(() => { + checkBiometricSupport(); + }, []); + + const checkBiometricSupport = async () => { + const supported = biometricApi.isSupported(); + if (supported) { + const available = await biometricApi.isPlatformAuthenticatorAvailable(); + setIsBiometricSupported(available); + } + }; + + const handleBiometricLogin = async () => { + setError(null); + setIsBiometricLoading(true); + + try { + // Get authentication options from server + const options = await biometricApi.getAuthenticationOptions(email || undefined); + + // Start WebAuthn authentication (triggers Face ID/Touch ID/Windows Hello) + const authenticationResponse = await startAuthentication(options); + + // Send response to server for verification and get tokens + const result = await biometricApi.verifyAuthentication( + authenticationResponse, + email || undefined, + { + deviceId: authenticationResponse.id.substring(0, 10), + platform: navigator.userAgent, + } + ); + + // Store tokens and navigate + tokenStorage.setTokens(result.tokens.accessToken, result.tokens.refreshToken); + router.push('/'); + } catch (err: any) { + console.error('Biometric login failed:', err); + if (err.name === 'NotAllowedError') { + setError('Biometric authentication was cancelled'); + } else if (err.name === 'NotSupportedError') { + setError('Biometric authentication is not supported on this device'); + } else { + setError(err.response?.data?.message || err.message || 'Biometric login failed. Please try again.'); + } + } finally { + setIsBiometricLoading(false); + } + }; + const onSubmit = async (data: LoginFormData) => { setError(null); setIsLoading(true); @@ -229,6 +287,20 @@ export default function LoginPage() { Continue with Apple + {isBiometricSupported && ( + + )} + Don't have an account?{' '}