diff --git a/app/diario-bordo.html b/app/diario-bordo.html index f73dc49..c8dd2b1 100644 --- a/app/diario-bordo.html +++ b/app/diario-bordo.html @@ -3137,6 +3137,8 @@ async function updateStorageInfo(){ setTimeout(maybeAutoFetchWeather,3000); // Welcome screen — só pra usuários sem login setTimeout(maybeShowWelcome,300); + // Retoma polling do OAuth se app foi morto durante login Google + setTimeout(resumePollingIfPending,500); })(); // Re-tenta init Google Sign-In quando o script async carrega window.addEventListener('load',()=>setTimeout(()=>{if(document.getElementById('welcome-screen').style.display==='flex')initGoogleSignIn()},500)); @@ -3146,6 +3148,8 @@ document.addEventListener('visibilitychange',async()=>{ if(anchorWatch.active&&!anchorWatch.wakeLock)await requestAnchorWakeLock(); // Reconecta WS ao voltar ao foreground if(cloudConfigured()&&(!_wsConn||_wsConn.readyState!==WebSocket.OPEN))rtConnect(); + // Retoma polling Google se sessão pendente + resumePollingIfPending(); } }); window.addEventListener('online',()=>{if(cloudConfigured())rtConnect()}); @@ -3921,6 +3925,24 @@ function welcomeGoogleClick(){ } let _googleAuthPolling=null; +const PENDING_SESSION_KEY='shivao_pending_google_session'; + +function savePendingSession(session){ + localStorage.setItem(PENDING_SESSION_KEY,JSON.stringify({session,startedAt:Date.now()})); +} +function getPendingSession(){ + try{ + const raw=localStorage.getItem(PENDING_SESSION_KEY); + if(!raw)return null; + const d=JSON.parse(raw); + if(Date.now()-d.startedAt>10*60*1000){localStorage.removeItem(PENDING_SESSION_KEY);return null} + return d.session; + }catch(e){return null} +} +function clearPendingSession(){ + localStorage.removeItem(PENDING_SESSION_KEY); +} + async function startGoogleRedirectFlow(){ toast('Abrindo Google...'); try{ @@ -3930,34 +3952,57 @@ async function startGoogleRedirectFlow(){ if(!r.ok)throw new Error('HTTP '+r.status); const{url,session}=await r.json(); if(!url||!session)throw new Error('servidor sem URL/session'); + // PERSISTE session no localStorage — sobrevive a app morrer/reabrir + savePendingSession(session); // Abre URL no browser EXTERNO (em Capacitor isso usa Custom Tabs) if(window.open){window.open(url,'_blank','noopener')}else{location.href=url} - // Inicia polling - if(_googleAuthPolling)clearInterval(_googleAuthPolling); - let tries=0; - _googleAuthPolling=setInterval(async()=>{ - tries++; - if(tries>120){clearInterval(_googleAuthPolling);toast('Tempo esgotado. Tente de novo.');return} - try{ - const pr=await fetch(cloudUrl('/api/auth/google/poll?session='+encodeURIComponent(session))); - if(pr.status===204)return; // ainda esperando - if(!pr.ok)return; - const j=await pr.json(); - if(j.accessToken&&j.refreshToken){ - clearInterval(_googleAuthPolling); - state.auth={accessToken:j.accessToken,refreshToken:j.refreshToken,user:j.user}; - saveState(); - toast('Bem-vindo, '+(j.user.name||j.user.email)); - welcomeFinish(); - if(typeof renderAuthBox==='function')renderAuthBox(); - } - }catch(e){console.warn('[gsi-poll]',e.message)} - },2000); + // Inicia polling (também resume após reabrir o app via resumePollingIfPending) + startSessionPolling(session); }catch(e){ console.warn('[gsi-redirect]',e); toast('Erro: '+e.message); } } + +function startSessionPolling(session){ + if(_googleAuthPolling)clearInterval(_googleAuthPolling); + let tries=0; + console.log('[gsi-poll] starting for session',session); + // Faz uma chamada IMEDIATA primeiro (caso já esteja pronto) + const pollOnce=async()=>{ + tries++; + if(tries>120){clearInterval(_googleAuthPolling);_googleAuthPolling=null;clearPendingSession();toast('Tempo esgotado. Tente de novo.');return} + try{ + const pr=await fetch(cloudUrl('/api/auth/google/poll?session='+encodeURIComponent(session))); + if(pr.status===204)return; // ainda esperando + if(!pr.ok)return; + const j=await pr.json(); + if(j.accessToken&&j.refreshToken){ + clearInterval(_googleAuthPolling);_googleAuthPolling=null; + clearPendingSession(); + state.auth={accessToken:j.accessToken,refreshToken:j.refreshToken,user:j.user}; + saveState(); + toast('Bem-vindo, '+(j.user.name||j.user.email)); + welcomeFinish(); + if(typeof renderAuthBox==='function')renderAuthBox(); + } + }catch(e){console.warn('[gsi-poll]',e.message)} + }; + pollOnce(); // imediato + _googleAuthPolling=setInterval(pollOnce,2000); +} + +// Retoma polling se app foi morto e reaberto durante OAuth +function resumePollingIfPending(){ + const session=getPendingSession(); + if(!session)return false; + if(_googleAuthPolling)return true; // já rodando + if(state.auth)return clearPendingSession(),false; // já logado + console.log('[gsi-poll] resuming session',session); + toast('Verificando login Google...'); + startSessionPolling(session); + return true; +} async function onGoogleCredential(resp){ if(!resp?.credential){toast('Sem credential do Google');return} state.cloud.url=DEFAULT_CLOUD_URL; diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 64ac06c..4948964 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "br.com.pontualtech.shivao" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 8 - versionName "1.6.1" + versionCode 9 + versionName "1.6.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/mobile/package.json b/mobile/package.json index b3033e6..3c5e443 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -1,6 +1,6 @@ { "name": "shivao-mobile", - "version": "1.6.1", + "version": "1.6.2", "description": "Shivao app nativo (Capacitor wrapper Android/iOS)", "main": "index.js", "type": "module", diff --git a/server/public/index.html b/server/public/index.html index f73dc49..c8dd2b1 100644 --- a/server/public/index.html +++ b/server/public/index.html @@ -3137,6 +3137,8 @@ async function updateStorageInfo(){ setTimeout(maybeAutoFetchWeather,3000); // Welcome screen — só pra usuários sem login setTimeout(maybeShowWelcome,300); + // Retoma polling do OAuth se app foi morto durante login Google + setTimeout(resumePollingIfPending,500); })(); // Re-tenta init Google Sign-In quando o script async carrega window.addEventListener('load',()=>setTimeout(()=>{if(document.getElementById('welcome-screen').style.display==='flex')initGoogleSignIn()},500)); @@ -3146,6 +3148,8 @@ document.addEventListener('visibilitychange',async()=>{ if(anchorWatch.active&&!anchorWatch.wakeLock)await requestAnchorWakeLock(); // Reconecta WS ao voltar ao foreground if(cloudConfigured()&&(!_wsConn||_wsConn.readyState!==WebSocket.OPEN))rtConnect(); + // Retoma polling Google se sessão pendente + resumePollingIfPending(); } }); window.addEventListener('online',()=>{if(cloudConfigured())rtConnect()}); @@ -3921,6 +3925,24 @@ function welcomeGoogleClick(){ } let _googleAuthPolling=null; +const PENDING_SESSION_KEY='shivao_pending_google_session'; + +function savePendingSession(session){ + localStorage.setItem(PENDING_SESSION_KEY,JSON.stringify({session,startedAt:Date.now()})); +} +function getPendingSession(){ + try{ + const raw=localStorage.getItem(PENDING_SESSION_KEY); + if(!raw)return null; + const d=JSON.parse(raw); + if(Date.now()-d.startedAt>10*60*1000){localStorage.removeItem(PENDING_SESSION_KEY);return null} + return d.session; + }catch(e){return null} +} +function clearPendingSession(){ + localStorage.removeItem(PENDING_SESSION_KEY); +} + async function startGoogleRedirectFlow(){ toast('Abrindo Google...'); try{ @@ -3930,34 +3952,57 @@ async function startGoogleRedirectFlow(){ if(!r.ok)throw new Error('HTTP '+r.status); const{url,session}=await r.json(); if(!url||!session)throw new Error('servidor sem URL/session'); + // PERSISTE session no localStorage — sobrevive a app morrer/reabrir + savePendingSession(session); // Abre URL no browser EXTERNO (em Capacitor isso usa Custom Tabs) if(window.open){window.open(url,'_blank','noopener')}else{location.href=url} - // Inicia polling - if(_googleAuthPolling)clearInterval(_googleAuthPolling); - let tries=0; - _googleAuthPolling=setInterval(async()=>{ - tries++; - if(tries>120){clearInterval(_googleAuthPolling);toast('Tempo esgotado. Tente de novo.');return} - try{ - const pr=await fetch(cloudUrl('/api/auth/google/poll?session='+encodeURIComponent(session))); - if(pr.status===204)return; // ainda esperando - if(!pr.ok)return; - const j=await pr.json(); - if(j.accessToken&&j.refreshToken){ - clearInterval(_googleAuthPolling); - state.auth={accessToken:j.accessToken,refreshToken:j.refreshToken,user:j.user}; - saveState(); - toast('Bem-vindo, '+(j.user.name||j.user.email)); - welcomeFinish(); - if(typeof renderAuthBox==='function')renderAuthBox(); - } - }catch(e){console.warn('[gsi-poll]',e.message)} - },2000); + // Inicia polling (também resume após reabrir o app via resumePollingIfPending) + startSessionPolling(session); }catch(e){ console.warn('[gsi-redirect]',e); toast('Erro: '+e.message); } } + +function startSessionPolling(session){ + if(_googleAuthPolling)clearInterval(_googleAuthPolling); + let tries=0; + console.log('[gsi-poll] starting for session',session); + // Faz uma chamada IMEDIATA primeiro (caso já esteja pronto) + const pollOnce=async()=>{ + tries++; + if(tries>120){clearInterval(_googleAuthPolling);_googleAuthPolling=null;clearPendingSession();toast('Tempo esgotado. Tente de novo.');return} + try{ + const pr=await fetch(cloudUrl('/api/auth/google/poll?session='+encodeURIComponent(session))); + if(pr.status===204)return; // ainda esperando + if(!pr.ok)return; + const j=await pr.json(); + if(j.accessToken&&j.refreshToken){ + clearInterval(_googleAuthPolling);_googleAuthPolling=null; + clearPendingSession(); + state.auth={accessToken:j.accessToken,refreshToken:j.refreshToken,user:j.user}; + saveState(); + toast('Bem-vindo, '+(j.user.name||j.user.email)); + welcomeFinish(); + if(typeof renderAuthBox==='function')renderAuthBox(); + } + }catch(e){console.warn('[gsi-poll]',e.message)} + }; + pollOnce(); // imediato + _googleAuthPolling=setInterval(pollOnce,2000); +} + +// Retoma polling se app foi morto e reaberto durante OAuth +function resumePollingIfPending(){ + const session=getPendingSession(); + if(!session)return false; + if(_googleAuthPolling)return true; // já rodando + if(state.auth)return clearPendingSession(),false; // já logado + console.log('[gsi-poll] resuming session',session); + toast('Verificando login Google...'); + startSessionPolling(session); + return true; +} async function onGoogleCredential(resp){ if(!resp?.credential){toast('Sem credential do Google');return} state.cloud.url=DEFAULT_CLOUD_URL; diff --git a/server/src/index.js b/server/src/index.js index d59e1fe..c7443a2 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -347,7 +347,7 @@ app.get('/.well-known/assetlinks.json', (req, res) => { }); // Atalho: /apk redireciona pra última APK release no Forgejo -const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.6.1/Shivao-v1.6.1.apk'; +const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.6.2/Shivao-v1.6.2.apk'; app.get('/apk', (req, res) => res.redirect(302, LATEST_APK_URL)); // Página A4 imprimível com QR Code + instruções (cola no barco/marina)