68 lines
2.0 KiB
TypeScript
68 lines
2.0 KiB
TypeScript
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 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>
|
|
);
|
|
}
|