mirror of
https://github.com/hanzoai/finance.git
synced 2026-10-30 18:36:28 +00:00
Deliverable 1 of the two customer-facing Hanzo Finance surfaces. The umbrella repo
now carries the frontend alongside the Go financial core (embedded in cloud).
packages/finance-ui (@hanzo/finance-ui, PUBLISHED 0.1.1 to npm) — the DRY core BOTH
surfaces render, so a spend/usage/credits card is identical in finance.hanzo.ai and the
console Finance module:
- FinanceClient boundary: stubFinanceClient() (preview/dev) ⇄ httpFinanceClient(transport)
(live /v1/finance/*); the host injects auth/transport, swap = one line, zero UI change.
- Optional-safe normalizers (renamed/absent field -> em-dash; card masked to brand+last4
by construction; USD cents end to end). FinanceDashboard + monochrome, theme-aware,
self-injecting styles (no CSS import). tsc clean; 27 unit tests.
web (finance.hanzo.ai, Next 15) — landing + Hanzo IAM login + treasury/payments app shell:
- Marketing landing: canonical Hanzo mark (viewBox 0 0 67 67 / 44.6369), tight monochrome
copy, embeds the SHARED board as the product preview.
- Auth: hanzo-finance PUBLIC PKCE OAuth (Hanzo IAM, canonical /v1/iam/oauth/*), secretless
BFF code exchange (+optional KMS confidential upgrade), sealed httpOnly session, AuthGate.
- App shell renders the shared FinanceDashboard over /v1/finance/* via a bearer BFF proxy
(stub preview until NEXT_PUBLIC_FINANCE_MODE=live). Build green (11/11 routes); the Next
standalone server boots + serves verified.
- web/iam/: hanzo-finance app REGISTERED + VERIFIED LIVE in IAM (get-app-login resolves,
no 'Invalid client_id'); JSON + runbook committed for re-provisioning.
CI: root hanzo.yml (images finance-web + test + deploy) + ~15-line cicd.yml importing
hanzoai/ci@main on arc (zero GHA minutes). web/Dockerfile builds the pnpm-workspace Next
standalone image — built by CI/CD, never locally.
59 lines
1.7 KiB
TypeScript
59 lines
1.7 KiB
TypeScript
'use client'
|
|
|
|
import { Suspense, useEffect, useState } from 'react'
|
|
import { useRouter, useSearchParams } from 'next/navigation'
|
|
import { completeSignin, takeReturnTo } from '~/lib/auth/iam'
|
|
import { HanzoMark } from '~/components/Logo'
|
|
|
|
/**
|
|
* IAM OAuth callback. IAM returns `?code&state`; we redeem the code via the BFF (secretless
|
|
* PKCE) to establish the session cookie, then land the user back where they started.
|
|
*/
|
|
function Callback(): React.JSX.Element {
|
|
const params = useSearchParams()
|
|
const router = useRouter()
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
const code = params?.get('code')
|
|
const err = params?.get('error')
|
|
if (err) {
|
|
setError(err === 'access_denied' ? 'Sign-in was cancelled.' : err)
|
|
return
|
|
}
|
|
if (!code) {
|
|
setError('Missing authorization code.')
|
|
return
|
|
}
|
|
completeSignin(code)
|
|
.then(() => router.replace(takeReturnTo()))
|
|
.catch((e: unknown) => setError(e instanceof Error ? e.message : 'Sign-in failed.'))
|
|
}, [params, router])
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="fin-center">
|
|
<HanzoMark size={30} />
|
|
<div style={{ fontWeight: 600 }}>{error}</div>
|
|
<button className="fin-btn" onClick={() => router.replace('/signin')}>
|
|
Back to sign in
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
return (
|
|
<div className="fin-center">
|
|
<div className="fin-spinner" />
|
|
<div className="fin-muted">Completing sign-in…</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function CallbackPage(): React.JSX.Element {
|
|
return (
|
|
<Suspense fallback={<div className="fin-center"><div className="fin-spinner" /></div>}>
|
|
<Callback />
|
|
</Suspense>
|
|
)
|
|
}
|