mirror of
https://github.com/hanzoai/billing.git
synced 2026-10-30 18:46:54 +00:00
The Tailwind strip left the colour *values* behind: markup still named literal whites and dark-theme hexes, so `html.light` moved the tokens and nothing followed. Light mode rendered near-black text on a near-black page. Same four facts, each with one home now: ground bg-[#09090b] · bg-[#141419] · bg-[#0d0d10] · bg-[#16161c] -> bg-bg · bg-bg-card · bg-bg-elevated ink the whole `*-white/α` family is really "foreground at α%", so it says so: `-ink/α`, mixed from var(--color-text) brand bg-white + text-black pills -> bg-brand + text-brand-foreground (+ .bg-brand-foreground, which the switch knob needed — it was painting white on the white ON-track and vanishing) selection one rule for both themes; the light override painted white text on a pale tint /commerce styled itself entirely with inline dark literals and ignored the flip wholesale — 82 unreadable elements. Its hexes and rgba(255,255,255,α) now read the same tokens, and the H-mark inherits currentColor instead of being permanently white. Also: every self-navigation goes through BASE_PATH. /plans, /transactions, /payment-methods and the auth callback jumped to a bare '/', which walks the browser out of the app whenever the SPA is mounted at /admin/billing inside the commerce binary. billing-shell already did this for /topup — now it is the only way. Verified: next build green; Playwright over the static export in both themes — light /commerce 82 unreadable -> 0, /plans -> /admin/billing/#pricing (was the server root); dark render byte-identical in intent (same alphas, now expressed in oklab). Co-authored-by: Hanzo Dev <[email protected]>
76 lines
2.9 KiB
TypeScript
76 lines
2.9 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import { BASE_PATH, getOrgByHost } from '@/lib/config'
|
|
import { makeAuthConf } from '@/lib/auth'
|
|
import { handleCallback, startLogin, logout } from '@/lib/iam-auth'
|
|
|
|
// One-shot retry flag — survives the authorize round-trip via sessionStorage so
|
|
// a transient first-attempt failure self-heals silently but can never loop.
|
|
const RETRY_KEY = 'iam-callback-retry'
|
|
|
|
export default function CallbackPage() {
|
|
const [error, setError] = useState<string | null>(null)
|
|
// Guard against the effect running more than once (StrictMode double-invoke
|
|
// or a re-render) — handleCallback consumes the one-time auth code, so a
|
|
// second run would otherwise fail.
|
|
const ran = useRef(false)
|
|
|
|
useEffect(() => {
|
|
if (ran.current) return
|
|
ran.current = true
|
|
|
|
const org = getOrgByHost(window.location.host)
|
|
const conf = makeAuthConf(org)
|
|
|
|
handleCallback(conf)
|
|
.then(() => {
|
|
sessionStorage.removeItem(RETRY_KEY)
|
|
window.location.href = `${BASE_PATH}/`
|
|
})
|
|
.catch((err) => {
|
|
const msg = err instanceof Error ? err.message : 'Authentication failed'
|
|
// A consumed/stale authorization code or a lost PKCE verifier is a
|
|
// transient first-attempt failure that a fresh authorize round-trip
|
|
// resolves. Recover automatically ONCE (guarded so it cannot loop) by
|
|
// clearing the stale PKCE/token state and restarting sign-in, so the
|
|
// user sees a brief spinner instead of a spurious "Authentication Error".
|
|
const transient = /invalid_grant|authorization code|code[_ ]?verifier|expired/i.test(msg)
|
|
if (transient && !sessionStorage.getItem(RETRY_KEY)) {
|
|
sessionStorage.setItem(RETRY_KEY, '1')
|
|
logout() // clear any stale token + PKCE/state so the retry starts clean
|
|
startLogin(conf).catch(() => setError(msg))
|
|
return
|
|
}
|
|
sessionStorage.removeItem(RETRY_KEY)
|
|
setError(msg)
|
|
})
|
|
}, [])
|
|
|
|
if (error) {
|
|
return (
|
|
<main className="flex min-h-screen items-center justify-center bg-bg p-8">
|
|
<div className="max-w-md rounded-2xl border border-border bg-bg-card p-8 text-center">
|
|
<h1 className="text-lg font-semibold text-text">Authentication Error</h1>
|
|
<p className="mt-3 text-sm text-text-muted">{error}</p>
|
|
<a
|
|
href="/"
|
|
className="mt-6 inline-block rounded-lg bg-brand px-4 py-2 text-sm font-medium text-brand-foreground transition hover:bg-brand-hover"
|
|
>
|
|
Return to billing
|
|
</a>
|
|
</div>
|
|
</main>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<main className="flex min-h-screen items-center justify-center bg-bg">
|
|
<div className="text-center">
|
|
<div className="mx-auto h-6 w-6 animate-spin rounded-full border-2 border-text-dim border-t-brand" />
|
|
<p className="mt-4 text-sm text-text-muted">Completing sign in...</p>
|
|
</div>
|
|
</main>
|
|
)
|
|
}
|