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);
|
setTimeout(maybeAutoFetchWeather,3000);
|
||||||
// Welcome screen — só pra usuários sem login
|
// Welcome screen — só pra usuários sem login
|
||||||
setTimeout(maybeShowWelcome,300);
|
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
|
// 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));
|
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();
|
if(anchorWatch.active&&!anchorWatch.wakeLock)await requestAnchorWakeLock();
|
||||||
// Reconecta WS ao voltar ao foreground
|
// Reconecta WS ao voltar ao foreground
|
||||||
if(cloudConfigured()&&(!_wsConn||_wsConn.readyState!==WebSocket.OPEN))rtConnect();
|
if(cloudConfigured()&&(!_wsConn||_wsConn.readyState!==WebSocket.OPEN))rtConnect();
|
||||||
|
// Retoma polling Google se sessão pendente
|
||||||
|
resumePollingIfPending();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
window.addEventListener('online',()=>{if(cloudConfigured())rtConnect()});
|
window.addEventListener('online',()=>{if(cloudConfigured())rtConnect()});
|
||||||
|
|
@ -3921,6 +3925,24 @@ function welcomeGoogleClick(){
|
||||||
}
|
}
|
||||||
|
|
||||||
let _googleAuthPolling=null;
|
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(){
|
async function startGoogleRedirectFlow(){
|
||||||
toast('Abrindo Google...');
|
toast('Abrindo Google...');
|
||||||
try{
|
try{
|
||||||
|
|
@ -3930,21 +3952,34 @@ async function startGoogleRedirectFlow(){
|
||||||
if(!r.ok)throw new Error('HTTP '+r.status);
|
if(!r.ok)throw new Error('HTTP '+r.status);
|
||||||
const{url,session}=await r.json();
|
const{url,session}=await r.json();
|
||||||
if(!url||!session)throw new Error('servidor sem URL/session');
|
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)
|
// Abre URL no browser EXTERNO (em Capacitor isso usa Custom Tabs)
|
||||||
if(window.open){window.open(url,'_blank','noopener')}else{location.href=url}
|
if(window.open){window.open(url,'_blank','noopener')}else{location.href=url}
|
||||||
// Inicia polling
|
// 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);
|
if(_googleAuthPolling)clearInterval(_googleAuthPolling);
|
||||||
let tries=0;
|
let tries=0;
|
||||||
_googleAuthPolling=setInterval(async()=>{
|
console.log('[gsi-poll] starting for session',session);
|
||||||
|
// Faz uma chamada IMEDIATA primeiro (caso já esteja pronto)
|
||||||
|
const pollOnce=async()=>{
|
||||||
tries++;
|
tries++;
|
||||||
if(tries>120){clearInterval(_googleAuthPolling);toast('Tempo esgotado. Tente de novo.');return}
|
if(tries>120){clearInterval(_googleAuthPolling);_googleAuthPolling=null;clearPendingSession();toast('Tempo esgotado. Tente de novo.');return}
|
||||||
try{
|
try{
|
||||||
const pr=await fetch(cloudUrl('/api/auth/google/poll?session='+encodeURIComponent(session)));
|
const pr=await fetch(cloudUrl('/api/auth/google/poll?session='+encodeURIComponent(session)));
|
||||||
if(pr.status===204)return; // ainda esperando
|
if(pr.status===204)return; // ainda esperando
|
||||||
if(!pr.ok)return;
|
if(!pr.ok)return;
|
||||||
const j=await pr.json();
|
const j=await pr.json();
|
||||||
if(j.accessToken&&j.refreshToken){
|
if(j.accessToken&&j.refreshToken){
|
||||||
clearInterval(_googleAuthPolling);
|
clearInterval(_googleAuthPolling);_googleAuthPolling=null;
|
||||||
|
clearPendingSession();
|
||||||
state.auth={accessToken:j.accessToken,refreshToken:j.refreshToken,user:j.user};
|
state.auth={accessToken:j.accessToken,refreshToken:j.refreshToken,user:j.user};
|
||||||
saveState();
|
saveState();
|
||||||
toast('Bem-vindo, '+(j.user.name||j.user.email));
|
toast('Bem-vindo, '+(j.user.name||j.user.email));
|
||||||
|
|
@ -3952,11 +3987,21 @@ async function startGoogleRedirectFlow(){
|
||||||
if(typeof renderAuthBox==='function')renderAuthBox();
|
if(typeof renderAuthBox==='function')renderAuthBox();
|
||||||
}
|
}
|
||||||
}catch(e){console.warn('[gsi-poll]',e.message)}
|
}catch(e){console.warn('[gsi-poll]',e.message)}
|
||||||
},2000);
|
};
|
||||||
}catch(e){
|
pollOnce(); // imediato
|
||||||
console.warn('[gsi-redirect]',e);
|
_googleAuthPolling=setInterval(pollOnce,2000);
|
||||||
toast('Erro: '+e.message);
|
}
|
||||||
}
|
|
||||||
|
// 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){
|
async function onGoogleCredential(resp){
|
||||||
if(!resp?.credential){toast('Sem credential do Google');return}
|
if(!resp?.credential){toast('Sem credential do Google');return}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ android {
|
||||||
applicationId "br.com.pontualtech.shivao"
|
applicationId "br.com.pontualtech.shivao"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 8
|
versionCode 9
|
||||||
versionName "1.6.1"
|
versionName "1.6.2"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "shivao-mobile",
|
"name": "shivao-mobile",
|
||||||
"version": "1.6.1",
|
"version": "1.6.2",
|
||||||
"description": "Shivao app nativo (Capacitor wrapper Android/iOS)",
|
"description": "Shivao app nativo (Capacitor wrapper Android/iOS)",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -3137,6 +3137,8 @@ async function updateStorageInfo(){
|
||||||
setTimeout(maybeAutoFetchWeather,3000);
|
setTimeout(maybeAutoFetchWeather,3000);
|
||||||
// Welcome screen — só pra usuários sem login
|
// Welcome screen — só pra usuários sem login
|
||||||
setTimeout(maybeShowWelcome,300);
|
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
|
// 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));
|
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();
|
if(anchorWatch.active&&!anchorWatch.wakeLock)await requestAnchorWakeLock();
|
||||||
// Reconecta WS ao voltar ao foreground
|
// Reconecta WS ao voltar ao foreground
|
||||||
if(cloudConfigured()&&(!_wsConn||_wsConn.readyState!==WebSocket.OPEN))rtConnect();
|
if(cloudConfigured()&&(!_wsConn||_wsConn.readyState!==WebSocket.OPEN))rtConnect();
|
||||||
|
// Retoma polling Google se sessão pendente
|
||||||
|
resumePollingIfPending();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
window.addEventListener('online',()=>{if(cloudConfigured())rtConnect()});
|
window.addEventListener('online',()=>{if(cloudConfigured())rtConnect()});
|
||||||
|
|
@ -3921,6 +3925,24 @@ function welcomeGoogleClick(){
|
||||||
}
|
}
|
||||||
|
|
||||||
let _googleAuthPolling=null;
|
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(){
|
async function startGoogleRedirectFlow(){
|
||||||
toast('Abrindo Google...');
|
toast('Abrindo Google...');
|
||||||
try{
|
try{
|
||||||
|
|
@ -3930,21 +3952,34 @@ async function startGoogleRedirectFlow(){
|
||||||
if(!r.ok)throw new Error('HTTP '+r.status);
|
if(!r.ok)throw new Error('HTTP '+r.status);
|
||||||
const{url,session}=await r.json();
|
const{url,session}=await r.json();
|
||||||
if(!url||!session)throw new Error('servidor sem URL/session');
|
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)
|
// Abre URL no browser EXTERNO (em Capacitor isso usa Custom Tabs)
|
||||||
if(window.open){window.open(url,'_blank','noopener')}else{location.href=url}
|
if(window.open){window.open(url,'_blank','noopener')}else{location.href=url}
|
||||||
// Inicia polling
|
// 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);
|
if(_googleAuthPolling)clearInterval(_googleAuthPolling);
|
||||||
let tries=0;
|
let tries=0;
|
||||||
_googleAuthPolling=setInterval(async()=>{
|
console.log('[gsi-poll] starting for session',session);
|
||||||
|
// Faz uma chamada IMEDIATA primeiro (caso já esteja pronto)
|
||||||
|
const pollOnce=async()=>{
|
||||||
tries++;
|
tries++;
|
||||||
if(tries>120){clearInterval(_googleAuthPolling);toast('Tempo esgotado. Tente de novo.');return}
|
if(tries>120){clearInterval(_googleAuthPolling);_googleAuthPolling=null;clearPendingSession();toast('Tempo esgotado. Tente de novo.');return}
|
||||||
try{
|
try{
|
||||||
const pr=await fetch(cloudUrl('/api/auth/google/poll?session='+encodeURIComponent(session)));
|
const pr=await fetch(cloudUrl('/api/auth/google/poll?session='+encodeURIComponent(session)));
|
||||||
if(pr.status===204)return; // ainda esperando
|
if(pr.status===204)return; // ainda esperando
|
||||||
if(!pr.ok)return;
|
if(!pr.ok)return;
|
||||||
const j=await pr.json();
|
const j=await pr.json();
|
||||||
if(j.accessToken&&j.refreshToken){
|
if(j.accessToken&&j.refreshToken){
|
||||||
clearInterval(_googleAuthPolling);
|
clearInterval(_googleAuthPolling);_googleAuthPolling=null;
|
||||||
|
clearPendingSession();
|
||||||
state.auth={accessToken:j.accessToken,refreshToken:j.refreshToken,user:j.user};
|
state.auth={accessToken:j.accessToken,refreshToken:j.refreshToken,user:j.user};
|
||||||
saveState();
|
saveState();
|
||||||
toast('Bem-vindo, '+(j.user.name||j.user.email));
|
toast('Bem-vindo, '+(j.user.name||j.user.email));
|
||||||
|
|
@ -3952,11 +3987,21 @@ async function startGoogleRedirectFlow(){
|
||||||
if(typeof renderAuthBox==='function')renderAuthBox();
|
if(typeof renderAuthBox==='function')renderAuthBox();
|
||||||
}
|
}
|
||||||
}catch(e){console.warn('[gsi-poll]',e.message)}
|
}catch(e){console.warn('[gsi-poll]',e.message)}
|
||||||
},2000);
|
};
|
||||||
}catch(e){
|
pollOnce(); // imediato
|
||||||
console.warn('[gsi-redirect]',e);
|
_googleAuthPolling=setInterval(pollOnce,2000);
|
||||||
toast('Erro: '+e.message);
|
}
|
||||||
}
|
|
||||||
|
// 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){
|
async function onGoogleCredential(resp){
|
||||||
if(!resp?.credential){toast('Sem credential do Google');return}
|
if(!resp?.credential){toast('Sem credential do Google');return}
|
||||||
|
|
|
||||||
|
|
@ -347,7 +347,7 @@ app.get('/.well-known/assetlinks.json', (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Atalho: /apk redireciona pra última APK release no Forgejo
|
// 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));
|
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)
|
// Página A4 imprimível com QR Code + instruções (cola no barco/marina)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue