fix(storefront): Refresh passport token before loading checkout app when near expiry - #785
fix(storefront): Refresh passport token before loading checkout app when near expiry#785vitorrgg wants to merge 1 commit into
Conversation
…hen near expiry When a recurring customer opens checkout with a passport token expiring within 2 minutes, the storefront-app would load with the stale token, causing fetchCustomer to 401 and leaving the user stuck on the "Complete seu cadastro" screen instead of the login or checkout step. Now checks synchronously if the stored token needs refresh before loading app.js. Only users in the ~2-minute expiry window experience a brief extra delay (Firebase authStateReady + /_api/passport/token). All other users — new, anonymous, or with a fresh token — load immediately with no change in behavior. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
leomp12
left a comment
There was a problem hiding this comment.
Revisei o diff com foco em caminho de regressão no checkout e em latência no caminho feliz. Encontrei dois problemas bloqueantes. Deixo tudo abaixo com as referências para dupla checagem, porque parte do raciocínio depende de semântica do Vue e do @ecomplus/passport-client, e eu posso ter lido algo errado.
Resumo: pelo que consegui rastrear, o fix não corrige o bug relatado (o token renovado não chega no app.js) e introduz um caminho onde o checkout pode não carregar. Além disso, o delay atinge uma população bem maior que a "janela de ~2 minutos" descrita.
packages/storefront não tem nenhuma suíte de testes — não existe *.test.ts/*.spec.ts nem script test no package.json. Todo o comportamento aqui depende de estado de runtime (cookie, localStorage, IndexedDB do Firebase, timing de rede). QA manual é o único gate possível. Checklist proposto no final.
🔴 Crítico 1 — o token renovado não chega no app.js
O ecomPassport lê a sessão do cookie ecomPassportClient no momento em que é construído. Conferido no fonte instalado (@ecomplus/passport-client@1.2.1):
src/constructor.js:52—storageKey = 'ecomPassportClient'(default)src/constructor.js:188—loadStoredSession(ecomPassport)roda na construção, ou seja, quando oapp.jscarregasrc/methods/load-stored-session.js:23— a sessão vem degetCookie(document, storageKey). OlocalStoragesó fornecesession.customer, nunca oauth.token
Quem escreve esse cookie é o watcher em vbeta-app.ts:237, com immediate: true.
Traçando o cenário-alvo do PR (token expirando em ~90s):
immediate: trueroda sincronamente no init.isAuthenticatedétrue(a regra emcustomer-session.ts:30só exige> 10sde validade) → cookie gravado com o token velho.- O novo bloco (
vbeta-app.ts:327-336) fazawait authenticate()→session.authé atualizado em memória. isAuthenticatedrecalcula → continuatrue.watchno Vue só dispara quando o valor muda (comparaçãoObject.is, semdeep/forceTriggerpara umComputedRef) → o callback não roda de novo →setCookienunca é chamado com o token novo.loadAppScript()→ecomPassportlê o cookie → token velho →fetchCustomer401 → "Complete seu cadastro".
Ou seja: paga-se o round-trip e o sintoma permanece. O fluxo só funciona por acaso quando o authenticate() disparado pelo onAuthStateChanged (customer-session.ts:123) ganha a corrida e faz isAuthenticated ir de false → true — o que já acontecia sem este PR.
🔴 Crítico 2 — o checkout pode nunca carregar
initializingAuth (vbeta-app.ts:300-307) não tem reject nem timeout. isAuthReady só vira true dentro do import('../scripts/firebase-app'), que termina em .catch(console.error) (customer-session.ts:146).
Se esse import falhar, a promise nunca settla → o .then não roda, e o .catch(() => loadAppScript()) da linha 336 também não (promise pendente não rejeita) → loadAppScript() nunca é chamado → tela branca.
Falhas reais que caem nisso:
- Deploy. O HTML do checkout é
max-age=300, stale-while-revalidate=900e_astro/**éimmutable. Na janela de ~15 min após cada deploy, HTML em cache aponta parafirebase-app.<hash-antigo>.js, que não existe mais no release atual → 404 →vite:preloadError→ engolido pelo.catch. - Adblock / extensão de privacidade / proxy corporativo bloqueando Firebase.
- Queda de rede durante o download do chunk (56 KB + 2 deps).
- Fluxo
isSignInWithEmailLink(customer-session.ts:133-143) quandosignInWithEmailLinkrejeita.
Antes deste PR isso só afetava URLs com #account. Agora afeta a entrada principal do checkout, e a falha piora de "preso no cadastro" (recuperável) para "página em branco" (não recuperável).
🟠 Alto — o delay atinge muito mais gente que a janela de 2 min
storedTokenExpiresAt - Date.now() < 2 * 60 * 1000É true para token expirado há 2 minutos, 2 dias ou 2 meses. E o localStorage.ecomSession nunca é limpo por expiração — só no logout() (customer-session.ts:154-165). Na prática a população que entra no caminho bloqueante é todo usuário que já logou naquele device e cujo token venceu, não a janela de ~2 min descrita na descrição do PR.
E para esses o fix não faz nada: isAuthenticated.value é false (exige > 10s), então o authenticate() da linha 332 é pulado. Eles esperam e não ganham nada.
Custo dessa espera, serializado antes do app.js sequer começar a baixar:
import(firebase-app)— 56 KB + 2 deps. E como o hosting está comcleanUrls: true, a URL canônica do checkout parece ser/app(sem barra), o que fazcanWaitIdle = !pathname.startsWith('/app/')dartrue→requestIdleCallback(runImport)sem timeout (sf-utils.ts:10-16). Em página de checkout com main thread ocupada isso pode atrasar de centenas de ms a segundos. (vale confirmar a URL real em produção — se for/app/com barra, esse item cai)authStateReady()— leitura de IndexedDB + eventual refresh emsecuretoken.googleapis.com.authenticate()quando roda —getIdToken()+ POST/_api/passport/token, que é Cloud Function fazendoverifyIdToken+ leitura no Firestore + possível POSTauthenticatena API. Cold start = segundos.
🟡 Menores
- Skew de relógio. Tudo depende do
Date.now()do device. Relógio adiantado → todo retornante cai no caminho lento. - Sem margem contra o servidor.
getCustomerToken(packages/passport/src/firebase/authenticate-customer.ts:92) devolve o token cacheado se restar>= 2min— mesma constante do cliente. Na fronteira, o cliente bloqueia por um round-trip e recebe o mesmo token de volta. authenticate()duplicado. OonAuthStateChangedtambém chama, em paralelo. Duas invocações da function por load, com janela de last-write-wins emsession.auth.- ✅ Ponto bom: o cálculo em
vbeta-app.ts:308-310é síncrono (useStoragelêlocalStorageno construtor), então usuário anônimo e usuário com token fresco realmente não pagam nada. Isso está correto.
Caminho sugerido
A raiz é uma só: isAuthenticated considera "autenticado" um token com 10s de vida. Isso semeia o cookie com token moribundo e suprime a transição false → true que entregaria o token renovado.
Dá para resolver os dois sintomas sem nenhuma espera, tudo síncrono:
- Predicado local com margem real (
> 2min) para decidir o que vai pro cookie, e limpar o cookie (setCookie(key, '', -1), mesmo padrão que o próprio passport-client usa emload-stored-session.js:29) quando o token não está fresco e oapp.jsainda não bootou. - Manter o logout forçado no critério antigo, em watcher separado — trocar o predicado nessa parte faria
ecomPassport.logout()disparar o handler devbeta-app.ts:281-287, que chamalogout()no Firebase e redireciona para/, chutando o usuário do checkout. - Chamar
loadAppScript()primeiro, sempre, e renovar em background. Quando oauthenticate()retorna, o watcher do item 1 disparaecomPassport.setSession()e o usuário é logado assincronamente. UsarfirebaseAuth.currentUserem vez deisAuthenticated.valuecobre também o token já expirado.
Efeito colateral honesto: usuário com token entre 10s e 2min passa a bootar anônimo e ser relogado ~1-2s depois. Se o Firebase não tiver sessão persistida, fica anônimo — o que é preferível a atrasar o checkout de todo mundo.
Opcional, em deploy separado: subir o threshold de authenticate-customer.ts:92 de 2 para 5 min, para garantir que todo token que o cliente rejeita volte genuinamente renovado.
✅ QA manual — obrigatório antes do merge
Sem cobertura automatizada, nada aqui se valida sem teste manual. Sugestão de roteiro. Em todos, medir no Network waterfall o tempo até o app.js começar a baixar, comparando com main:
| # | Cenário | Como reproduzir | O que verificar |
|---|---|---|---|
| 1 | Token expirando em <2min (caso-alvo) | Logar, editar localStorage.ecomSession → auth.expires para now + 90s, abrir o checkout |
Inspecionar document.cookie → ecomPassportClient contém o token novo ou o velho? É aqui que acredito que o fix falha |
| 2 | Token expirado há horas | auth.expires para now - 3h, com cookie ecomPassportClient ainda presente |
Tempo até renderizar; e se o usuário cai no login ou fica preso no cadastro |
| 3 | Token expirado + Firebase deslogado | Cenário 2 + limpar IndexedDB firebaseLocalStorageDb |
Não pode travar |
| 4 | Chunk do Firebase indisponível | DevTools → Network → Block request URL em _astro/firebase-app*.js |
Teste do Crítico 2. O checkout ainda carrega? |
| 5 | Adblock ativo | uBlock/Brave Shields bloqueando Firebase | Idem #4 |
| 6 | Rede lenta + function fria | Throttling "Slow 3G" + primeira chamada do dia em /_api/passport/token |
Delta de tempo até o checkout renderizar vs. main |
| 7 | Relógio adiantado | Adiantar o relógio do SO em +10min | Confirmar quanto da base cai no caminho lento |
| 8 | Caminho feliz — anônimo e token fresco | Sessão nova; e sessão com token de várias horas | Confirmar zero regressão de tempo. É o requisito mais importante |
Os cenários 1 e 4 são os decisivos: o 1 diz se o PR resolve o que se propõe, e o 4 diz se ele quebra o checkout.
Vale confirmar principalmente os pontos 1 e 2 antes de qualquer coisa — se eu estiver errado sobre o watcher não redisparar, boa parte da análise muda.
When a recurring customer opens checkout with a passport token expiring within 2 minutes, the storefront-app would load with the stale token, causing fetchCustomer to 401 and leaving the user stuck on the "Complete seu cadastro" screen instead of the login or checkout step.
Now checks synchronously if the stored token needs refresh before loading app.js. Only users in the ~2-minute expiry window experience a brief extra delay (Firebase authStateReady + /_api/passport/token). All other users — new, anonymous, or with a fresh token — load immediately with no change in behavior.