@yuno-payments/dashboard-embed-sdk
v1.11.0
Published
Lightweight SDK for embedding the Yuno Dashboard via iframe
Maintainers
Keywords
Readme
@yuno-payments/dashboard-embed-sdk
Lightweight SDK for embedding the Yuno Dashboard via iframe. Zero dependencies — uses only DOM APIs.
Installation
npm install @yuno-payments/dashboard-embed-sdkQuick Start
import { initDashboard } from "@yuno-payments/dashboard-embed-sdk";
const sdk = initDashboard({
baseUrl: "https://dashboard.y.uno",
container: document.getElementById("dashboard")!,
token: "your-jwt-token",
theme: {
// Simple: flat tokens apply to both light mode and dark mode;
tokens: { primary: "#134AC3", surface: "#FFFFFF" },
// Per-mode: full control over both light and dark colors
// tokens: {
// light: { primary: "#134AC3", surface: "#FFFFFF" },
// dark: { primary: "#5B7BFF", surface: "#0A0A0A" },
// },
typography: {
fontFamily: "'Inter', sans-serif",
fontUrl: "https://fonts.googleapis.com/css2?family=Inter",
},
mode: "light",
styles: ".yuno-card { border-radius: 8px; }",
},
lang: "en",
path: "/connections",
onReady: () => console.log("Dashboard is ready"),
});API
Singleton helpers
The recommended way to manage the SDK instance:
import {
initDashboard,
getDashboard,
destroyDashboard,
} from "@yuno-payments/dashboard-embed-sdk";
// Create the single dashboard instance
const sdk = initDashboard(config);
// Retrieve the current instance from anywhere
const sdk = getDashboard();
// Destroy the instance and clean up
destroyDashboard();The SDK runs a single instance. Call
initDashboard()once, then drive that instance —navigate()to change pages,setTheme()/setLang()/setToken()for runtime updates. CallinginitDashboard()again while an instance already exists throws: re-initializing reloads the iframe and, withsyncUrlon, the newpathis overridden by the previously synced route. To genuinely tear down and start over, calldestroyDashboard()first.// ❌ Don't re-init to change pages — this throws on the 2nd call: initDashboard({ ...config, path: "/connections" }); initDashboard({ ...config, path: "/checkout-builder" }); // ✅ Init once, then navigate: const sdk = initDashboard({ ...config, path: "/connections" }); sdk.navigate("/checkout-builder"); // ✅ Or explicitly tear down first if you really need a fresh instance: destroyDashboard(); initDashboard({ ...config, path: "/checkout-builder" });
initDashboard(config)
| Option | Type | Required | Description |
|---|---|---|---|
| baseUrl | string | Yes | Dashboard base URL |
| container | HTMLElement | Yes | Element to mount the iframe |
| token | string | No | JWT auth token — sent via PostMessage after the iframe is ready |
| theme | DashboardTheme | No | Initial theme configuration |
| lang | string | No | Language code (default: "en") |
| path | string | No | Initial navigation path (default: "/"). Any query string you pass is stripped — only the SDK sets the iframe query params (embed/theme/lang/test) |
| testMode | boolean | No | When set, the dashboard mounts in test/sandbox mode (true) or live mode (false). Omit to inherit the dashboard's own default. See Test mode |
| onReady | () => void | No | Callback invoked when the dashboard is fully loaded and authenticated |
| onSessionExpired | () => void | No | Callback invoked when the embedded session has expired. The host should re-authenticate and call setToken(newToken) to resume. |
| onNavigationChange | (event: NavigationChangedEvent) => void | No | Fires on every in-app navigation inside the dashboard, delivering the new route via event.path. A cross-cutting notification (like onReady), not a domain event. Optional — with syncUrl on (default) the SDK already keeps the host URL in sync. See Navigation. |
| events | DashboardEvents | No | Notifications the dashboard sends the host, grouped by domain and action — e.g. events.connections.created, events.checkout.published. Pure callbacks: providing a handler does not change what the dashboard does. To make the dashboard step aside (e.g. skip its connection success screen), use ui. See Embed events. |
| loading | HTMLElement | No | Custom loading overlay element. If omitted, a default spinner is shown |
| autoHeight | boolean | No | When true, the iframe is resized to match the dashboard content height (no inner scroll). Default false (iframe fills its container at 100%). See Auto height |
| syncUrl | boolean | No | Keep the host's browser URL in sync with the dashboard's in-app route automatically, via the URL hash (e.g. #/payments/abc) — no host code required. Default true; set false to opt out (e.g. if the host owns routing). See Navigation |
| ui | EmbedUi | No | Declarative UI config announced to the dashboard on init — hide/show dashboard UI elements for this embed (e.g. the checkout publish button). See Commands & UI config |
Methods
setTheme(theme)— Update colors, typography, mode, or external stylessetLang(lang)— Change display languagesetToken(token)— Update the auth token via PostMessagenavigate(path)— Navigate to a dashboard routedispatch(command)— Send an imperative command into the dashboard (e.g. publish the open checkout). See Commands & UI configsetTestMode(enabled)— Toggle test/sandbox mode at runtime via PostMessage. See Test modedestroy()— Remove iframe and clean up event listeners
Types
interface ThemeColors {
primary: string;
primaryForeground: string;
secondary: string;
secondaryForeground: string;
background: string;
foreground: string;
muted: string;
mutedForeground: string;
accent: string;
accentForeground: string;
destructive: string;
destructiveForeground: string;
border: string;
input: string;
ring: string;
surface: string;
card: string;
cardForeground: string;
popover: string;
popoverForeground: string;
success: string;
warning: string;
info: string;
}
interface ThemeTypography {
fontFamily: string;
fontUrl: string;
}
interface ModeTokens {
light?: Partial<ThemeColors>;
dark?: Partial<ThemeColors>;
}
interface DashboardTheme {
tokens?: Partial<ThemeColors> | ModeTokens; // flat OR per-mode
typography?: Partial<ThemeTypography>;
mode?: "light" | "dark";
styles?: string; // Custom CSS injected into the dashboard
}
type EmbedEventStatus = "loading" | "success" | "error";
interface ConnectionCreatedPayload {
connectionCode: string;
providerId: string;
}
interface ConnectionCreatedEvent {
status: EmbedEventStatus;
payload?: ConnectionCreatedPayload;
}
interface CheckoutPublishedEvent {
status: EmbedEventStatus;
payload?: unknown; // The configuration object that was published
}
interface NavigationChangedEvent {
path: string; // The new in-app route, e.g. "/payments/abc?status=approved"
}
interface DashboardEvents {
connections?: {
created?: (event: ConnectionCreatedEvent) => void | Promise<void>;
};
checkout?: {
published?: (event: CheckoutPublishedEvent) => void | Promise<void>;
};
}
// Imperative command sent into the dashboard via dashboard.dispatch().
type EmbedCommand = {
domain: "checkout";
action: "publish";
payload?: unknown;
};
// Declarative UI config passed via the `ui` option.
interface EmbedUi {
checkout?: {
hidePublishButton?: boolean;
};
connections?: {
hideSuccessScreen?: boolean;
};
}
onNavigationChangeis a top-level lifecycle callback (likeonReady), not aDashboardEventsentry: navigation is a cross-cutting notification with no domain ownership, so it does not announce a host capability. See Navigation.
Session timeouts
By default, sessions issued by POST /v1/external/authenticate are valid for 24 hours.
You can issue a shorter session by passing timeout_seconds (between 60 and 86400):
curl -X POST https://api.y.uno/v1/external/authenticate \
-H "x-organization-code: <your-org-uuid>" \
-H "Content-Type: application/json" \
-d '{"user_id":"<user-uuid>", "timeout_seconds": 1800}'When the embedded session expires, the iframe paints a "Session expired" overlay
and emits a message to the host. Subscribe via onSessionExpired:
const sdk = initDashboard({
baseUrl: "https://dashboard.y.uno",
container: document.getElementById("dashboard")!,
token: initialToken,
onSessionExpired: async () => {
const newToken = await myBackend.requestEmbedToken({ timeout_seconds: 1800 })
sdk.setToken(newToken)
},
})Embed events
The embedded dashboard sends business events to the host through the events
config, grouped by MFE (domain) and action. Register a handler only for what
you care about:
const sdk = initDashboard({
baseUrl: "https://dashboard.y.uno",
container: document.getElementById("dashboard")!,
token: initialToken,
events: {
connections: {
created: (event) => {
// event.status is "loading" | "success" | "error"
// event.payload is { connectionCode, providerId } on success
if (event.status === "success") {
router.push(`/integrations/${event.payload!.connectionCode}/done`)
}
},
},
checkout: {
published: (event) => {
if (event.status === "success") {
console.log("Checkout published", event.payload)
}
},
},
},
})Events are notifications; ui makes the dashboard step aside
Events are pure callbacks — providing a handler tells the dashboard nothing and
does not change its behavior. Making the dashboard skip its own UI is a
separate, explicit decision via the ui option.
For connection creation the two compose: register
events.connections.created to be notified when a connection is created, and
set ui: { connections: { hideSuccessScreen: true } } to stop the dashboard
from showing its own success screen so you can route the user yourself. Each
works independently — notify without suppressing, or suppress without notifying.
Status lifecycle
Handlers fire once per state transition: first status: "loading" when the
action starts, then "success" or "error" once the dashboard's call resolves.
event.payload is only meaningful on "success".
Navigation (keeping the host URL in sync)
When the user navigates inside the iframe (e.g. Payment Detail → Routing),
the SDK keeps the host's browser URL in sync automatically — you write no
code. This is on by default (syncUrl: true) and works in both directions via
the URL hash:
- iframe → host: on every in-app route change the SDK writes the route to the
host URL hash, e.g.
https://your-app.com/dashboard#/payments/abc. The address bar stays shareable and bookmarkable, and each navigation is a history entry. - host → iframe (reload / shared link): on load the SDK reads the route from the hash and boots the iframe straight into that view, so a reload or a shared bookmark reopens where the user was.
- host → iframe (back / forward): the SDK listens for the host URL changing (browser back/forward) and steers the dashboard to match.
// That's it — bookmarking, sharing, and back/forward just work:
const sdk = initDashboard({
baseUrl: "https://dashboard.y.uno",
container: document.getElementById("dashboard")!,
token: initialToken,
});Set syncUrl: false to opt out — for example if the host owns routing and
prefers to manage the URL itself. In that case (or alongside the automatic sync)
you can register the optional top-level onNavigationChange callback to run
custom logic on each navigation:
const sdk = initDashboard({
baseUrl: "https://dashboard.y.uno",
container: document.getElementById("dashboard")!,
token: initialToken,
syncUrl: false, // SDK won't touch the host URL
onNavigationChange: (event) => {
// event.path is the dashboard's new in-app route
window.history.replaceState(null, "", `/embed${event.path}`);
},
});onNavigationChange is a pure notification — like onReady it is a top-level
lifecycle callback (not a domain event), it has no status lifecycle, and the
dashboard does not change its own behavior; it just reports the new route via
event.path.
Host/iframe contract: the embedded dashboard posts
{ type: "yuno-dashboard:embed-event", domain: "navigation", action: "changed", payload: { path } }
to the host on each internal navigation; the SDK reflects path into the host URL
hash (when syncUrl) and routes it to the optional callback. Messages are accepted
only from the configured baseUrl origin.
Commands & UI config
The previous sections cover the dashboard handing off to the host. These two channels go the other way — the host drives the dashboard:
- Commands (imperative): trigger an action inside the dashboard, e.g. publish the checkout the user is editing.
- UI config (declarative): hide/show dashboard UI elements for this embed, e.g. hide the dashboard's own publish button so the host can render its own.
Commands — dispatch(command)
Navigate to the relevant view first, then dispatch. A command for an MFE that is
not currently mounted is a no-op (nothing is queued for it), so make sure the
target view is open. The result of the action comes back through the matching
embed event — e.g. publishing reports via events.checkout.published.
const sdk = initDashboard({
baseUrl: "https://dashboard.y.uno",
container: document.getElementById("dashboard")!,
token,
path: "/checkout/editor/abc",
events: {
checkout: {
published: (event) => {
if (event.status === "success") console.log("Published!", event.payload);
},
},
},
});
// Host's own "Publish" button:
publishBtn.addEventListener("click", () => {
sdk.dispatch({ domain: "checkout", action: "publish" });
});UI config — ui
Announced once to the dashboard when it becomes ready. Use it to suppress dashboard UI that the host replaces with its own:
initDashboard({
baseUrl: "https://dashboard.y.uno",
container: document.getElementById("dashboard")!,
token,
ui: {
checkout: {
hidePublishButton: true, // host renders its own publish button
},
connections: {
hideSuccessScreen: true, // host owns the post-create moment
},
},
});Host/iframe contract: dispatch() posts
{ type: "yuno-dashboard:command", domain, action, payload } to the iframe; the ui
config is announced as { type: "yuno-dashboard:ui", ui } on ready. The dashboard
validates both come from the host (parent) origin.
Test mode
The dashboard can run in test/sandbox mode or live mode. Set it at mount time
via the testMode option, or toggle it at runtime via setTestMode(enabled):
const sdk = initDashboard({
baseUrl: "https://dashboard.y.uno",
container: document.getElementById("dashboard")!,
token,
path: "/connections", // clean route — no query string
testMode: false, // mount in live mode
});
// Later — flip to test mode without remounting
sdk.setTestMode(true);Do not pass test mode through path. The SDK builds the iframe URL by appending its
own query string (embed, theme, lang, and test when testMode is set) to the route:
${baseUrl}${path}?embed=true&…. A path that already carries a query (e.g.
"/connections?test=false") produces a malformed double-? URL that swallows embed and
breaks the embed (the dashboard never mounts). Always pass a query-free path and use the
testMode option / setTestMode() method instead.
Host/iframe contract: when testMode is provided, the SDK appends test=<bool> to the
initial iframe URL — the embedded dashboard reads it from its own URL on load. For the runtime
toggle, setTestMode(enabled) sends postMessage({ action: "setTestMode", testMode }) (accepted
only from the configured baseUrl origin); the embedded dashboard must handle that action to
switch mode without a reload.
Loading overlay
A loading overlay covers the iframe while initialization completes. You can provide a custom element via the loading config option, or the SDK renders a default spinner. The overlay fades out automatically (300ms transition) once the dashboard is authenticated and ready.
Auto height
By default the iframe fills its container (height: 100%) and the dashboard content
scrolls inside it. Pass autoHeight: true to size the iframe to the dashboard's content
height instead, so the whole host page scrolls naturally with no inner scrollbar:
initDashboard({
baseUrl: "https://dashboard.y.uno",
container: document.getElementById("dashboard")!,
token,
autoHeight: true,
});Host/iframe contract: when autoHeight is enabled, the embedded dashboard reports its
content height to the host via postMessage({ action: "resize", height }) whenever the
content resizes; the SDK applies that height to the iframe and its wrapper. Messages are
accepted only from the configured baseUrl origin. Your container must allow vertical growth
(avoid a fixed height / overflow: hidden) for the effect to be visible.
Overlays (modals & drawers)
An embedded overlay is positioned relative to its iframe, not the host page — so a
modal would appear centered to the iframe panel, not the browser viewport. To fix this,
the embedded dashboard signals the SDK when an overlay opens or closes
(embed-event with domain: "ui", action: "overlay-open" | "overlay-close"), and the
SDK expands the iframe to the full viewport (position: fixed; inset: 0, top
z-index) for as long as any overlay is open, then restores it. This is automatic — no
host code required. Signals are ref-counted, so stacked overlays are handled correctly.
While expanded the iframe is transparent, and the SDK captures the iframe's prior
panel position (getBoundingClientRect) and sends it to the dashboard as an offset
(postMessage({ action: "overlay-mode", active, offset })). The dashboard uses that
offset to leave the host's chrome regions unpainted, so your surrounding UI stays visible
through the iframe while the overlay is centered to the page. No host code required.
Host/iframe contract: while an overlay is open the iframe covers the whole viewport
(transparent) at a maximal z-index. Host chrome behind the iframe is visible but not
interactive during the overlay (the iframe captures pointer events) — expected for a
modal. Messages are accepted only from the configured baseUrl origin.
Migrating from 0.x to 1.0
1.0.0 drops the Yuno brand prefix from the public API so the SDK reads cleanly
in white-labelled integrations. The old names were removed — there is no
compatibility shim, so update every import and call site:
| 0.x (removed) | 1.0 (new) |
|---|---|
| initYunoDashboard(config) | initDashboard(config) |
| getYunoDashboard() | getDashboard() |
| destroyYunoDashboard() | destroyDashboard() |
| YunoDashboard (class) | Dashboard |
| YunoDashboardConfig (type) | DashboardConfig |
- import { initYunoDashboard, getYunoDashboard, destroyYunoDashboard } from "@yuno-payments/dashboard-embed-sdk";
- import type { YunoDashboardConfig } from "@yuno-payments/dashboard-embed-sdk";
+ import { initDashboard, getDashboard, destroyDashboard } from "@yuno-payments/dashboard-embed-sdk";
+ import type { DashboardConfig } from "@yuno-payments/dashboard-embed-sdk";
- const sdk = initYunoDashboard(config);
+ const sdk = initDashboard(config);Behavior, config options, methods, callbacks, and the PostMessage host/iframe contract are unchanged — this release renames the API surface only.
Development
npm install
npm run build # Build with tsup
npm run dev # Watch mode
npm run type-check # TypeScript check