@@ -35,6 +35,7 @@ import {
3535 Square ,
3636 Image as ImageIcon ,
3737 Sparkles ,
38+ ScanFace ,
3839 Printer ,
3940 FileSpreadsheet ,
4041} from "lucide-react" ;
@@ -54,6 +55,11 @@ import {
5455import logoUrl from "./logo.png" ;
5556import emblemaUrl from "./emblema.png" ;
5657import { supabase } from "./supabaseClient" ;
58+ import {
59+ temAutenticadorPlataforma ,
60+ registrarBiometria ,
61+ verificarBiometria ,
62+ } from "./biometria" ;
5763
5864// Dados reais importados das planilhas — o arquivo fica só neste computador,
5965// fora do repositório público. Na versão publicada ele não existe e o app
@@ -2014,7 +2020,7 @@ function CampoLogin({ label, ...props }) {
20142020 ) ;
20152021}
20162022
2017- function TelaLogin ( { modo, nome, foto, onCriar, onDesbloquear, onEsqueci, escuro, onTema } ) {
2023+ function TelaLogin ( { modo, nome, foto, onCriar, onDesbloquear, onEsqueci, onBiometria , temBio , escuro, onTema } ) {
20182024 const [ nomeInput , setNomeInput ] = useState ( "" ) ;
20192025 const [ pin , setPin ] = useState ( "" ) ;
20202026 const [ pin2 , setPin2 ] = useState ( "" ) ;
@@ -2154,6 +2160,26 @@ function TelaLogin({ modo, nome, foto, onCriar, onDesbloquear, onEsqueci, escuro
21542160 </ form >
21552161 ) : (
21562162 < form onSubmit = { desbloquear } className = "space-y-4" >
2163+ { temBio && (
2164+ < >
2165+ < button
2166+ type = "button"
2167+ onClick = { async ( ) => {
2168+ setErro ( "" ) ;
2169+ const ok = await onBiometria ( ) ;
2170+ if ( ! ok ) setErro ( "Não reconheci. Você pode entrar com o PIN abaixo." ) ;
2171+ } }
2172+ className = "flex w-full items-center justify-center gap-2 rounded-xl bg-emerald-600 py-3 text-sm font-semibold text-white transition hover:bg-emerald-700"
2173+ >
2174+ < ScanFace size = { 18 } /> Entrar com Face ID / digital
2175+ </ button >
2176+ < div className = "flex items-center gap-3 text-[11px] text-slate-400" >
2177+ < span className = "h-px flex-1 bg-slate-200 dark:bg-slate-700" />
2178+ ou use o PIN
2179+ < span className = "h-px flex-1 bg-slate-200 dark:bg-slate-700" />
2180+ </ div >
2181+ </ >
2182+ ) }
21572183 < CampoLogin
21582184 label = "Seu PIN"
21592185 type = "password"
@@ -2162,7 +2188,7 @@ function TelaLogin({ modo, nome, foto, onCriar, onDesbloquear, onEsqueci, escuro
21622188 value = { pin }
21632189 onChange = { ( e ) => setPin ( e . target . value . replace ( / \D / g, "" ) ) }
21642190 placeholder = "••••"
2165- autoFocus
2191+ autoFocus = { ! temBio }
21662192 />
21672193 { erro && < p className = "text-sm text-red-600 dark:text-red-400" > { erro } </ p > }
21682194 < button
@@ -2229,6 +2255,7 @@ export default function App() {
22292255 }
22302256 } ) ;
22312257 const [ arquivoFoto , setArquivoFoto ] = useState ( null ) ;
2258+ const [ bioDisponivel , setBioDisponivel ] = useState ( false ) ;
22322259 const dirtyRef = useRef ( false ) ;
22332260 const toastTimer = useRef ( null ) ;
22342261 const fileRef = useRef ( null ) ;
@@ -2252,7 +2279,7 @@ export default function App() {
22522279 const conf = rawLogin ? JSON . parse ( rawLogin ) : null ;
22532280 if ( conf && conf . pinHash ) {
22542281 setLogin ( conf ) ;
2255- setAuth ( conf . pedirSempre ? "lock" : "open" ) ;
2282+ setAuth ( conf . pedirSempre || conf . bioCredId ? "lock" : "open" ) ;
22562283 } else {
22572284 setAuth ( "setup" ) ;
22582285 }
@@ -2358,6 +2385,44 @@ export default function App() {
23582385 setAuth ( "lock" ) ;
23592386 }
23602387
2388+ // Verifica, ao abrir, se o aparelho tem Face ID / digital
2389+ useEffect ( ( ) => {
2390+ temAutenticadorPlataforma ( ) . then ( setBioDisponivel ) ;
2391+ } , [ ] ) ;
2392+
2393+ async function ativarBiometria ( ) {
2394+ if ( ! login ) return ;
2395+ try {
2396+ const credId = await registrarBiometria ( login . nome ) ;
2397+ const conf = { ...login , bioCredId : credId } ;
2398+ setLogin ( conf ) ;
2399+ await storageSet ( LOGIN_KEY , JSON . stringify ( conf ) ) ;
2400+ showToast ( "Face ID / digital ativado ✓" ) ;
2401+ } catch {
2402+ showToast ( "Não consegui ativar a biometria neste aparelho." , true ) ;
2403+ }
2404+ }
2405+
2406+ async function desativarBiometria ( ) {
2407+ if ( ! login ) return ;
2408+ const conf = { ...login } ;
2409+ delete conf . bioCredId ;
2410+ setLogin ( conf ) ;
2411+ await storageSet ( LOGIN_KEY , JSON . stringify ( conf ) ) ;
2412+ showToast ( "Biometria desativada." ) ;
2413+ }
2414+
2415+ async function desbloquearBiometria ( ) {
2416+ if ( ! login ?. bioCredId ) return false ;
2417+ try {
2418+ await verificarBiometria ( login . bioCredId ) ;
2419+ setAuth ( "open" ) ;
2420+ return true ;
2421+ } catch {
2422+ return false ;
2423+ }
2424+ }
2425+
23612426 async function salvarFoto ( dataUrl ) {
23622427 if ( ! login ) return ;
23632428 const conf = { ...login , foto : dataUrl } ;
@@ -2436,6 +2501,8 @@ export default function App() {
24362501 onCriar = { criarLogin }
24372502 onDesbloquear = { desbloquear }
24382503 onEsqueci = { esqueciPin }
2504+ onBiometria = { desbloquearBiometria }
2505+ temBio = { ! ! login ?. bioCredId }
24392506 escuro = { escuro }
24402507 onTema = { ( ) => setEscuro ( ( e ) => ! e ) }
24412508 />
@@ -2519,6 +2586,19 @@ export default function App() {
25192586 < LogOut size = { 16 } />
25202587 </ button >
25212588 </ div >
2589+ { bioDisponivel && (
2590+ < button
2591+ onClick = { login ?. bioCredId ? desativarBiometria : ativarBiometria }
2592+ className = {
2593+ "flex w-full items-center justify-center gap-2 rounded-xl border px-3 py-2 text-sm font-medium transition " +
2594+ ( login ?. bioCredId
2595+ ? "border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-300"
2596+ : "border-slate-200 text-slate-500 hover:bg-slate-50 dark:border-slate-700 dark:text-slate-400 dark:hover:bg-slate-800" )
2597+ }
2598+ >
2599+ < ScanFace size = { 16 } /> { login ?. bioCredId ? "Face ID / digital ativo" : "Ativar Face ID / digital" }
2600+ </ button >
2601+ ) }
25222602 < button
25232603 onClick = { exportData }
25242604 className = "flex w-full items-center justify-center gap-2 rounded-xl border border-slate-200 px-3 py-2 text-sm font-medium text-slate-500 transition hover:bg-slate-50 dark:border-slate-700 dark:text-slate-400 dark:hover:bg-slate-800"
0 commit comments