@@ -98,6 +98,10 @@ const SEED = seedFiles["./dadosPlanilha.json"]?.default ?? { months: {} };
9898
9999const STORAGE_KEY = "financas_app_data" ;
100100const LOGIN_KEY = "financas_app_login" ;
101+ // A trava local (PIN/foto/nome) é POR CONTA: cada usuário da nuvem tem a sua,
102+ // senão uma conta herda o PIN/foto de outra que já usou este aparelho.
103+ // Sem sessão (offline), cai na chave antiga (compatibilidade).
104+ const loginKey = ( uid ) => ( uid ? `${ LOGIN_KEY } :${ uid } ` : LOGIN_KEY ) ;
101105const TEMA_KEY = "financas_app_tema" ;
102106const MIGR_KEY = "financas_app_migrado_" ; // + userId: marca que já perguntamos da migração
103107
@@ -3043,6 +3047,7 @@ export default function App() {
30433047 const [ nuvemPronta , setNuvemPronta ] = useState ( false ) ;
30443048 const ultimoSyncRef = useRef ( null ) ; // último estado que já está na nuvem (base do diff)
30453049 const modoNuvemRef = useRef ( false ) ; // true quando os dados vêm/vão pra nuvem
3050+ const ultimoUidRef = useRef ( null ) ; // conta cuja trava local já foi resolvida (p/ detectar troca)
30463051 const [ escuro , setEscuro ] = useState ( ( ) => {
30473052 try {
30483053 return localStorage . getItem ( TEMA_KEY ) === "escuro" ;
@@ -3079,17 +3084,52 @@ export default function App() {
30793084 return "open" ;
30803085 }
30813086
3087+ // Resolve a trava local (PIN/foto/nome) DA CONTA logada na nuvem e atualiza o
3088+ // estado `login`. Cada conta tem sua trava (chave por uid). Se a única trava
3089+ // for a antiga (device-global, sem dono) e o nome bater com o da conta, ela é
3090+ // adotada por essa conta — preserva o PIN/foto de quem já usava o aparelho,
3091+ // sem entregá-los a uma conta diferente. O nome exibido segue a nuvem (fonte
3092+ // da verdade). Troca de conta zera a janela de 1h (re-pede a tranca da nova).
3093+ async function resolverLogin ( sess ) {
3094+ const uid = sess ?. user ?. id ?? null ;
3095+ const nomeNuvem = ( sess ?. user ?. user_metadata ?. nome || "" ) . trim ( ) ;
3096+
3097+ let raw = await storageGet ( loginKey ( uid ) ) ;
3098+ let conf = raw ? JSON . parse ( raw ) : null ;
3099+
3100+ if ( ! conf && uid ) {
3101+ const rawLegado = await storageGet ( LOGIN_KEY ) ;
3102+ const legado = rawLegado ? JSON . parse ( rawLegado ) : null ;
3103+ const mesmaPessoa =
3104+ legado ?. nome && nomeNuvem &&
3105+ legado . nome . trim ( ) . toLowerCase ( ) === nomeNuvem . toLowerCase ( ) ;
3106+ if ( legado && mesmaPessoa ) {
3107+ conf = { ...legado , uid } ;
3108+ await storageSet ( loginKey ( uid ) , JSON . stringify ( conf ) ) ; // migra p/ a conta
3109+ await storageSet ( LOGIN_KEY , "" ) ; // consome a antiga (não vaza p/ outra conta)
3110+ }
3111+ }
3112+
3113+ // Trocou de conta neste aparelho → não reaproveita a janela de 1h.
3114+ if ( uid && ultimoUidRef . current && uid !== ultimoUidRef . current ) limparDesbloqueio ( ) ;
3115+ ultimoUidRef . current = uid ;
3116+
3117+ // Identidade exibida: PIN/foto vêm da trava desta conta; nome prioriza a nuvem.
3118+ if ( conf ?. pinHash ) setLogin ( nomeNuvem ? { ...conf , nome : nomeNuvem } : conf ) ;
3119+ else if ( nomeNuvem ) setLogin ( { nome : nomeNuvem , foto : conf ?. foto ?? null } ) ;
3120+ else setLogin ( conf ) ;
3121+
3122+ return conf ;
3123+ }
3124+
30823125 // Carrega dados salvos ao abrir
30833126 useEffect ( ( ) => {
30843127 ( async ( ) => {
30853128 try {
30863129 const sess = await sessaoAtual ( ) ;
30873130 setSessao ( sess ) ;
30883131 setUserId ( sess ?. user ?. id ?? null ) ;
3089- const rawLogin = await storageGet ( LOGIN_KEY ) ;
3090- const conf = rawLogin ? JSON . parse ( rawLogin ) : null ;
3091- if ( conf && conf . pinHash ) setLogin ( conf ) ;
3092- else if ( sess ?. user ?. user_metadata ?. nome ) setLogin ( { nome : sess . user . user_metadata . nome } ) ;
3132+ const conf = await resolverLogin ( sess ) ; // trava local DA conta logada
30933133 // Aberto por link de "Esqueci a senha" → tela de senha nova (mesmo
30943134 // que já exista sessão de recuperação, NÃO entrar direto).
30953135 // Sem sessão na nuvem → portão externo (entrar/criar conta).
@@ -3228,8 +3268,9 @@ export default function App() {
32283268 /* ---- login / foto / sair ---- */
32293269 async function criarLogin ( nome , pin , pedirSempre , foto ) {
32303270 const salt = Math . random ( ) . toString ( 36 ) . slice ( 2 , 12 ) ;
3231- const conf = { nome, salt, pinHash : await hashPin ( pin , salt ) , pedirSempre, foto : foto || null } ;
3232- await storageSet ( LOGIN_KEY , JSON . stringify ( conf ) ) ;
3271+ const conf = { nome, salt, pinHash : await hashPin ( pin , salt ) , pedirSempre, foto : foto || null , uid : userId } ;
3272+ await storageSet ( loginKey ( userId ) , JSON . stringify ( conf ) ) ;
3273+ ultimoUidRef . current = userId ;
32333274 setLogin ( conf ) ;
32343275 setAuth ( "open" ) ;
32353276 showToast ( `Tudo pronto, ${ nome } ! ✓` ) ;
@@ -3244,7 +3285,7 @@ export default function App() {
32443285
32453286 async function esqueciPin ( ) {
32463287 if ( ! window . confirm ( "Redefinir o PIN? Seus dados financeiros NÃO serão apagados — você só vai criar um novo PIN." ) ) return ;
3247- await storageSet ( LOGIN_KEY , "" ) ;
3288+ await storageSet ( loginKey ( userId ) , "" ) ;
32483289 setLogin ( null ) ;
32493290 setAuth ( "setup" ) ;
32503291 }
@@ -3276,6 +3317,13 @@ export default function App() {
32763317 if ( auth === "open" ) marcarDesbloqueio ( ) ;
32773318 } , [ auth ] ) ;
32783319
3320+ // Espelha `auth` num ref, pra o observador da nuvem (deps []) saber se
3321+ // estamos no portão de login sem virar dependência do efeito.
3322+ const authRef = useRef ( auth ) ;
3323+ useEffect ( ( ) => {
3324+ authRef . current = auth ;
3325+ } , [ auth ] ) ;
3326+
32793327 // Observa login/logout na nuvem (ex.: logo após entrar pela TelaAuth,
32803328 // ou quando a sessão expira/é encerrada em outra aba).
32813329 useEffect ( ( ) => {
@@ -3287,14 +3335,21 @@ export default function App() {
32873335 // A restauração da sessão ao abrir (INITIAL_SESSION) e o refresh de token
32883336 // NÃO decidem a tela: quem decide o estado inicial é a carga (com a regra
32893337 // das 24h). Aqui só reagimos a um login FEITO na tela de entrada
3290- // (prev === "auth") ou a um logout — assim nada fura a checagem de 24h.
3338+ // (auth === "auth") ou a um logout — assim nada fura a checagem de 24h.
32913339 if ( evento === "INITIAL_SESSION" ) return ;
3292- if ( ! sess ) return setAuth ( "auth" ) ;
3293- setAuth ( ( prev ) => ( prev === "auth" ? estadoComSessao ( login ) : prev ) ) ;
3340+ if ( ! sess ) {
3341+ setLogin ( null ) ; // logout → limpa nome/foto na saudação
3342+ return setAuth ( "auth" ) ;
3343+ }
3344+ // Login feito no portão: resolve a trava DESTA conta (nome/foto/PIN certos)
3345+ // e só então decide entre pedir o PIN (lock) ou abrir.
3346+ if ( authRef . current === "auth" ) {
3347+ resolverLogin ( sess ) . then ( ( conf ) => setAuth ( estadoComSessao ( conf ) ) ) ;
3348+ }
32943349 } ) ;
32953350 return parar ;
32963351 // eslint-disable-next-line react-hooks/exhaustive-deps
3297- } , [ login ] ) ;
3352+ } , [ ] ) ;
32983353
32993354 // Ao abrir logado, carrega os dados da NUVEM. No 1º acesso deste aparelho,
33003355 // se houver dados locais ainda não enviados, pergunta se quer subir (migração).
@@ -3356,9 +3411,9 @@ export default function App() {
33563411 if ( ! login ) return ;
33573412 try {
33583413 const credId = await registrarBiometria ( login . nome ) ;
3359- const conf = { ...login , bioCredId : credId } ;
3414+ const conf = { ...login , bioCredId : credId , uid : userId } ;
33603415 setLogin ( conf ) ;
3361- await storageSet ( LOGIN_KEY , JSON . stringify ( conf ) ) ;
3416+ await storageSet ( loginKey ( userId ) , JSON . stringify ( conf ) ) ;
33623417 showToast ( "Face ID / digital ativado ✓" ) ;
33633418 } catch {
33643419 showToast ( "Não consegui ativar a biometria neste aparelho." , true ) ;
@@ -3367,10 +3422,10 @@ export default function App() {
33673422
33683423 async function desativarBiometria ( ) {
33693424 if ( ! login ) return ;
3370- const conf = { ...login } ;
3425+ const conf = { ...login , uid : userId } ;
33713426 delete conf . bioCredId ;
33723427 setLogin ( conf ) ;
3373- await storageSet ( LOGIN_KEY , JSON . stringify ( conf ) ) ;
3428+ await storageSet ( loginKey ( userId ) , JSON . stringify ( conf ) ) ;
33743429 showToast ( "Biometria desativada." ) ;
33753430 }
33763431
@@ -3387,10 +3442,10 @@ export default function App() {
33873442
33883443 async function salvarFoto ( dataUrl ) {
33893444 if ( ! login ) return ;
3390- const conf = { ...login , foto : dataUrl } ;
3445+ const conf = { ...login , foto : dataUrl , uid : userId } ;
33913446 setLogin ( conf ) ;
33923447 setArquivoFoto ( null ) ;
3393- await storageSet ( LOGIN_KEY , JSON . stringify ( conf ) ) ;
3448+ await storageSet ( loginKey ( userId ) , JSON . stringify ( conf ) ) ;
33943449 showToast ( "Foto atualizada ✓" ) ;
33953450 }
33963451
0 commit comments