mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-06 22:09:44 +02:00
* feat(billing): test page consuming /api/cloud-billing/*
Stuffs every cloud-billing endpoint onto a single dev page so we can
verify the proxy + Stripe end-to-end flow without a designed UI.
Reachable at /<workspaceSlug>/billing — the page is account-level
data but lives under the workspace dashboard layout because that's
where the authenticated shell sits. No sidebar entry on purpose;
this is test-quality and meant to be deleted when the real billing
UI ships.
What's there:
* Balance card (GET /balance)
* Stripe-success polling banner — visible only when ?session_id=
is in the URL (Stripe substitutes it into checkout_success_url
on its way back). React Query refetchInterval polls every 2s
until the topup status reaches credited / failed / canceled,
then a 'Clear from URL' button calls navigation.replace(pathname).
* Buy section: server-authoritative price tier buttons (GET
/price-tiers) → POST /checkout-sessions → window.location to the
Stripe URL. We do NOT hard-code amounts on the frontend; tier
config lives in cloud's billing.price_tiers.
* Stripe Billing Portal button (POST /portal-sessions). Opens in a
new tab so the originating page stays put for easy verification.
Documented behaviour: 400 is expected for users with no Stripe
customer record yet.
* Three lists: transactions / batches / topups.
Plumbing:
* packages/core/types/billing.ts — interfaces mirroring the cloud
response shapes. Status / source / tx_type fields are typed
'string' rather than enum unions to match the schemas' z.string()
parsing (same convention as CloudRuntimeNode); the canonical
enum values are exported as separate type aliases for callers
that want to switch on them.
* packages/core/api/schemas.ts — 9 zod schemas + 7 EMPTY_ fallbacks,
all .loose() so a non-breaking cloud-side field addition doesn't
crash the parser.
* packages/core/api/client.ts — 8 methods using parseWithFallback,
matching the existing cloud-runtime shape.
* packages/core/billing/{queries,mutations,index}.ts — React Query
queryOptions + mutations. Notable choices: balance / lists are
NOT keyed on workspace (account-level data), and the
checkout-session polling stops automatically when status is
terminal so we don't poll forever after a user closes the tab.
* packages/core/package.json + packages/views/package.json — exports
map updated for @multica/core/billing and @multica/views/billing.
Verification:
* pnpm --filter @multica/core typecheck clean
* pnpm --filter @multica/views typecheck — only pre-existing
hast-util-to-html error in editor code (exists on main)
* pnpm --filter @multica/core test — 412 passing
* pnpm --filter @multica/views test — 877 passing, 1 failure
(editor/readonly-content) is also pre-existing on main, not
caused by this change
Out of scope: real production-quality billing UI; sidebar entry; i18n
strings; mobile app. This is a single test page; it gets replaced
when the real UI ships.
* fix(billing): refetch balance/lists when checkout polling reaches terminal
Closes the second-half of the Stripe-return race the previous commit
left dangling.
Symptom:
After Stripe redirects back with ?session_id=..., the banner polls
/checkout-sessions/{id} every 2s and the rest of the page (balance,
transactions, batches, topups) is fetched once on mount. The
webhook race means those four queries usually see pre-credit state
— but the banner is the only thing that keeps polling, so once it
reads 'credited' nothing else on the page knows. The user would
see 'Final status: credited' next to a stale balance card until
they manually refresh.
Fix:
Add useInvalidateBillingDataAfterCredit() in @multica/core/billing —
a hook returning a callback that flushes balance / transactions /
batches / topups (NOT the checkout-session itself; its
refetchInterval already terminated, refetching would just confirm
the same value). The Stripe-success banner runs this callback in
a useEffect keyed on terminal-status transition, so it fires
exactly once when the polling lands.
Strict scope is documented in the hook's JSDoc:
- balance/transactions/batches: only change at the 'credited'
transition (cloud writes ledger + batch + wallet in one DB tx)
- topups: changes on every terminal transition
- For 'failed' / 'canceled' we technically over-fetch the first
three; three cheap round-trips, simplifies the call site, fine
on a test page.
Effect dep is . terminal flips false→true at most once
per session id (the polling stops when terminal is true so the
data won't change again). If the user lands here with a session
that is already terminal (re-opened tab on a credited URL), the
effect still fires on first data load and we still re-fetch —
correct, the cached snapshot is just as stale in that case.
go build / pnpm typecheck / pnpm test clean (core 412 passing; only
pre-existing hast-util-to-html error in unrelated editor code on
views, same as on main).
127 lines
4.6 KiB
JSON
127 lines
4.6 KiB
JSON
{
|
|
"name": "@multica/views",
|
|
"version": "0.0.0",
|
|
"private": true,
|
|
"type": "module",
|
|
"scripts": {
|
|
"typecheck": "tsc --noEmit",
|
|
"lint": "eslint .",
|
|
"test": "vitest run"
|
|
},
|
|
"exports": {
|
|
"./navigation": "./navigation/index.ts",
|
|
"./common/actor-avatar": "./common/actor-avatar.tsx",
|
|
"./common/markdown": "./common/markdown.tsx",
|
|
"./editor": "./editor/index.ts",
|
|
"./issues/components": "./issues/components/index.ts",
|
|
"./issues/hooks": "./issues/hooks/index.ts",
|
|
"./issues/utils/filter": "./issues/utils/filter.ts",
|
|
"./issues/utils/sort": "./issues/utils/sort.ts",
|
|
"./common/task-transcript": "./common/task-transcript/index.ts",
|
|
"./projects/components": "./projects/components/index.ts",
|
|
"./autopilots/components": "./autopilots/components/index.ts",
|
|
"./modals/registry": "./modals/registry.tsx",
|
|
"./modals/create-issue": "./modals/create-issue.tsx",
|
|
"./my-issues": "./my-issues/index.ts",
|
|
"./skills": "./skills/index.ts",
|
|
"./agents": "./agents/index.ts",
|
|
"./members": "./members/index.ts",
|
|
"./inbox": "./inbox/index.ts",
|
|
"./runtimes": "./runtimes/index.ts",
|
|
"./billing": "./billing/index.ts",
|
|
"./dashboard": "./dashboard/index.ts",
|
|
"./squads": "./squads/index.ts",
|
|
"./squads/components": "./squads/components/index.ts",
|
|
"./workspace/workspace-avatar": "./workspace/workspace-avatar.tsx",
|
|
"./workspace/create-workspace-form": "./workspace/create-workspace-form.tsx",
|
|
"./workspace/no-access-page": "./workspace/no-access-page.tsx",
|
|
"./workspace/new-workspace-page": "./workspace/new-workspace-page.tsx",
|
|
"./workspace/welcome-after-onboarding": "./workspace/welcome-after-onboarding.tsx",
|
|
"./workspace/use-workspace-seen": "./workspace/use-workspace-seen.ts",
|
|
"./layout": "./layout/index.ts",
|
|
"./auth": "./auth/index.ts",
|
|
"./search": "./search/index.ts",
|
|
"./chat": "./chat/index.ts",
|
|
"./settings": "./settings/index.ts",
|
|
"./invite": "./invite/index.ts",
|
|
"./invitations": "./invitations/index.ts",
|
|
"./onboarding": "./onboarding/index.ts",
|
|
"./platform": "./platform/index.ts",
|
|
"./i18n": "./i18n/index.ts",
|
|
"./attachments": "./attachments/index.ts",
|
|
"./locales": "./locales/index.ts",
|
|
"./locales/*": "./locales/*"
|
|
},
|
|
"dependencies": {
|
|
"@base-ui/react": "^1.3.0",
|
|
"@dnd-kit/core": "^6.3.1",
|
|
"@dnd-kit/sortable": "^10.0.0",
|
|
"@dnd-kit/utilities": "^3.2.2",
|
|
"@floating-ui/dom": "^1.7.6",
|
|
"@multica/core": "workspace:*",
|
|
"@multica/ui": "workspace:*",
|
|
"@tiptap/core": "^3.22.1",
|
|
"@tiptap/extension-code-block-lowlight": "^3.22.1",
|
|
"@tiptap/extension-document": "^3.22.1",
|
|
"@tiptap/extension-image": "^3.22.1",
|
|
"@tiptap/extension-link": "^3.22.1",
|
|
"@tiptap/extension-list": "^3.22.1",
|
|
"@tiptap/extension-mention": "^3.22.1",
|
|
"@tiptap/extension-paragraph": "^3.22.1",
|
|
"@tiptap/extension-placeholder": "^3.22.1",
|
|
"@tiptap/extension-table": "^3.22.1",
|
|
"@tiptap/extension-table-cell": "^3.22.1",
|
|
"@tiptap/extension-table-header": "^3.22.1",
|
|
"@tiptap/extension-table-row": "^3.22.1",
|
|
"@tiptap/extension-text": "^3.22.1",
|
|
"@tiptap/extension-typography": "^3.22.1",
|
|
"@tiptap/markdown": "^3.22.1",
|
|
"@tiptap/pm": "^3.22.1",
|
|
"@tiptap/react": "^3.22.1",
|
|
"@tiptap/starter-kit": "^3.22.1",
|
|
"@tiptap/suggestion": "^3.22.1",
|
|
"cmdk": "^1.1.1",
|
|
"hast-util-to-html": "^9.0.5",
|
|
"katex": "catalog:",
|
|
"lowlight": "^3.3.0",
|
|
"mermaid": "catalog:",
|
|
"motion": "^12.38.0",
|
|
"pinyin-pro": "3.26.0",
|
|
"react-markdown": "^10.1.0",
|
|
"react-resizable-panels": "^4.7.5",
|
|
"react-virtuoso": "catalog:",
|
|
"recharts": "3.8.0",
|
|
"rehype-katex": "catalog:",
|
|
"rehype-raw": "^7.0.0",
|
|
"remark-breaks": "^4.0.0",
|
|
"remark-gfm": "^4.0.1",
|
|
"remark-math": "catalog:",
|
|
"rehype-sanitize": "^6.0.0",
|
|
"sonner": "^2.0.7"
|
|
},
|
|
"peerDependencies": {
|
|
"@tanstack/react-query": "catalog:",
|
|
"@tanstack/react-table": "catalog:",
|
|
"i18next": "catalog:",
|
|
"lucide-react": "catalog:",
|
|
"react": "catalog:",
|
|
"react-dom": "catalog:",
|
|
"react-i18next": "catalog:",
|
|
"zustand": "catalog:"
|
|
},
|
|
"devDependencies": {
|
|
"@multica/eslint-config": "workspace:*",
|
|
"@multica/tsconfig": "workspace:*",
|
|
"@testing-library/jest-dom": "catalog:",
|
|
"@testing-library/react": "catalog:",
|
|
"@testing-library/user-event": "catalog:",
|
|
"@types/react": "catalog:",
|
|
"@types/react-dom": "catalog:",
|
|
"@vitejs/plugin-react": "catalog:",
|
|
"eslint-plugin-i18next": "catalog:",
|
|
"jsdom": "catalog:",
|
|
"typescript": "catalog:",
|
|
"vitest": "catalog:"
|
|
}
|
|
}
|