Skip to main content
Monigo ships first-class UI packages for React, Vue, Svelte, and Flutter. Each one embeds the full customer portal — invoices, subscriptions, wallets, payout accounts, payment methods — directly in your app, styled to match your brand, with a single portal-token prop. Use these packages when you want the customer experience to live inside your product rather than redirecting to the hosted portal at monigo.co/portal/<token>.
TargetPackage
React 18+@monigo/react
Vue 3.4+@monigo/vue
Svelte 5@monigo/svelte
Flutter 3.24+monigo_portal
All four deliver the same building blocks (invoice list, wallet card, subscription card, etc.) and the same opinionated composed portal (<MonigoPortal />). They share a common core: @monigo/portal-core (typed API client + state machines) and @monigo/tokens (CSS variables and theming helpers).

How it fits together

┌─────────────────────────────────────────────────────────┐
│  Your app (React / Vue / Svelte / Flutter)              │
└────────────────────────┬────────────────────────────────┘

┌────────────────────────▼────────────────────────────────┐
│  @monigo/react | vue | svelte   •  monigo_portal (Dart) │
│  Styled components, page components, MonigoPortal       │
└────────────────────────┬────────────────────────────────┘

┌────────────────────────▼────────────────────────────────┐
│  @monigo/portal-core (framework-agnostic TS)            │
│  Portal API client • State stores • Formatters • i18n   │
└────────────────────────┬────────────────────────────────┘

┌────────────────────────▼────────────────────────────────┐
│  Monigo backend   /api/v1/portal/*  (X-Portal-Token)    │
└─────────────────────────────────────────────────────────┘
Your backend generates a portal token (see Customer Portal). Your frontend drops in <MonigoPortal /> with that token. Done.
Generate portal tokens server-side. Never expose your Monigo API key to the browser. See Security considerations.

Install

npm install @monigo/react @monigo/portal-core @monigo/tokens react react-dom
The TS packages have zero runtime dependencies beyond their peer framework. No CSS-in-JS, no Tailwind, no router lock-in. Monigo’s design tokens ship as plain CSS variables.

Quick start (the 5-minute integration)

The flow is always the same:
  1. Your backend mints a portal token and hands it to your frontend.
  2. Your frontend renders <MonigoProvider> with the token and drops in <MonigoPortal />.

1. Mint the token on your server

Node.js / Server
import { MonigoClient } from '@monigo/sdk'
const monigo = new MonigoClient({ apiKey: process.env.MONIGO_API_KEY! })

// In a route authenticated to the current user:
const { token } = await monigo.portalTokens.create({
  customer_external_id: session.userId,
  label: 'In-app portal',
})
// Return `token` to your frontend (e.g. as JSON)

2. Render the portal on the client

'use client'
import { MonigoProvider, MonigoPortal } from '@monigo/react'
import '@monigo/tokens/monigo.css'

export default function BillingPage({ portalToken }: { portalToken: string }) {
  return (
    <MonigoProvider
      portalToken={portalToken}
      theme={{ primary: '#6366f1', accent: '#f43f5e', mode: 'auto' }}
    >
      <MonigoPortal basePath="/billing" />
    </MonigoProvider>
  )
}
That’s the entire integration. <MonigoPortal /> renders its own navigation, routing, and all feature pages — dashboard, invoices, bills, subscriptions, wallets, payment methods, payout accounts.

Building-block composition

If you want the data and behaviour but not the opinionated layout, use the building-block components directly. They share the same <MonigoProvider> context and run independently.
import {
  MonigoProvider,
  DashboardSummary,
  InvoiceList,
  SubscriptionList,
  WalletList,
} from '@monigo/react'
import '@monigo/tokens/monigo.css'

export function MyCustomBilling({ portalToken }: { portalToken: string }) {
  return (
    <MonigoProvider portalToken={portalToken}>
      <section className="my-layout">
        <DashboardSummary />
        <div className="grid">
          <InvoiceList limit={10} onInvoiceClick={(i) => router.push(`/invoices/${i.id}`)} />
          <SubscriptionList />
        </div>
        <WalletList />
      </section>
    </MonigoProvider>
  )
}
Every building block handles its own loading, empty, and error states. Each fetches independently and refetches on window focus after 30 seconds.

Exported building blocks

The same component names exist across React, Vue, and Svelte. Flutter widgets follow Dart’s snake_case → Pascal convention (InvoiceList, invoice_list.dart).
FeatureComponents
DashboardDashboardSummary, RecentActivity
InvoicesInvoiceList, InvoiceDetail, InvoiceStatusBadge, PayInvoiceButton
PayoutsBillList, BillDetail, BillStatusBadge
SubscriptionsSubscriptionList, SubscriptionCard, CancelSubscriptionButton
WalletsWalletList, WalletCard, WalletDetail, WalletTransactionList, FundWalletButton
Payment methodsPaymentMethodList, PaymentMethodCard, AddPaymentMethodButton, RemovePaymentMethodButton, SetDefaultPaymentMethodButton
Payout accountsPayoutAccountList, PayoutAccountCard
Shared primitivesSkeleton, EmptyState, ErrorState

Theming

Monigo’s components render against a set of CSS custom properties. The defaults are sensible; the ones you usually want to change are primary, accent, and mode.

Quick theming via props

<MonigoProvider
  portalToken={token}
  theme={{ primary: '#6366f1', accent: '#f43f5e', mode: 'dark', radius: 'lg' }}
>
  ...
</MonigoProvider>
mode accepts 'light', 'dark', or 'auto'. With auto, the portal follows the user’s system preference via prefers-color-scheme.

Full theme API (web packages)

interface MonigoTheme {
  primary: string   // hex color for primary actions (buttons, links, focus ring)
  accent: string    // hex color for accent surfaces (highlights, badges)
  mode: 'light' | 'dark' | 'auto'
  radius?: 'sm' | 'md' | 'lg'   // corner radius preset; default 'md'
  font?: string     // any valid CSS font-family; default falls back to system sans
}

Theming via CSS (any host app)

Every token is a plain CSS variable. Override any of them on any ancestor element:
.billing-section {
  --monigo-color-primary: #6366f1;
  --monigo-color-accent: #f43f5e;
  --monigo-radius-md: 12px;
  --monigo-font-sans: 'Inter', sans-serif;
}

/* Dark mode — Monigo toggles this automatically, but you can force it: */
.billing-section[data-monigo-theme='dark'] {
  --monigo-color-bg: #0b1220;
  --monigo-color-fg: #f1f5f9;
}
Want the full list? It’s in @monigo/tokens/monigo.css — or use the typed TOKEN_NAMES array:
import { TOKEN_NAMES, cssVar } from '@monigo/tokens'

