Files
billing/lib/wallet-config.ts
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

53 lines
1.7 KiB
TypeScript

'use client'
import { getDefaultConfig } from '@rainbow-me/rainbowkit'
import { http } from 'viem'
import {
mainnet,
polygon,
optimism,
arbitrum,
base,
avalanche,
bsc,
} from 'wagmi/chains'
import { projectId } from './wallet'
// CORS-enabled public RPCs. wagmi/viem's DEFAULT mainnet transport is
// eth.merkle.io, which does NOT send Access-Control-Allow-Origin, so every
// chain read from the browser fails CORS and wagmi retries in a tight loop
// (the postMessage storm). publicnode.com endpoints are public, free, and
// CORS-enabled, so the reads succeed and the loop stops.
const TRANSPORTS = {
[mainnet.id]: http('https://ethereum-rpc.publicnode.com'),
[polygon.id]: http('https://polygon-bor-rpc.publicnode.com'),
[optimism.id]: http('https://optimism-rpc.publicnode.com'),
[arbitrum.id]: http('https://arbitrum-one-rpc.publicnode.com'),
[base.id]: http('https://base-rpc.publicnode.com'),
[avalanche.id]: http('https://avalanche-c-chain-rpc.publicnode.com'),
[bsc.id]: http('https://bsc-rpc.publicnode.com'),
}
// Lazy-init to avoid crashing the entire app when projectId is not yet configured.
// RainbowKit's getDefaultConfig throws if projectId is empty.
let _walletConfig: ReturnType<typeof getDefaultConfig> | null = null
export function getWalletConfig() {
if (!_walletConfig) {
const id = projectId()
if (!id) {
// Don't memoize on miss — a later call (after KMS config lands) can succeed.
return null
}
_walletConfig = getDefaultConfig({
appName: 'Hanzo Billing',
projectId: id,
chains: [mainnet, polygon, optimism, arbitrum, base, avalanche, bsc],
transports: TRANSPORTS,
ssr: false,
})
}
return _walletConfig
}