> ## Documentation Index
> Fetch the complete documentation index at: https://docs.monigo.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Embedding the Portal

> Drop Monigo's customer portal into your own React, Vue, Svelte, or Flutter app — no iframes, no auth flow to write.

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>`.

| Target        | Package          |
| ------------- | ---------------- |
| 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

```mermaid theme={null}
flowchart TD
    A["Your app<br>React · Vue · Svelte · Flutter"]
    B["@monigo/react · vue · svelte · monigo_portal (Dart)<br>Styled components · page components · MonigoPortal"]
    C["@monigo/portal-core (framework-agnostic TS)<br>Portal API client · state stores · formatters · i18n"]
    D["Monigo backend<br>/api/v1/portal/* · X-Portal-Token"]

    A --> B --> C --> D

    style A fill:#F3F4F6,color:#1F2937,stroke:#D1D5DB
    style B fill:#F3F4F6,color:#1F2937,stroke:#D1D5DB
    style C fill:#F3F4F6,color:#1F2937,stroke:#D1D5DB
    style D fill:#1F2937,color:#fff,stroke:#374151
```

Your backend generates a portal token (see [Customer Portal](/guides/customer-portal)). Your frontend drops in `<MonigoPortal />` with that token. Done.

<Warning>
  Generate portal tokens **server-side**. Never expose your Monigo API key to the browser. See [Security considerations](/guides/customer-portal#security-considerations).
</Warning>

***

## Install

<CodeGroup>
  ```bash React theme={null}
  npm install @monigo/react @monigo/portal-core @monigo/tokens react react-dom
  ```

  ```bash Vue theme={null}
  npm install @monigo/vue @monigo/portal-core @monigo/tokens vue
  ```

  ```bash Svelte theme={null}
  npm install @monigo/svelte @monigo/portal-core @monigo/tokens svelte
  ```

  ```bash Flutter theme={null}
  flutter pub add monigo_portal
  ```
</CodeGroup>

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

```ts Node.js / Server theme={null}
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

<CodeGroup>
  ```tsx React theme={null}
  '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>
    )
  }
  ```

  ```vue Vue theme={null}
  <script setup lang="ts">
  import { MonigoProvider, MonigoPortal } from '@monigo/vue'
  import '@monigo/tokens/monigo.css'

  defineProps<{ portalToken: string }>()
  </script>

  <template>
    <MonigoProvider
      :portal-token="portalToken"
      :theme="{ primary: '#6366f1', accent: '#f43f5e', mode: 'auto' }"
    >
      <MonigoPortal base-path="/billing" />
    </MonigoProvider>
  </template>
  ```

  ```svelte Svelte theme={null}
  <script lang="ts">
    import { MonigoProvider, MonigoPortal } from '@monigo/svelte'
    import '@monigo/tokens/monigo.css'

    let { portalToken }: { portalToken: string } = $props()
  </script>

  <MonigoProvider {portalToken} theme={{ primary: '#6366f1', accent: '#f43f5e', mode: 'auto' }}>
    <MonigoPortal basePath="/billing" />
  </MonigoProvider>
  ```

  ```dart Flutter theme={null}
  import 'package:flutter/material.dart';
  import 'package:monigo_portal/monigo_portal.dart';

  class BillingPage extends StatelessWidget {
    final String portalToken;
    const BillingPage({super.key, required this.portalToken});

    @override
    Widget build(BuildContext context) {
      return MonigoThemeProvider(
        theme: MonigoTheme.light(
          primary: const Color(0xFF6366F1),
          accent: const Color(0xFFF43F5E),
        ),
        child: MonigoScope(
          portalToken: portalToken,
          child: const MonigoPortal(),
        ),
      );
    }
  }
  ```
</CodeGroup>

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.