TOKEN_NAMES // ['color-primary', 'color-accent', 'color-bg', ... ]
cssVar('color-primary') // 'var(--monigo-color-primary)'

Runtime theme generation (per-tenant)

If you render the portal for multiple tenants with different brand colors, inject a scoped theme at runtime:
import { createTheme } from '@monigo/tokens'

const css = createTheme({
  primary: tenant.brandColor,
  accent: tenant.accentColor,
  mode: 'auto',
  selector: `[data-tenant="${tenant.id}"]`,
})

// Inject anywhere it's easy for you — a <style> tag, a CSS module, a stylesheet file:
document.head.appendChild(Object.assign(document.createElement('style'), { textContent: css }))

Using your own router

The <MonigoPortal /> component bundles a lightweight internal router so the 5-minute demo stays a one-liner. For production apps that already have a router (Next.js App Router, Nuxt, SvelteKit, Remix, TanStack Router), import the page components and wire them into your own routes.
// app/billing/layout.tsx
import { MonigoProvider } from '@monigo/react'
import '@monigo/tokens/monigo.css'

export default function BillingLayout({
  children,
  portalToken,
}: {
  children: React.ReactNode
  portalToken: string
}) {
  return <MonigoProvider portalToken={portalToken}>{children}</MonigoProvider>
}

// app/billing/invoices/page.tsx
'use client'
import { PortalInvoicesPage } from '@monigo/react'
import { useRouter } from 'next/navigation'

export default function Page() {
  const router = useRouter()
  return <PortalInvoicesPage onInvoiceClick={(i) => router.push(`/billing/invoices/${i.id}`)} />
}

// app/billing/invoices/[id]/page.tsx
'use client'
import { PortalInvoiceDetailPage } from '@monigo/react'

export default function Page({ params }: { params: { id: string } }) {
  return <PortalInvoiceDetailPage invoiceId={params.id} />
}

Exported page components

ComponentPathNotes
PortalDashboardPage/Summary + recent activity
PortalInvoicesPage/invoicesClickable list
PortalInvoiceDetailPage/invoices/:idRequires invoiceId prop
PortalBillsPage/billsPayout slips list
PortalBillDetailPage/bills/:idRequires billId prop
PortalSubscriptionsPage/subscriptionsActive and trial plans
PortalWalletsPage/wallets
PortalWalletDetailPage/wallets/:idIncludes transaction history
PortalPaymentMethodsPage/payment-methods
PortalPayoutAccountsPage/payout-accounts

Customer actions

Every action component fires an optional onError callback for telemetry and emits a success callback you can use to refresh state or navigate.

Pay an invoice

React
<PayInvoiceButton
  invoiceId={invoice.id}
  onSuccess={(result) => {
    /* result.authorization_url — Monigo has already redirected the browser */
  }}
  onError={(err) => analytics.track('pay_invoice_failed', { err })}
