Skip to content

fix(storefront): Refresh passport token before loading checkout app when near expiry - #785

Open
vitorrgg wants to merge 1 commit into
mainfrom
fix/checkout-passport-token-refresh
Open

fix(storefront): Refresh passport token before loading checkout app when near expiry#785
vitorrgg wants to merge 1 commit into
mainfrom
fix/checkout-passport-token-refresh

Conversation

@vitorrgg

Copy link
Copy Markdown
Member

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.

…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>
@vitorrgg
vitorrgg requested a review from leomp12 July 24, 2026 22:40

@leomp12 leomp12 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

⚠️ Este PR não tem como ser validado por código. 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:52storageKey = 'ecomPassportClient' (default)
  • src/constructor.js:188loadStoredSession(ecomPassport) roda na construção, ou seja, quando o app.js carrega
  • src/methods/load-stored-session.js:23 — a sessão vem de getCookie(document, storageKey). O localStorage só fornece session.customer, nunca o auth.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):

  1. immediate: true roda sincronamente no init. isAuthenticated é true (a regra em customer-session.ts:30 só exige > 10s de validade) → cookie gravado com o token velho.
  2. O novo bloco (vbeta-app.ts:327-336) faz await authenticate()session.auth é atualizado em memória.
  3. isAuthenticated recalcula → continua true. watch no Vue só dispara quando o valor muda (comparação Object.is, sem deep/forceTrigger para um ComputedRef) → o callback não roda de novosetCookie nunca é chamado com o token novo.
  4. loadAppScript()ecomPassport lê o cookie → token velhofetchCustomer 401 → "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=900 e _astro/** é immutable. Na janela de ~15 min após cada deploy, HTML em cache aponta para firebase-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) quando signInWithEmailLink rejeita.

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á com cleanUrls: true, a URL canônica do checkout parece ser /app (sem barra), o que faz canWaitIdle = !pathname.startsWith('/app/') dar truerequestIdleCallback(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 em securetoken.googleapis.com.
  • authenticate() quando roda — getIdToken() + POST /_api/passport/token, que é Cloud Function fazendo verifyIdToken + leitura no Firestore + possível POST authenticate na 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. O onAuthStateChanged também chama, em paralelo. Duas invocações da function por load, com janela de last-write-wins em session.auth.
  • Ponto bom: o cálculo em vbeta-app.ts:308-310 é síncrono (useStoragelocalStorage no 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:

  1. 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 em load-stored-session.js:29) quando o token não está fresco e o app.js ainda não bootou.
  2. Manter o logout forçado no critério antigo, em watcher separado — trocar o predicado nessa parte faria ecomPassport.logout() disparar o handler de vbeta-app.ts:281-287, que chama logout() no Firebase e redireciona para /, chutando o usuário do checkout.
  3. Chamar loadAppScript() primeiro, sempre, e renovar em background. Quando o authenticate() retorna, o watcher do item 1 dispara ecomPassport.setSession() e o usuário é logado assincronamente. Usar firebaseAuth.currentUser em vez de isAuthenticated.value cobre 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.ecomSessionauth.expires para now + 90s, abrir o checkout Inspecionar document.cookieecomPassportClient 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants