Mobile-BillTracker/src/BiometricLock.tsx

68 lines
2.0 KiB
TypeScript
Raw Normal View History

import { useEffect, useState } from 'react';
import { BiometricAuth, BiometryError, BiometryErrorType } from '@aparajita/capacitor-biometric-auth';
interface Props {
onUnlocked: () => void;
/** User can't (or won't) use biometrics — fall back to server login. */
onFallback: () => void;
}
/**
* App-lock gate shown on launch when biometric unlock is enabled. The
* server session cookie persists in the WebView's cookie store between
* launches, so this is what stands between "phone unlocked" and "Bill
* Tracker dashboard visible".
*/
export default function BiometricLock({ onUnlocked, onFallback }: Props) {
const [error, setError] = useState('');
const [checking, setChecking] = useState(true);
async function attempt() {
setError('');
setChecking(true);
try {
await BiometricAuth.authenticate({
reason: 'Unlock Bill Tracker',
cancelTitle: 'Cancel',
allowDeviceCredential: true,
androidTitle: 'Unlock Bill Tracker',
});
onUnlocked();
} catch (err) {
if (err instanceof BiometryError && err.code === BiometryErrorType.userCancel) {
setError('');
} else {
setError('Authentication failed. Try again.');
}
} finally {
setChecking(false);
}
}
useEffect(() => {
attempt();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="setup-root">
<div className="setup-card" style={{ alignItems: 'center', textAlign: 'center' }}>
{checking ? (
<div className="spinner" />
) : (
<>
<h1 className="setup-title">Locked</h1>
<p className="setup-subtitle">{error || 'Authenticate to continue.'}</p>
<button className="btn-primary" onClick={attempt} style={{ width: '100%' }}>
Try Again
</button>
<button className="btn-secondary" onClick={onFallback} style={{ width: '100%' }}>
Sign In Instead
</button>
</>
)}
</div>
</div>
);
}