/>
Clicking PayInvoiceButton calls POST /portal/invoices/:id/pay, which returns a gateway authorization_url. The component redirects the browser to that URL automatically.

Cancel a subscription

Svelte
<CancelSubscriptionButton
  subscription={sub}
  oncancel={(sub) => subscriptionsRune.dispatch({ type: 'refresh' })}
/>
Cancellation requires user confirmation via window.confirm. The backend call happens only after confirmation.

Add a payment method

Vue
<AddPaymentMethodButton
  @add="() => analytics.track('pm_setup_started')"
  @unsupported="() => alert('Card setup is not available for your region yet.')"
/>
The add flow redirects to the payment gateway’s tokenization page (Paystack today). If your Monigo organisation uses a gateway that doesn’t yet support stored-card setup, the component emits unsupported and shows a localized message.

Wallet top-ups

Flutter
FundWalletButton(walletId: wallet.id, currency: 'NGN')
Opens an inline amount input, calls POST /portal/wallets/:id/fund, and redirects to the gateway.

Error handling

Every component renders three states out of the box: loading (skeleton), empty (“you have no invoices yet”), and error (with a Try again button). You can override any of them.
<InvoiceList
  limit={10}
  components={{
    Loading: () => <MyBrandedSpinner />,
    Empty: () => <MyEmptyIllustration label="No invoices yet" />,
    Error: ({ error, onRetry }) => <MyErrorScreen message={error.message} onRetry={onRetry} />,
  }}
/>
For a global handler (telemetry, Sentry, unauthorized-token redirect), use the provider:
<MonigoProvider
  portalToken={token}
  onUnauthorized={() => window.location.href = '/billing/expired'}
  onError={(err) => Sentry.captureException(err)}
>
  <MonigoPortal />
</MonigoProvider>

SSR & server components

All web packages are SSR-safe. The components don’t touch window, document, or localStorage at module scope — only inside framework lifecycle hooks.
FrameworkTested setup
Next.js 14+App Router. Mark pages that use <MonigoProvider> as client components ('use client'). The provider and every interactive component run client-side; static content is SSR-safe.
Nuxt 3SSR just works. Don’t call useMonigoContext() inside <script setup> at the module level — use it inside a computed or onMounted.
SvelteKitSSR + hydration both work. <MonigoProvider> renders its theme <style> tag server-side so there’s no flash of unstyled content.
RemixWorks. Render <MonigoProvider> in a route segment; it hydrates on the client.
The portal fetches data on the client regardless — portal tokens are customer-scoped and we don’t want them pre-fetched on a shared cache. The initial HTML shows skeletons; the data streams in after hydration.

i18n

v1 ships English. All user-visible strings live in a typed catalog you can override:
React
<MonigoProvider
  portalToken={token}
  locale="en-US"
  messages={{
    'invoices.title': 'Bills',
    'invoices.action.pay': 'Pay this bill',
    'subscriptions.cancel.confirm': 'Are you sure? This ends access at the end of the period.',
  }}
>
  <MonigoPortal />
</MonigoProvider>
Every key comes with a sensible default. The full list is exported as the MessageKey union from @monigo/portal-core.

Proxying the API (advanced)

By default, every component calls https://api.monigo.co/api/v1/portal/* directly from the browser with the X-Portal-Token header. If your security policy requires all traffic to flow through your own backend, pass a custom baseUrl and fetch:
<MonigoProvider
  portalToken={token}
  baseUrl="/api/monigo"   // your proxy
  fetch={(url, init) => fetch(url, { ...init, credentials: 'include' })}
>
  <MonigoPortal />
</MonigoProvider>
Then set up a lightweight reverse proxy on your server that forwards /api/monigo/* to https://api.monigo.co/api/v1/* and injects the X-Portal-Token header (or validates one from a cookie you control).

Bundle size

Tree-shaking works end-to-end. A minimal integration (provider + InvoiceList only) ships well under 20 kB min+gzip in React, Vue, and Svelte. The full <MonigoPortal /> is under 60 kB min+gzip per framework. Flutter’s monigo_portal adds roughly 450 kB to release APKs (mostly from generated models). It uses dart:http — no extra native plugins — so it compiles unchanged for iOS, Android, web, macOS, Linux, and Windows.

Publishing targets

PackageRegistryCurrent version
@monigo/reactnpm0.3.x
@monigo/vuenpm0.3.x
@monigo/sveltenpm0.3.x
@monigo/portal-corenpm0.3.x
@monigo/tokensnpm0.3.x
monigo_portalpub.dev0.1.x
The web packages use synchronised versioning — they bump together via Changesets. Flutter ships its own cadence.