<CodeGroup>
  ```tsx React theme={null}
  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>
    )
  }
  ```

  ```vue Vue theme={null}
  <script setup lang="ts">
  import {
    MonigoProvider,
    DashboardSummary,
    InvoiceList,
    SubscriptionList,
    WalletList,
  } from '@monigo/vue'
  import '@monigo/tokens/monigo.css'

  defineProps<{ portalToken: string }>()
  </script>

  <template>
    <MonigoProvider :portal-token="portalToken">
      <section class="my-layout">
        <DashboardSummary />
        <div class="grid">
          <InvoiceList :limit="10" @invoice-click="(i) => router.push(`/invoices/${i.id}`)" />
          <SubscriptionList />
        </div>
        <WalletList />
      </section>
    </MonigoProvider>
  </template>
  ```

  ```svelte Svelte theme={null}
  <script lang="ts">
    import {
      MonigoProvider,
      DashboardSummary,
      InvoiceList,
      SubscriptionList,
      WalletList,
    } from '@monigo/svelte'
    import { goto } from '$app/navigation'
    import '@monigo/tokens/monigo.css'

    let { portalToken }: { portalToken: string } = $props()
  </script>

  <MonigoProvider {portalToken}>
    <section class="my-layout">
      <DashboardSummary />
      <div class="grid">
        <InvoiceList limit={10} onInvoiceClick={(i) => goto(`/invoices/${i.id}`)} />
        <SubscriptionList />
      </div>
      <WalletList />
    </section>
  </MonigoProvider>
  ```

  ```dart Flutter theme={null}
  MonigoScope(
    portalToken: portalToken,
    child: Column(
      children: [
        const DashboardSummary(),
        const InvoiceList(limit: 10),
        const SubscriptionList(),
        const WalletList(),
      ],
    ),
  )
  ```
</CodeGroup>

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`).

| Feature           | Components                                                                                                                       |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Dashboard         | `DashboardSummary`, `RecentActivity`                                                                                             |
| Invoices          | `InvoiceList`, `InvoiceDetail`, `InvoiceStatusBadge`, `PayInvoiceButton`                                                         |
| Payouts           | `BillList`, `BillDetail`, `BillStatusBadge`                                                                                      |
| Subscriptions     | `SubscriptionList`, `SubscriptionCard`, `CancelSubscriptionButton`                                                               |
| Wallets           | `WalletList`, `WalletCard`, `WalletDetail`, `WalletTransactionList`, `FundWalletButton`                                          |
| Payment methods   | `PaymentMethodList`, `PaymentMethodCard`, `AddPaymentMethodButton`, `RemovePaymentMethodButton`, `SetDefaultPaymentMethodButton` |
| Payout accounts   | `PayoutAccountList`, `PayoutAccountCard`                                                                                         |
| Shared primitives | `Skeleton`, `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

<CodeGroup>
  ```tsx React theme={null}
  <MonigoProvider
    portalToken={token}
    theme={{ primary: '#6366f1', accent: '#f43f5e', mode: 'dark', radius: 'lg' }}
  >
    ...
  </MonigoProvider>
  ```

  ```vue Vue theme={null}
  <MonigoProvider
    :portal-token="token"
    :theme="{ primary: '#6366f1', accent: '#f43f5e', mode: 'dark', radius: 'lg' }"
  >
    ...
  </MonigoProvider>
  ```

  ```svelte Svelte theme={null}
  <MonigoProvider {portalToken} theme={{ primary: '#6366f1', accent: '#f43f5e', mode: 'dark', radius: 'lg' }}>
    ...
  </MonigoProvider>
  ```

  ```dart Flutter theme={null}
  MonigoThemeProvider(
    theme: MonigoTheme.dark(
      primary: const Color(0xFF6366F1),
      accent: const Color(0xFFF43F5E),
    ),
    child: MonigoScope(portalToken: token, child: const MonigoPortal()),
  )
  ```
</CodeGroup>

`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)

```ts theme={null}
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:

```css theme={null}
.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:

```ts theme={null}
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:

