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
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:
parent
24f6df3da7
commit
b57ba0da37
5 changed files with 136 additions and 46 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue