fix(auth): persiste session_id pra OAuth sobreviver app-kill v1.6.2
Some checks are pending
Build Android (APK + AAB) / build-android (push) Waiting to run

Bug: ao clicar "Entrar com Google" no APK Capacitor, app abria Chrome,
user logava OK ("Logado, volte pro app"), mas ao voltar pro app o login
não completava — ficava em loop pedindo pra logar de novo.

Causa: Android matava o WebView do app quando ele ia pra background
(usuario indo pro Chrome). Ao reabrir o app, _googleAuthPolling interval
estava perdido e o session_id (em variável JS) também.

Fix: persiste session_id em localStorage com timestamp. Adiciona
resumePollingIfPending() chamado em:
- Bootstrap (sempre, 500ms após init)
- visibilitychange visible (volta do background)

Também faz uma chamada imediata de poll antes de iniciar interval —
caso os tokens já estejam prontos quando o app reabre.

TTL de 10min no localStorage (mesmo TTL do Map no servidor) — após
isso considera expirado e limpa.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
PontualTech / Karlão 2026-04-28 08:30:32 -03:00
parent 24f6df3da7
commit b57ba0da37
5 changed files with 136 additions and 46 deletions

View file

@ -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;

View file

@ -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.

View file

@ -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",

View file

@ -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;

View file

@ -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)