```ts theme={null}
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.

<CodeGroup>
  ```tsx Next.js (App Router) theme={null}
  // 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} />
  }
  ```

  ```ts Nuxt (Vue) theme={null}
  <!-- pages/billing.vue -->
  <script setup lang="ts">
  import { MonigoProvider } from '@monigo/vue'
  import '@monigo/tokens/monigo.css'

  const { portalToken } = useRuntimeConfig().public
  </script>

  <template>
    <MonigoProvider :portal-token="portalToken">
      <NuxtPage />
    </MonigoProvider>
  </template>

  <!-- pages/billing/invoices/[id].vue -->
  <script setup lang="ts">
  import { PortalInvoiceDetailPage } from '@monigo/vue'
  const route = useRoute()
  </script>
  <template>
    <PortalInvoiceDetailPage :invoice-id="route.params.id as string" />
  </template>
  ```

  ```svelte SvelteKit theme={null}
  <!-- src/routes/billing/+layout.svelte -->
  <script lang="ts">
    import { MonigoProvider } from '@monigo/svelte'
    import '@monigo/tokens/monigo.css'
    let { data, children } = $props<{ data: { portalToken: string }; children: any }>()
  </script>

  <MonigoProvider portalToken={data.portalToken}>
    {@render children()}
  </MonigoProvider>

  <!-- src/routes/billing/invoices/[id]/+page.svelte -->
  <script lang="ts">
    import { page } from '$app/state'
    import { PortalInvoiceDetailPage } from '@monigo/svelte'
  </script>

  <PortalInvoiceDetailPage invoiceId={page.params.id as string} />
  ```

  ```dart Flutter + GoRouter theme={null}
  final router = GoRouter(
    routes: [
      GoRoute(
        path: '/billing',
        builder: (context, state) => MonigoScope(
          portalToken: state.extra as String,
          child: const PortalDashboardPage(),
        ),
      ),
      GoRoute(
        path: '/billing/invoices/:id',
        builder: (context, state) => MonigoScope(
          portalToken: state.extra as String,
          child: PortalInvoiceDetailPage(invoiceId: state.pathParameters['id']!),
        ),
      ),
    ],
  );
  ```
</CodeGroup>

### Exported page components

| Component                  | Path               | Notes                        |
| -------------------------- | ------------------ | ---------------------------- |
| `PortalDashboardPage`      | `/`                | Summary + recent activity    |
| `PortalInvoicesPage`       | `/invoices`        | Clickable list               |
| `PortalInvoiceDetailPage`  | `/invoices/:id`    | Requires `invoiceId` prop    |
| `PortalBillsPage`          | `/bills`           | Payout slips list            |
| `PortalBillDetailPage`     | `/bills/:id`       | Requires `billId` prop       |
| `PortalSubscriptionsPage`  | `/subscriptions`   | Active and trial plans       |
| `PortalWalletsPage`        | `/wallets`         |                              |
| `PortalWalletDetailPage`   | `/wallets/:id`     | Includes 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

```tsx React theme={null}
<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 Svelte theme={null}
<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 Vue theme={null}
<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

```dart Flutter theme={null}
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.

<CodeGroup>
  ```tsx React theme={null}
  <InvoiceList
    limit={10}
    components={{
      Loading: () => <MyBrandedSpinner />,
      Empty: () => <MyEmptyIllustration label="No invoices yet" />,
      Error: ({ error, onRetry }) => <MyErrorScreen message={error.message} onRetry={onRetry} />,
    }}
  />
  ```
</CodeGroup>

For a global handler (telemetry, Sentry, unauthorized-token redirect), use the provider:

```tsx theme={null}
<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.

| Framework       | Tested 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 3**      | SSR just works. Don't call `useMonigoContext()` inside `<script setup>` at the module level — use it inside a `computed` or `onMounted`.                                            |
| **SvelteKit**   | SSR + hydration both work. `<MonigoProvider>` renders its theme `<style>` tag server-side so there's no flash of unstyled content.                                                  |
| **Remix**       | Works. Render `<MonigoProvider>` in a route segment; it hydrates on the client.                                                                                                     |

<Info>
  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.
</Info>

***

## i18n

v1 ships English. All user-visible strings live in a typed catalog you can override:

```tsx React theme={null}
<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`:

```tsx theme={null}
<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

| Package               | Registry | Current version |
| --------------------- | -------- | --------------- |
| `@monigo/react`       | npm      | `0.3.x`         |
| `@monigo/vue`         | npm      | `0.3.x`         |
| `@monigo/svelte`      | npm      | `0.3.x`         |
| `@monigo/portal-core` | npm      | `0.3.x`         |
| `@monigo/tokens`      | npm      | `0.3.x`         |
| `monigo_portal`       | pub.dev  | `0.1.x`         |

The web packages use synchronised versioning — they bump together via Changesets. Flutter ships its own cadence.

***

## Related

* [Customer Portal](/guides/customer-portal) — how portal tokens work and the hosted portal at `monigo.co/portal/<token>`
* [Authentication](/getting-started/authentication) — where portal tokens fit in the auth model
* [Invoice Lifecycle](/guides/invoice-lifecycle) — the state machine behind `InvoiceStatusBadge`
* [Subscription Lifecycle](/guides/subscription-lifecycle) — what `CancelSubscriptionButton` triggers
