Component and root providers
Cascading React context providers that give embedded widgets an authenticated scope.
Cascading React context providers that give embedded widgets an authenticated scope. They create the API client, load widget init, and pass config down the tree so each widget needs no setup of its own.
Overview
Config resolves from three layers. Each level inherits every field it doesn't set from the level above:
| Layer | How to set | Priority |
|---|---|---|
| WidgetProvider | <WidgetProvider widgetToken="..."> in the tree | Highest |
| RootWidgetProvider | <RootWidgetProvider baseUrl="..." ...> at root | Mid |
| Global store | setGlobalWidgetConfig({ ... }) outside React | Lowest |
For widgetToken and organizationId, undefined means "inherit", null means "explicitly cleared" (descendants see no value even if an ancestor set one), and a string overrides any ancestor.
Once baseUrl and widgetToken both resolve, the provider loads widget init and exposes the result as initResponse through useWidgetConfig(). initResponse carries the user's status, scopes, disclosuresRequired and organizationId. When no level sets organizationId, it defaults to initResponse.organizationId, so org-scoped widgets wait for init. Changing organizationId alone doesn't reload init.
Every WidgetProvider, including the one each widget mounts, wraps its subtree in an error boundary (see Error boundary). RootWidgetProvider doesn't. For automatic token rotation, use WidgetTokenRefreshProvider.
Quick Start
Single provider for the whole app
import {
RootWidgetProvider,
BalancesWidget,
} from '@tesouro/embedded-components-react';
export function App() {
return (
<RootWidgetProvider
baseUrl="https://api.tesouro.com"
widgetToken="wt_live_abc123"
organizationId="org_xyz"
>
<BalancesWidget />
</RootWidgetProvider>
);
}Widget as a self-contained web component
With no provider ancestor, a WidgetProvider acts as its own root.
import { WidgetProvider } from '@tesouro/embedded-components-react';
function PaymentWidget({ baseUrl, token }: { baseUrl: string; token: string }) {
return (
<WidgetProvider baseUrl={baseUrl} widgetToken={token}>
<PaymentWidgetContent />
</WidgetProvider>
);
}Providers
RootWidgetProvider
Place this once near the root of your React tree. It resolves baseUrl, widgetToken and organizationId from its props (falling back to the global store), loads widget init, and provides the HTTP client and query cache to every descendant. Rotating widgetToken doesn't recreate the client. Requests to known Tesouro API hosts are routed through the widget gateway automatically; set gatewayRouting only for a custom domain (true) or a host that routes its own requests (false). Pass configClient as a stable reference (hoisted or useCallback); a new function recreates the client.
Props
| Prop | Type | Description |
|---|---|---|
baseUrl | string | Base URL of the Tesouro embedded API, such as "https://api.tesouro.com". Falls back to global.baseUrl. |
widgetToken | string | null | Bearer token for widget auth. Falls back to global.widgetToken; null suppresses auth. |
organizationId | string | null | Org ID sent with data requests. Falls back to global.organizationId, then to initResponse.organizationId; null clears org scoping. |
configClient |
| Post-creation hook for the HTTP client, applied after the built-in auth and routing interceptors. Falls back to global.configClient. |
gatewayRouting | boolean | Forces widget-gateway routing on (true) or off (false); unset, it applies automatically to known Tesouro API hosts. Falls back to global.gatewayRouting. |
linkComponent | LinkComponent | Host link component widgets use for navigational links. Falls back to global.linkComponent. |
uiFramework | 'shadcn' | 'tecton' | null | UI framework for this tree; falls back to global.uiFramework, then 'shadcn'. |
analytics | boolean | Enables anonymous analytics (default true); false disables capture and skips loading PostHog. |
unstable_initResponseOverride | WidgetInitResponse | First-party hosts only: skips the init fetch and uses this object as initResponse. Embed integrations minting widget JWEs must leave this unset. |
disclosuresAcceptanceSlot | ReactNode | Accept surface shown when the user owes disclosures; pass <AcceptDisclosuresWidget />. Cascades to nested providers. |
childrenSlot | ReactNode | The React subtree that consumes the widget context. |
Label overrides
| Prop | Type | Description |
|---|---|---|
providerLabelsLabels | Partial<WidgetProviderLabels> | Overrides the copy a provider renders in place of a widget (error fallback, disclosures gate). Cascades to nested providers. |
RootWidgetProviderdoesn't takeerrorFallback/onError. Set them on aWidgetProviderinside it.
WidgetProvider
Mid-tree provider for overriding config for one subtree. It has three modes:
- Pass-through (no props): inherits everything; no fetch, no new client. Loading, error and refetch come from the nearest ancestor that owns a fetch.
- Override (
baseUrlorwidgetTokenset): loads its own init and creates its own client for this subtree only. While its fetch is in flight it shows the parent'sinitResponse. - Standalone (no provider ancestor): behaves like
RootWidgetProvider. Use this for widgets mounted as isolated web components with their own React root.
Props
| Prop | Type | Description |
|---|---|---|
baseUrl | string | Overrides the base URL for this subtree and creates a new client. Inherits when omitted. |
widgetToken | string | null | Overrides the token for this subtree and reloads init; null suppresses auth here. Inherits when omitted. |
organizationId | string | null | Overrides the org ID for this subtree without reloading init; null clears org scoping. Inherits when omitted. |
configClient |
| Post-creation hook for this provider's client; not called in pass-through mode. Inherits when omitted. |
gatewayRouting | boolean | Forces widget-gateway routing on or off for this subtree's client; unset, it applies to known Tesouro API hosts. Inherits when omitted. |
linkComponent | LinkComponent | Overrides the host link component for this subtree. Inherits when omitted. |
uiFramework | 'shadcn' | 'tecton' | null | Overrides the UI framework for this subtree; omitted or null inherits, defaulting to 'shadcn'. |
errorFallbackSlot |
| Custom error-boundary fallback: a node, or a function receiving { error, resetErrorBoundary }. |
onErrorEvent |
| Telemetry hook, called once per caught error before the fallback renders. |
analytics | boolean | Enables anonymous analytics (default true); honored only in standalone mode, ignored on a nested provider. |
disclosuresAcceptanceSlot | ReactNode | Accept surface shown when the user owes disclosures; pass <AcceptDisclosuresWidget />. Inherits when omitted. |
childrenSlot | ReactNode | The React subtree that consumes the overridden context. |
Label overrides
| Prop | Type | Description |
|---|---|---|
providerLabelsLabels | Partial<WidgetProviderLabels> | Overrides the copy this provider renders in place of the widget. Inherits when omitted. |
UI framework
uiFramework picks the look ('shadcn' default, or 'tecton') and keeps the widget API identical. It cascades like other config, so a subtree can opt in on its own:
import {
RootWidgetProvider,
WidgetProvider,
UIFramework,
} from '@tesouro/embedded-components-react';
<RootWidgetProvider baseUrl={API_URL} widgetToken={token}>
<Dashboard />
<WidgetProvider uiFramework={UIFramework.Tecton}>
<PilotWidget />
</WidgetProvider>
</RootWidgetProvider>;Hooks
useWidgetConfig()
Returns the resolved config at the nearest provider, including initResponse. Outside any provider it reads the global store.
useWidgetConfig(): WidgetConfiguseWidgetLoading()
true while the nearest fetch-owning provider has a request in flight; false outside any provider. If initResponse is still defined while loading, it is a background refetch and the previous data is safe to show.
useWidgetLoading(): booleanuseWidgetError()
The latest init error from the nearest fetch-owning provider, or null. It clears when the token or base URL changes or a fetch succeeds. A first-load failure leaves initResponse undefined; a failed refetch keeps the previous one. API errors carry the response body as error.cause, so don't log the whole error object.
useWidgetError(): Error | nulluseRefetchWidget()
A stable function that reloads init at the nearest fetch-owning provider, keeping the current initResponse visible meanwhile. A no-op outside any provider.
useRefetchWidget(): () => voiduseOwnWidgetConfig()
Only the fields set explicitly at the current provider level, never initResponse; null outside any provider.
useOwnWidgetConfig(): WidgetConfigInput | nullError boundary
Every WidgetProvider catches render-time exceptions in its subtree, so one failing widget doesn't crash the page. RootWidgetProvider has no boundary of its own: widgets are covered by the WidgetProvider they mount, but your own JSX placed directly under the root isn't, so wrap it in a WidgetProvider if it needs one. Resetting the boundary also retries failed queries, and a token change resets it automatically. Don't add your own boundary to catch widget errors; pass errorFallback / onError instead.
<RootWidgetProvider baseUrl="https://api.tesouro.com" widgetToken={token}>
<WidgetProvider
errorFallback={({ error, resetErrorBoundary }) => (
<ErrorPanel error={error} onRetry={resetErrorBoundary} />
)}
onError={(error, info) => errorTracker.capture(error, { extra: info })}
>
<Dashboard />
</WidgetProvider>
</RootWidgetProvider>Custom JSX placed directly inside RootWidgetProvider, outside any WidgetProvider, isn't covered by crash telemetry.
Provider copy
A provider renders its own UI in place of a widget in two cases: the error-boundary fallback, and the disclosures gate (see AcceptDisclosuresWidget). Override that copy with providerLabels; unlisted keys keep their defaults, and a descendant's providerLabels replaces an ancestor's rather than merging.
<RootWidgetProvider
baseUrl="https://api.tesouro.com"
widgetToken={token}
disclosuresAcceptance={<AcceptDisclosuresWidget />}
providerLabels={{
disclosuresRequiredTitle: 'Review your account agreements to continue',
}}
>
<Dashboard />
</RootWidgetProvider>See the exported WidgetProviderLabels type for every key. Without disclosuresAcceptance, a gated widget can only tell the user that acceptance is owed, with no way to act on it.
Non-React hosts
Global config store
Push config into the tree from outside React, before or after mount; mounted providers update automatically.
import {
setGlobalWidgetConfig,
updateGlobalWidgetConfig,
getGlobalWidgetConfig,
} from '@tesouro/embedded-components-react';
setGlobalWidgetConfig({ baseUrl: 'https://api.tesouro.com', widgetToken });
updateGlobalWidgetConfig({ widgetToken: 'wt_rotated' });
const config = getGlobalWidgetConfig();setGlobalWidgetConfigreplaces the whole config; omitted fields becomeundefined.updateGlobalWidgetConfigmerges a patch; use it for single-field updates such as a new token.getGlobalWidgetConfigis a non-reactive read.initResponsecan't be set here.
Observing widget init
getGlobalWidgetInitState() and subscribeToGlobalWidgetInitState(listener) report the init lifecycle to hosts without hooks. The state is a WidgetInitState union: 'idle', 'loading', 'ready' (with initResponse) or 'error' (with error, and possibly initResponse).
import {
getGlobalWidgetInitState,
subscribeToGlobalWidgetInitState,
} from '@tesouro/embedded-components-react';
const initState = getGlobalWidgetInitState();
const unsubscribe = subscribeToGlobalWidgetInitState((state) => {
host.toggleSpinner(state.status === 'idle' || state.status === 'loading');
if (state.status === 'ready') {
host.setScopes(state.initResponse.scopes);
}
});- The listener fires only on change, so also read the current state.
- The state stays
'idle'until a token is available, so treat'idle'like'loading'while your host mints a token. 'loading'means nothing usable yet; a background refetch stays'ready'.'error'with aninitResponsemeans a refetch failed but the previous init is still valid. Show error UI only wheninitResponseis absent.- Forward
state.error.message, not the whole error, which carries the gateway response body. - Only a top-level provider (a
RootWidgetProvideror standaloneWidgetProvider) publishes here. Nested overrides are visible only throughuseWidgetConfig().
Examples
Gating the app on init
function App() {
return (
<RootWidgetProvider
baseUrl={API_URL}
widgetToken={token}
disclosuresAcceptance={<AcceptDisclosuresWidget />}
>
<WidgetReadyGate>
<AppShell />
</WidgetReadyGate>
</RootWidgetProvider>
);
}
function WidgetReadyGate({ children }: { children: React.ReactNode }) {
const { initResponse } = useWidgetConfig();
const loading = useWidgetLoading();
const error = useWidgetError();
const refetch = useRefetchWidget();
if (loading && !initResponse) return <Skeleton />;
if (error && !initResponse) return <ErrorBanner onRetry={refetch} />;
return <>{children}</>;
}
function CardsNavItem() {
const { initResponse } = useWidgetConfig();
if (!initResponse?.scopes.includes('cards:read')) return null;
return <NavLink to="/cards">Cards</NavLink>;
}Gate on initResponse, not on loading alone: during a token rotation the previous response stays visible so on-screen widgets don't unmount.
Per-subtree token override
<RootWidgetProvider baseUrl={API_URL} widgetToken={platformToken}>
<MainContent />
<WidgetProvider widgetToken={partnerToken}>
<PartnerWidget />
</WidgetProvider>
</RootWidgetProvider>PartnerWidget gets the partner's initResponse, and useRefetchWidget() inside it refetches with partnerToken.
organizationId scoping
<RootWidgetProvider
baseUrl={API_URL}
widgetToken={token}
organizationId={currentOrgId}
>
<OrgDashboard />
<WidgetProvider organizationId={otherOrgId}>
<OtherOrgPanel />
</WidgetProvider>
<WidgetProvider organizationId={null}>
<GlobalPanel />
</WidgetProvider>
</RootWidgetProvider>organizationId={null} clears org scoping for that subtree.
Analytics
The package sends a few anonymous PostHog events (provider lifecycle, API status and timing by route template, uncaught widget errors); no query strings, path IDs, pageviews or person profiles. Set analytics={false} on RootWidgetProvider or a standalone WidgetProvider to turn it off; on a nested provider the prop is ignored.
TypeScript
import type {
WidgetConfigInput,
WidgetConfig,
WidgetInitResponse,
WidgetInitState,
WidgetProviderLabels,
WidgetProviderProps,
RootWidgetProviderProps,
LinkComponent,
} from '@tesouro/embedded-components-react';
import {
UIFramework,
type EmbeddedClient,
} from '@tesouro/embedded-components-react';WidgetInitResponse is the init payload you gate on:
interface WidgetInitResponse {
bankName?: string | null;
disclosuresRequired?: 'NOT_REQUIRED' | 'REQUIRED';
organizationId?: string | null;
organizationTypes?: Array<
| 'BANK'
| 'EMBEDDED'
| 'PLATFORM'
| 'TRANSACTOR'
| 'VERTICAL_SOFTWARE_PROVIDER'
> | null;
scopes: string[];
status?: 'ACTIVE' | 'INACTIVE' | 'INVITED' | 'NOT_FOUND';
userId?: string | null;
vspName?: string | null;
}