Payments
Section titled “Payments”In-app plan selection and Stripe Checkout via @nebulr-group/bridge-react.
There are two ways to consume billing state:
- The
useBridge()billing surface + drop-in components (recommended) — live, reactive, zero wiring. See Billing 2.0 below. - The classic
<PlanSelector>+useSubscription()store — the original checkout flow. Still fully supported; covered first below.
Install the optional peer
Section titled “Install the optional peer”For Stripe Checkout flows:
bun add @stripe/stripe-js(Not needed if all your plans are free.)
Plan selection page
Section titled “Plan selection page”import { PlanSelector } from '@nebulr-group/bridge-react';
export default function SubscriptionPage() { return ( <PlanSelector successUrl={`${window.location.origin}/subscription/success`} cancelUrl={`${window.location.origin}/subscription/cancel`} /> );}Register it as a route in your router (React Router shown):
<Route path="/subscription" element={<SubscriptionPage />} /><PlanSelector>:
- Loads plans + subscription status on mount via
loadSubscription(). - Renders a card per plan.
- Free plans →
selectFreePlan(planKey)(no redirect). - Paid plans, no existing payment method → Stripe Checkout via
startCheckout. - Paid plans, existing payment method →
changePlan(in-app, no redirect).
Success and cancel pages
Section titled “Success and cancel pages”import { useEffect } from 'react';import { loadSubscription } from '@nebulr-group/bridge-react';import { Link } from 'react-router-dom';
export default function SuccessPage() { useEffect(() => { void loadSubscription(); }, []); return <p>Subscription active. <Link to="/">Continue</Link></p>;}
// pages/SubscriptionCancelPage.tsximport { Link } from 'react-router-dom';
export default function CancelPage() { return <p>Checkout cancelled. <Link to="/subscription">Try again</Link></p>;}Register both as routes:
<Route path="/subscription/success" element={<SuccessPage />} /><Route path="/subscription/cancel" element={<CancelPage />} />Why the successUrl matters
Section titled “Why the successUrl matters”Stripe Checkout appends ?session_id=cs_test_… to successUrl. <BridgeProvider>’s mount effect saves this to sessionStorage before any client-side router can strip it. The next loadSubscription() call picks it up and the backend syncs the subscription.
If the session_id is lost, the user may see a stale “no plan” state after checkout — that’s the symptom of broken preservation. The OAuth callback handler also preserves the ?payment=... query param through the redirect for the same reason.
Reading subscription state elsewhere
Section titled “Reading subscription state elsewhere”import { useSubscription, loadSubscription } from '@nebulr-group/bridge-react';import { useEffect } from 'react';
export function PlanBadge() { const { status, loading } = useSubscription(); useEffect(() => { if (!status && !loading) void loadSubscription(); }, [status, loading]); return <span>{status?.plan ?? 'No plan'}</span>;}Common pitfalls
Section titled “Common pitfalls”@stripe/stripe-jsnot installed. Paid plan selection throws “Failed to load Stripe”. Install the peer.- Plans don’t render. Check the Bridge admin UI — plans must be configured on the app, with at least one price offer each.
- Free plan flow: even free plans need a
Plandefinition in the admin UI.<PlanSelector>won’t render anything ifgetPlans()returns an empty array. - Styles look unstyled. Import the plugin’s structural CSS once at your app entry:
import '@nebulr-group/bridge-react/styles';.
Billing 2.0 — live, reactive billing UI
Section titled “Billing 2.0 — live, reactive billing UI”Bridge gives every workspace one canonical subscription — a plan, a status, and an optional trial — kept live in your app over the Bridge live channel. When a payment fails, a trial nears its end, or an admin changes the plan in Stripe, your UI reflects it within seconds, without polling.
These components are backed by auth-core’s reactive billing surface and are wired automatically by <BridgeProvider> — drop them in, no extra setup. They coexist with the classic <PlanSelector> above.
Drop-in components
Section titled “Drop-in components”<BridgeSubscriptionStatus />
Section titled “<BridgeSubscriptionStatus />”Renders the current plan name + a status badge. Mounts and subscribes itself — no props required.
import { BridgeSubscriptionStatus } from '@nebulr-group/bridge-react';
export function Header() { return <BridgeSubscriptionStatus />;}| Prop | Type | Default | Description |
|---|---|---|---|
className | string | '' | Class applied to the root span |
<BridgeBillingNotice />
Section titled “<BridgeBillingNotice />”The unified billing banner. Renders nothing while the subscription is healthy, and the right notice when it needs attention — trial countdown, payment failed, dunning retries, cancellation, locked. Not dismissible; it disappears when the status flips back to healthy.
import { BridgeBillingNotice } from '@nebulr-group/bridge-react';
// Put it once near your root layout (inside <BridgeProvider>).<BridgeBillingNotice />| Prop | Type | Default | Description |
|---|---|---|---|
chassis | 'bar' | 'rail' | 'card' | 'rail' | Visual variant |
mode | 'soft' | 'hard' | 'soft' | soft always renders inline; hard renders a full-screen lockscreen when the workspace is billing-locked |
className | string | '' | Class applied to the root element |
onActionClick | (state) => void | — | Override the default CTA click handler |
States it covers: trial active, trial ending soon, past due, cancellation scheduled, canceled, dunning retry scheduled, final retry, exhausted (locked). Each state has two role variants: admins get an action CTA (“Update card”, “Upgrade”); members get an informational variant pointing them to their workspace owner.
<BridgeQuotaBanner />
Section titled “<BridgeQuotaBanner />”A live usage-cap banner for one metric. Renders nothing while usage is below 80% of the plan’s quota (or when the plan has no quota for that metric); shows a warning at 80–94%, critical at 95%+, and over-cap copy when the limit is exceeded. Updates live on quota.updated pushes.
import { BridgeQuotaBanner } from '@nebulr-group/bridge-react';
<BridgeQuotaBanner metric="ai_completions" />| Prop | Type | Default | Description |
|---|---|---|---|
metric | string | required | Metric key to watch |
label | string | metric key | Humanized display label |
className | string | '' | Class applied to the root element |
onActionClick | (snap) => void | — | Override the default Upgrade CTA handler |
For a fully custom quota UI, read the underlying snapshot directly via the auth-core billing surface (re-exported as useBridgeBilling to avoid colliding with the unified useBridge):
import { useBridgeBilling } from '@nebulr-group/bridge-react';
const q = useBridgeBilling().quota('ai_completions');// q?.used, q?.limit, q?.remaining, q?.warningLevel ('approaching' | 'critical' | null)<BridgePaywall />
Section titled “<BridgePaywall />”A hard gate for workspaces that haven’t picked a plan yet. While shouldSelectPlan is true it renders a full-screen modal with a <PlanSelector> inside; otherwise it renders its children.
import { BridgePaywall } from '@nebulr-group/bridge-react';
<BridgePaywall successRedirect="/" cancelRedirect="/"> {/* your app — only rendered once a plan is active */} {children}</BridgePaywall>| Prop | Type | Default | Description |
|---|---|---|---|
successRedirect | string | '/' | Where to send the user after a successful Stripe payment |
cancelRedirect | string | '/' | Where to send the user if they cancel checkout |
onSelect | ({ plan, price }) => void | — | Called after free-plan selection or a direct plan change |
heading | ReactNode | ”Choose a plan” | Override the modal heading |
Workspaces with paymentsAutoRedirect: false are exempt from the gate. successRedirect / cancelRedirect are resolved against window.location.origin and handed to the inner <PlanSelector> as successUrl / cancelUrl.
Entitlements
Section titled “Entitlements”Plans grant entitlements — named capabilities like ai_completions or sso. They arrive with the session snapshot and update live on every entitlements.changed push, so an upgrade unlocks features without a reload.
import { useBridgeBilling } from '@nebulr-group/bridge-react';
// Imperative check (synchronous, fail-closed — false until the snapshot lands)if (useBridgeBilling().entitlements.can('ai_completions')) { /* ... */ }Entitlements are billing-derived (what the plan grants the workspace). They are not roles — use Bridge’s role/privilege system for who-may-do-what inside a workspace. The recommended gating pattern is a feature flag targeting
bridge:billing.entitlement.<key>, not a raw conditional — see the Feature Flags guide.
Billing events
Section titled “Billing events”For side effects — analytics, audit logs, Slack alerts — register handlers on the billing surface. This is separate from UI rendering, which the components above own:
import { useBridgeBilling } from '@nebulr-group/bridge-react';
const unsubscribe = useBridgeBilling().handle({ 'subscription.plan_changed': (m) => analytics.track('plan_changed', m), 'payment.failed': (m) => alertOps('payment failed'), 'quota.updated': (m) => updateMeter(m.metric, m.remaining), 'entitlements.changed': (m) => analytics.track('entitlements', m),});Multiple handlers can register for the same kind; one throwing handler never blocks the others.
Environment variables
Section titled “Environment variables”The components need no env vars of their own — they read live state through <BridgeProvider>, which is configured by VITE_BRIDGE_APP_ID (the same var the rest of the SDK uses).