Getting started
Get credentials, add a widget token route, wrap your app in a provider and render your first embedded component.
This package is the public npm surface for Tesouro embedded React widgets.
@tesouro/embedded-components-react is an ESM package for bundler-based React
apps (Vite, webpack, esbuild, Next.js). react and react-dom (v19) are peer
dependencies.
These steps put one widget on screen: a token route on your server, a provider in the browser, and the widget itself. The examples use the Next.js App Router.
Using a coding assistant? Point it at
llms-full.txt, which has every
page of the docs site as Markdown in one file.
llms.txt is the index.
Before you start
You need three credentials from Tesouro: a client ID, a client secret and a widget secret. Authentication › User token in the Embedded Banking or FinOps docs explains how to request them. All three stay on your server.
1. Install
npm install @tesouro/embedded-components-react
npm install @tesouro/embedded-components-widget-tokenThe second package mints widget tokens and handles your secrets, so only your server imports it.
2. Add a token route
A widget acts as one of your users, authenticated by a short-lived widget token
that your server mints. This route handler, at app/api/widget-token/route.ts,
returns the { widgetToken, exp } shape the provider in step 3 expects:
import { NextRequest, NextResponse } from 'next/server';
import { configureCreateWidgetToken } from '@tesouro/embedded-components-widget-token';
const mintWidgetToken = configureCreateWidgetToken({
clientId: process.env.TESOURO_CLIENT_ID!,
clientSecret: process.env.TESOURO_CLIENT_SECRET!,
widgetSecret: process.env.TESOURO_WIDGET_SECRET!,
});
export async function POST(req: NextRequest) {
const session = await getSession(req);
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { widgetToken, exp } = await mintWidgetToken({
userId: session.userId,
userEmail: session.userEmail,
organizationReference: session.organizationReference,
});
return NextResponse.json(
{ widgetToken, exp },
{ headers: { 'Cache-Control': 'no-store' } },
);
}getSession stands in for your own server-side session check. Take the user
and their organizationReference (your stable ID for the business they belong
to) from that session, never from the request: the route mints a valid token
for whatever identity it's given. Never expose the three credentials to the
browser, for example through a NEXT_PUBLIC_ variable.
@tesouro/embedded-components-widget-token
documents the other options, including the token lifetime.
Backend not on Node? Build the token yourself: Authentication › User token in
the Embedded Banking
or FinOps
docs specifies its claims and encryption. Your route returns the same
{ widgetToken, exp }.
3. Add the provider
RefreshingRootWidgetProvider calls your route on mount, fetches a new token
shortly before exp, and shares the signed-in scope with every widget inside
it. It runs in the browser, so in the App Router it goes in a Client Component
that your root layout renders, such as app/providers.tsx. Import the
stylesheet there too: widgets render unstyled without it.
'use client';
import '@tesouro/embedded-components-react/styles.css';
import type { ReactNode } from 'react';
import {
RefreshingRootWidgetProvider,
type WidgetTokenFetcher,
} from '@tesouro/embedded-components-react';
const fetcher: WidgetTokenFetcher = async () => {
const res = await fetch('/api/widget-token', { method: 'POST' });
if (!res.ok) throw new Error('Widget token request failed');
return res.json();
};
export function Providers({ children }: { children: ReactNode }) {
return (
<RefreshingRootWidgetProvider
baseUrl="https://api.sandbox.tesouro.com"
fetcher={fetcher}
>
{children}
</RefreshingRootWidgetProvider>
);
}Use https://api.sandbox.tesouro.com while you build and
https://api.tesouro.com in production.
4. Render a widget
Any widget inside the provider is ready to use:
import { BankAccountsWidget } from '@tesouro/embedded-components-react';
export default function AccountsPage() {
return <BankAccountsWidget />;
}Browse the widgets for Embedded Banking or FinOps. Every provider and widget prop is typed in the shipped declarations, so your editor documents them inline.
Entry points
| Import path | What you get |
|---|---|
@tesouro/embedded-components-react | All widgets, WidgetSuite, and the providers, including RefreshingRootWidgetProvider. |
@tesouro/embedded-components-react/styles.css | The widget stylesheet. See About the stylesheet. |
@tesouro/embedded-components-react/core | Framework-agnostic token utilities (createWidgetTokenManager). |
@tesouro/embedded-components-react/experimental | Work in progress, with no compatibility promise. See below. |
Experimental entry point
@tesouro/embedded-components-react/experimental (and .../experimental/<Module>)
holds work in progress. It is outside semver: exports
can change or disappear in any release, including a patch, and aren't documented
here. Use it only if we point you at a specific module, and pin an exact version.
About the stylesheet
The stylesheet won't restyle your host page. It ships no global CSS reset (the
reset is scoped under the .tesouro-embedded wrapper every widget renders), and
all utilities are ttw-prefixed, so it won't collide with your own Tailwind.
Design tokens (--ttw-*) are declared on :root / .dark. Dark mode follows an
ancestor .dark class, and you can set the widget font with --ttw-font-family.
Theming
Widgets read their look from --ttw-* CSS variables. Set them on :root, or
on any element around the widgets, in plain CSS or with createTheme.
Basic theming is all most integrations need: your brand color, the corner radius and the font.
import type { CSSProperties } from 'react';
import { createTheme, themeToCss } from '@tesouro/embedded-components-react';
const theme = createTheme({
primary: '#0f766e',
primaryForeground: '#ffffff',
radius: '0.5rem',
fontFamily: 'Inter, sans-serif',
});
// On a wrapper around the widgets…
<div style={theme as CSSProperties}>{/* widgets */}</div>;
// …or once for the page: themeToCss(theme) prints a `:root { … }` rule.A wrapper theme reaches the widgets' dialogs, sheets and menus too, although
they render at the end of <body>: the widgets copy the wrapper's values onto
their overlay root each time they render.
Nested widgets share that overlay root. When a themed widget sits inside
another widget (or inside another WidgetProvider / RootWidgetProvider
scope), its dialogs, sheets and menus render into the outer scope's overlay
root and so take the outer theme. If two widgets need different themes, theme
the outermost wrapper, or render them side by side rather than nested.
Advanced theming reaches every public variable:
| Group | Keys (createTheme) and variables |
|---|---|
| Surfaces | background, foreground, card, cardForeground, popover, popoverForeground, muted, mutedForeground (--ttw-background …) |
| Actions | primary, primaryForeground, secondary, secondaryForeground, accent, accentForeground, destructive, destructiveForeground |
| Lines | border, input, ring |
| Tones | info, success, warning, and destructive: one color each for a status badge |
| Accents | violet, cyan: for a status no tone carries |
| Overrides | <tone>Subtle and <tone>Foreground (--ttw-success-subtle, --ttw-success-foreground; for destructive, destructiveSubtle and destructiveSubtleForeground), all optional |
| Overlay | overlay: the scrim behind dialogs and sheets, black at 50% by default |
| Shape/type | radius (0–1.25rem), fontFamily |
A tone or accent is one color. The badge's tinted fill and its text are
derived from it where the badge is painted, so they follow a dark card or a
tinted page without extra variables; set the Subtle / Foreground override
only to pick either half yourself.
Compatibility variables. surfaceSubtle (--ttw-surface-subtle) and the
eight sidebar keys (sidebar, sidebarForeground, sidebarPrimary,
sidebarPrimaryForeground, sidebarAccent, sidebarAccentForeground,
sidebarBorder, sidebarRing) keep working for existing themes and will be
removed in a future major version. None is declared: until you set one,
surfaceSubtle is muted and each sidebar color is its base color (card,
foreground, primary, accent, border, ring). A new theme can leave
them out.
In development, createTheme warns once in the console about unknown keys,
text under 4.5:1 contrast (3:1 for mutedForeground on muted) and a radius
outside 0–1.25rem, and the widgets run the same contrast check once against
the theme they actually render with.
Nothing warns in production, and a warning never stops a widget rendering.
themeIssues(tokens) returns the same findings as data, for a test in your
own CI; a variable you leave out is judged at its default, so
themeIssues(createTheme({ primary })) checks primary against the default
primaryForeground.
A status colour you set (info, success, warning, destructive,
violet, cyan) gets its pale fill and its text derived from it. Browsers
without CSS relative colours derive them a different way, so themeIssues
checks each one both ways: a contrast issue means it fails in current
browsers, and a fallback-contrast issue means it only fails in those older
ones. Setting its text colour explicitly (warningForeground, or
destructiveSubtleForeground for destructive) fixes both. If your CI
switches on issue.kind, handle fallback-contrast too.
Only the variables above are supported. Tailwind's own variables share the
--ttw- prefix (--ttw-spacing, --ttw-text-sm, --ttw-color-* …) because
the utilities are prefixed ttw; they are pinned inside the widgets, so
setting one has no effect. --ttw-chart-* were removed.
UI frameworks (shadcn default, Tecton optional)
Widgets render with the default shadcn surface and need no configuration. Tecton (the Q2 / Stencil design system) ships as a separate, opt-in package:
npm i @tesouro/embedded-components-react-tecton-uiimport { registerTectonUI } from '@tesouro/embedded-components-react-tecton-ui';
registerTectonUI();Call registerTectonUI() once at app startup, then set uiFramework="tecton" on a provider. Without the extension installed it
falls back to shadcn. See the extension's README for setup.
TypeScript module resolution
Set "moduleResolution" to "bundler" (or "node16" / "nodenext"); every
entry point then resolves, and the declarations type-check with
skipLibCheck: false. Legacy "node" / node10 resolution is not supported
(it ignores the exports map and reports TS2307). The package is ESM-only and
can't be require()d from CommonJS.