Files
zooqueenandHanzo Dev fc46d59703 money in works: the card mounts, and the page knows who is paying
Self-serve top-up could not complete for anyone. Three faults, one path.

The card field never appeared. `useSquareCard` kept liveness in a ref shared
across effect runs, so a cleanup could flip it false and the attach still in
flight from the CURRENT run would then read "unmounted" and destroy the card it
had just successfully attached. Measured on the live page: the field attaches at
700ms and is gone by 1200ms, leaving no spinner, no error and no card — and Pay
latched disabled forever, because ready never went true. Liveness now belongs to
the run that owns it.

What made the effect run three times was `providers`, which rendered `children`
in three places in turn: bare while a `mounted` flag was false, then as a
Suspense fallback while the wallet chunk loaded, then inside the provider. React
remounts a subtree whose parent changes, so every page mounted three times and
every billing fetch ran twice. The wallet stack is half a megabyte and only the
crypto UI needs it, so the decision is made once now and children never move.

And the page did not know who was paying. It read a bearer token from `?token=`
or a `hanzo-auth-token` cookie — neither of which anything sets. The session is
in localStorage, where `getAccessToken` reads it, so every top-up opened from
our own Add-credit links carried no token and died on submit with
"Authentication required". The subject falls back the same way, to the org slug
commerce keys billing on, so the money lands in the account whose balance the
rest of the app shows.

A card on file is no longer offered here: POST /v1/billing/topup answers 404 to
an authenticated caller on both the gateway and commerce, while the nonce path
beside it answers 400 "sourceId is required". The option could only fail, and it
was selected BY DEFAULT — so restoring the token would have made failing the
default.

Two colours were wrong in the light theme, both because they were spelled
literally. Pay was `#ffffff` on `var(--color-bg)`: white on white, the primary
action on the money page, invisible. It takes the brand pair, which inverts.
The card ink was `#fafafa`, and Square paints its fields on a white surface it
will not let us repaint — `.input-container` backgroundColor is rejected
outright and fails the whole attach — so that was ghost text in the DEFAULT dark
theme too, not only in light. Square's own colours are legible under both.

`square-card-form` carried a second copy of the load/attach/retry/destroy dance,
with the same liveness bug and the same container id as the top-up page, aiming
Square at whichever box mounted first. It uses the hook and its own id.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 02:53:53 -07:00

24 lines
996 B
TypeScript

'use client'
/**
* The WalletConnect project id, and nothing else.
*
* Its own module because two callers need this one fact and only one of them
* can afford the dependencies. `wallet-config` builds the RainbowKit config, so
* it pulls in wagmi and RainbowKit — half a megabyte. `providers` only needs to
* know whether a wallet stack should mount at all, and asking the config would
* drag that bundle into the first paint of every page.
*
* Resolved at CALL time so it picks up the value the serving layer injects from
* KMS as `window.__HANZO_CONFIG__.walletConnectProjectId`, whenever that lands
* relative to this module's load. Empty means crypto is off.
*/
export function projectId(): string {
const runtime =
typeof window !== 'undefined'
? (window as { __HANZO_CONFIG__?: { walletConnectProjectId?: string } })
.__HANZO_CONFIG__?.walletConnectProjectId
: undefined
return runtime || process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || ''
}