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:

LayerHow to setPriority
WidgetProvider<WidgetProvider widgetToken="..."> in the treeHighest
RootWidgetProvider<RootWidgetProvider baseUrl="..." ...> at rootMid
Global storesetGlobalWidgetConfig({ ... }) outside ReactLowest

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

PropTypeDescription
baseUrlstringBase URL of the Tesouro embedded API, such as "https://api.tesouro.com". Falls back to global.baseUrl.
widgetTokenstring | nullBearer token for widget auth. Falls back to global.widgetToken; null suppresses auth.
organizationIdstring | nullOrg ID sent with data requests. Falls back to global.organizationId, then to initResponse.organizationId; null clears org scoping.
configClient
(client: EmbeddedClient) => EmbeddedClient
(client: EmbeddedClient) => EmbeddedClient
Post-creation hook for the HTTP client, applied after the built-in auth and routing interceptors. Falls back to global.configClient.
gatewayRoutingbooleanForces widget-gateway routing on (true) or off (false); unset, it applies automatically to known Tesouro API hosts. Falls back to global.gatewayRouting.
linkComponentLinkComponentHost link component widgets use for navigational links. Falls back to global.linkComponent.
uiFramework'shadcn' | 'tecton' | nullUI framework for this tree; falls back to global.uiFramework, then 'shadcn'.
analyticsbooleanEnables anonymous analytics (default true); false disables capture and skips loading PostHog.
unstable_initResponseOverrideWidgetInitResponseFirst-party hosts only: skips the init fetch and uses this object as initResponse. Embed integrations minting widget JWEs must leave this unset.
disclosuresAcceptanceSlotReactNodeAccept surface shown when the user owes disclosures; pass <AcceptDisclosuresWidget />. Cascades to nested providers.
childrenSlotReactNodeThe React subtree that consumes the widget context.
Label overrides
PropTypeDescription
providerLabelsLabelsPartial<WidgetProviderLabels>Overrides the copy a provider renders in place of a widget (error fallback, disclosures gate). Cascades to nested providers.

RootWidgetProvider doesn't take errorFallback / onError. Set them on a WidgetProvider inside 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 (baseUrl or widgetToken set): loads its own init and creates its own client for this subtree only. While its fetch is in flight it shows the parent's initResponse.
  • Standalone (no provider ancestor): behaves like RootWidgetProvider. Use this for widgets mounted as isolated web components with their own React root.

Props

PropTypeDescription
baseUrlstringOverrides the base URL for this subtree and creates a new client. Inherits when omitted.
widgetTokenstring | nullOverrides the token for this subtree and reloads init; null suppresses auth here. Inherits when omitted.
organizationIdstring | nullOverrides the org ID for this subtree without reloading init; null clears org scoping. Inherits when omitted.
configClient
(client: EmbeddedClient) => EmbeddedClient
(client: EmbeddedClient) => EmbeddedClient
Post-creation hook for this provider's client; not called in pass-through mode. Inherits when omitted.
gatewayRoutingbooleanForces widget-gateway routing on or off for this subtree's client; unset, it applies to known Tesouro API hosts. Inherits when omitted.
linkComponentLinkComponentOverrides the host link component for this subtree. Inherits when omitted.
uiFramework'shadcn' | 'tecton' | nullOverrides the UI framework for this subtree; omitted or null inherits, defaulting to 'shadcn'.
errorFallbackSlot
ReactNode | (props: FallbackProps) => ReactNode
ReactNode | (props: FallbackProps) => ReactNode
Custom error-boundary fallback: a node, or a function receiving { error, resetErrorBoundary }.
onErrorEvent
(error: unknown, info: ErrorInfo) => void
(error: unknown, info: ErrorInfo) => void
Telemetry hook, called once per caught error before the fallback renders.
analyticsbooleanEnables anonymous analytics (default true); honored only in standalone mode, ignored on a nested provider.
disclosuresAcceptanceSlotReactNodeAccept surface shown when the user owes disclosures; pass <AcceptDisclosuresWidget />. Inherits when omitted.
childrenSlotReactNodeThe React subtree that consumes the overridden context.
Label overrides
PropTypeDescription
providerLabelsLabelsPartial<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(): WidgetConfig

useWidgetLoading()

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(): boolean

useWidgetError()

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 | null

useRefetchWidget()

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(): () => void

useOwnWidgetConfig()

Only the fields set explicitly at the current provider level, never initResponse; null outside any provider.

useOwnWidgetConfig(): WidgetConfigInput | null

Error 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();
  • setGlobalWidgetConfig replaces the whole config; omitted fields become undefined.
  • updateGlobalWidgetConfig merges a patch; use it for single-field updates such as a new token.
  • getGlobalWidgetConfig is a non-reactive read. initResponse can'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 an initResponse means a refetch failed but the previous init is still valid. Show error UI only when initResponse is absent.
  • Forward state.error.message, not the whole error, which carries the gateway response body.
  • Only a top-level provider (a RootWidgetProvider or standalone WidgetProvider) publishes here. Nested overrides are visible only through useWidgetConfig().

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;
}

On this page