Skip to content

Commit f058c39

Browse files
committed
Update FaceID
1 parent 0c5dc48 commit f058c39

2 files changed

Lines changed: 63 additions & 105 deletions

File tree

src/screens/Login.jsx

Lines changed: 25 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -43,29 +43,35 @@ const Login = () => {
4343
}
4444
};
4545

46-
const handleFaceID = () => {
46+
const handleFaceID = async () => {
4747
if (!userBiometrics[selectedUser.id]) {
48-
setStatus('failed');
49-
setTimeout(() => setStatus('idle'), 2000);
48+
alert("Please enroll Face ID in Settings first.");
5049
return;
5150
}
5251

53-
setStatus('scanning');
54-
// Simulate Face ID scanning process with a chance of failure (e.g. 1 in 10)
55-
setTimeout(() => {
56-
const isSuccess = Math.random() > 0.1;
57-
if (isSuccess) {
58-
setStatus('success');
59-
setTimeout(() => {
60-
handleLoginSuccess();
61-
}, 1000);
62-
} else {
63-
setStatus('failed');
64-
setTimeout(() => {
65-
setStatus('idle');
66-
}, 1500);
52+
try {
53+
const challenge = new Uint8Array(32);
54+
window.crypto.getRandomValues(challenge);
55+
56+
const options = {
57+
publicKey: {
58+
challenge,
59+
rpId: window.location.hostname,
60+
userVerification: "required",
61+
timeout: 60000
62+
}
63+
};
64+
65+
// --- REAL HARDWARE PROMPT ---
66+
const credential = await navigator.credentials.get(options);
67+
if (credential) {
68+
handleLoginSuccess();
6769
}
68-
}, 2000);
70+
} catch (err) {
71+
console.error("Hardware Authentication Failed:", err);
72+
setError(true);
73+
setTimeout(() => setError(false), 2000);
74+
}
6975
};
7076

7177
const handleLoginSuccess = () => {
@@ -217,90 +223,7 @@ const Login = () => {
217223
)}
218224
</AnimatePresence>
219225

220-
{/* Face ID Overlay */}
221-
<AnimatePresence>
222-
{(status === 'scanning' || status === 'success' || status === 'failed') && (
223-
<motion.div
224-
initial={{ opacity: 0 }}
225-
animate={{ opacity: 1 }}
226-
exit={{ opacity: 0 }}
227-
style={{
228-
position: 'fixed', inset: 0, zIndex: 1000,
229-
background: 'rgba(0,0,0,0.85)', backdropFilter: 'blur(20px)',
230-
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center'
231-
}}
232-
>
233-
<div style={{ position: 'relative', width: '160px', height: '160px' }}>
234-
<motion.div
235-
animate={{
236-
scale: status === 'success' ? [1, 1.1, 1] : 1,
237-
x: status === 'failed' ? [0, -10, 10, -10, 10, 0] : 0
238-
}}
239-
style={{
240-
width: '160px', height: '160px', borderRadius: '40px',
241-
border: `2px solid ${status === 'success' ? '#34C759' : status === 'failed' ? '#FF3B30' : 'rgba(255,255,255,0.2)'}`,
242-
display: 'flex', alignItems: 'center', justifyContent: 'center',
243-
position: 'relative', overflow: 'hidden'
244-
}}
245-
>
246-
{status === 'scanning' ? (
247-
<>
248-
<ScanFace size={80} color="white" />
249-
<motion.div
250-
animate={{ top: ['0%', '100%', '0%'] }}
251-
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
252-
style={{
253-
position: 'absolute', left: 0, right: 0, height: '2px',
254-
background: 'linear-gradient(90deg, transparent, var(--accent-primary), transparent)',
255-
boxShadow: '0 0 15px var(--accent-primary)',
256-
zIndex: 2
257-
}}
258-
/>
259-
</>
260-
) : status === 'success' ? (
261-
<motion.div
262-
initial={{ scale: 0 }}
263-
animate={{ scale: 1 }}
264-
transition={{ type: 'spring', damping: 12 }}
265-
>
266-
<CheckCircle2 size={80} color="#34C759" />
267-
</motion.div>
268-
) : (
269-
<div style={{ textAlign: 'center', color: '#FF3B30' }}>
270-
<ShieldCheck size={80} />
271-
</div>
272-
)}
273-
</motion.div>
274-
275-
{/* Scanning corners */}
276-
{[0, 1, 2, 3].map(i => (
277-
<div key={i} style={{
278-
position: 'absolute', width: '20px', height: '20px',
279-
borderTop: `3px solid ${status === 'failed' ? '#FF3B30' : 'white'}`,
280-
borderLeft: `3px solid ${status === 'failed' ? '#FF3B30' : 'white'}`,
281-
top: i < 2 ? -10 : 'auto', bottom: i >= 2 ? -10 : 'auto',
282-
left: i % 2 === 0 ? -10 : 'auto', right: i % 2 !== 0 ? -10 : 'auto',
283-
transform: `rotate(${i * 90}deg)`,
284-
opacity: status === 'success' ? 0 : 0.5,
285-
transition: 'all 0.3s'
286-
}} />
287-
))}
288-
</div>
289-
290-
<motion.p
291-
style={{ color: status === 'failed' ? '#FF3B30' : 'white', marginTop: '40px', fontSize: '18px', fontWeight: '700', letterSpacing: '0.5px' }}
292-
>
293-
{status === 'scanning' ? 'Scanning Face...' :
294-
status === 'success' ? 'Identity Verified' :
295-
!userBiometrics[selectedUser.id] ? 'Biometrics Not Enrolled' : 'Face Not Recognized'}
296-
</motion.p>
297-
298-
{status === 'failed' && !userBiometrics[selectedUser.id] && (
299-
<p style={{ color: 'rgba(255,255,255,0.6)', fontSize: '13px', marginTop: '8px' }}>Enable Face ID in Settings first.</p>
300-
)}
301-
</motion.div>
302-
)}
303-
</AnimatePresence>
226+
{/* Real Hardware Face ID doesn't need overlays — the OS handles it */}
304227

305228
{status === 'loading' && (
306229
<div style={{ position: 'fixed', inset: 0, zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--bg-color)' }}>

src/store/useStore.js

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,46 @@ export const useStore = create(
6464
if (currentUser) newUserThemes[currentUser.id] = newTheme;
6565
set({ theme: newTheme, userThemes: newUserThemes });
6666
},
67-
toggleBiometrics: () => {
67+
toggleBiometrics: async () => {
6868
const { currentUser, userBiometrics } = get();
6969
if (!currentUser) return;
70-
const newUserBiometrics = { ...userBiometrics, [currentUser.id]: !userBiometrics[currentUser.id] };
71-
set({ userBiometrics: newUserBiometrics });
70+
71+
// If disabling, just turn it off
72+
if (userBiometrics[currentUser.id]) {
73+
const newUserBiometrics = { ...userBiometrics, [currentUser.id]: false };
74+
set({ userBiometrics: newUserBiometrics });
75+
return;
76+
}
77+
78+
// --- REAL WEBAUTHN ENROLLMENT ---
79+
try {
80+
const challenge = new Uint8Array(32);
81+
window.crypto.getRandomValues(challenge);
82+
83+
const options = {
84+
publicKey: {
85+
challenge,
86+
rp: { name: "MoneyPlanner", id: window.location.hostname },
87+
user: {
88+
id: Uint8Array.from(currentUser.id, c => c.charCodeAt(0)),
89+
name: currentUser.name,
90+
displayName: currentUser.name
91+
},
92+
pubKeyCredParams: [{ type: "public-key", alg: -7 }, { type: "public-key", alg: -257 }],
93+
authenticatorSelection: { userVerification: "required", authenticatorAttachment: "platform" },
94+
timeout: 60000
95+
}
96+
};
97+
98+
const credential = await navigator.credentials.create(options);
99+
if (credential) {
100+
const newUserBiometrics = { ...userBiometrics, [currentUser.id]: true };
101+
set({ userBiometrics: newUserBiometrics });
102+
}
103+
} catch (err) {
104+
console.error("WebAuthn Enrollment Failed:", err);
105+
alert("Enrollment failed. Ensure you are on HTTPS or localhost and biometrics are supported.");
106+
}
72107
},
73108
setLastActive: () => set({ lastActive: Date.now() }),
74109
fetchExchangeRate: async () => {

0 commit comments

Comments
 (0)