Files
zooqueen c362541bce the funnel events reach the door
plan_clicked and checkout_started were sent by a bare beacon with no
credential, and the door refuses a write it cannot file under a project —
401 ingest_key_required — so every one of them was discarded. The money
funnel had no steps after the click because none of them ever landed.

They go through @hanzo/event now, which carries the publishable key for an
anonymous visitor and the buyer's own IAM token when there is one. That also
supplies the anonymous id, a first-party cookie on hanzo.ai, so a visitor who
read pricing on the marketing site is the same person here and the steps join.

The plans grid emits pricing_viewed: it is a tab, not a page, so a pageview
never saw it, and a buyer who arrives straight at billing looked like a plan
click from nowhere.
2026-08-15 10:12:18 -07:00

42 lines
1.8 KiB
TypeScript

'use client'
import * as React from 'react'
import { AnalyticsProvider } from '@hanzo/event/react'
import { analytics } from './analytics'
import { projectId } from './wallet'
// wagmi + RainbowKit are half a megabyte and only the crypto payment UI needs
// them, so they load lazily and only where a WalletConnect id is configured.
const WalletProviders = React.lazy(() => import('./wallet-providers'))
export function Providers({ children }: { children: React.ReactNode }) {
// Decided ONCE, on first render, and never revisited — because `children`
// must keep the same position in the tree for their whole life.
//
// This used to render them 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. Two symptoms, one cause: every billing
// fetch ran twice, and the in-flight Square card attach was torn down and
// never retried, which left the top-up form permanently empty and the Pay
// button permanently disabled — self-service money-in could not complete.
const [wallet] = React.useState(() => projectId() !== '')
// Telemetry wraps BOTH paths and is never conditional, for the same reason the
// wallet decision is frozen: a provider that appears or disappears remounts
// everything under it. It also has to be outside the wallet branch to count a
// visitor who never loads the wallet chunk, which is most of them.
return (
<AnalyticsProvider client={analytics}>
{wallet ? (
<React.Suspense fallback={null}>
<WalletProviders>{children}</WalletProviders>
</React.Suspense>
) : (
children
)}
</AnalyticsProvider>
)
}