npm.io
0.1.1 • Published 2 weeks ago

@formations-embedded/react

Licence
UNLICENSED
Version
0.1.1
Deps
5
Size
12.7 MB
Vulns
0
Weekly
0

@formations-embedded/react

React component library partners install to embed Formations S-Corp workflows directly inside their own product.

This package is currently 0.1.0 — pre-publish. Public API is liable to change until we tag 1.0.0.

Installation

npm install @formations-embedded/react react react-dom

Material UI, Emotion, and React Query are bundled inside the package. The library only declares react and react-dom (>=17) as peer dependencies.

Usage

import { Dashboard, Formations } from "@formations-embedded/react";
import "@formations-embedded/react/index.css";

function HomeDashboard({ accessToken }: { accessToken: string }) {
  return (
    <Formations environment="SANDBOX" accessToken={accessToken}>
      <Dashboard />
    </Formations>
  );
}

That's the minimum needed to render an embedded dashboard. <Formations> wraps children in the shared Formations MUI theme automatically — you do not need a separate ThemeProvider unless you want to customize it (see Material UI theme).

see Token lifecycle below.

Material UI theme

Embedded components use Material UI with the same Formations design tokens as the main app. <Formations> applies FormationsThemeProvider (ThemeProvider + StyledEngineProvider injectFirst) automatically.

To reuse the theme in tests or Storybook:

import {
  FormationsThemeProvider,
  theme,
  FormationsPrimaryButton,
} from "@formations-embedded/react";
import { MockTheme } from "@formations-embedded/react/testing";

render(
  <MockTheme>
    <Dashboard />
  </MockTheme>,
);

Export theme, palette, typography, and button helpers when building custom embed UI that should match Formations styling.

Icons (FormationsIcon) live in a separate entry so the default embed bundle stays small — import only when needed:

import { FormationsIcon } from "@formations-embedded/react/icon";

<Formations> props

Prop Type Required Description
environment "SANDBOX" | "PRODUCTION" yes Which Formations backend the API client targets.
accessToken string yes A customer-scoped JWT minted by your backend.
refreshAccessToken (ctx) => Promise<string> no Called when the token is about to expire or after a 401. Resolve with a fresh token to keep the session alive.
refreshLeadTimeMs number no How early (ms) before exp to refresh proactively. Defaults to 60_000.
apiBaseUrlOverride string no Point the client at a non-default base URL (useful when developing against a local Formations backend).
onError (error: FormationsError) => void no Notified for unrecoverable transport / auth errors.
queryClient QueryClient no React Query client instance. Defaults to the package singleton. Import QueryClient from this package if you need a shared client.

React Query

React Query v3 is bundled — you do not install it separately. Import hooks and types from @formations-embedded/react:

import { useQuery, useQueryClient, queryClient, type UseQueryOptions } from "@formations-embedded/react";

<Formations> wraps children in a QueryClientProvider using the shared queryClient singleton (same defaults as the main Formations app: 5-minute staleTime / cacheTime, no retries, no refetch on window focus).

Data fetching follows the same three-layer pattern as the main codebase:

Component  →  hooks/api/*  →  services/*
Query hooks
import { useMe, useDashboard } from "@formations-embedded/react";

function Profile() {
  const { me, isLoading, error } = useMe();
  const { dashboard, refetch } = useDashboard();
  // ...
}
Adding new hooks
  1. Add a service function in services/ that accepts ApiClient.
  2. Create hooks/api/useXxx.ts wrapping useQuery / useMutation.
  3. Export from the package index.ts when it's part of the public API.
// hooks/api/useWidgets.ts (template)
import {
  useQuery,
  useQueryClient,
  useMutation,
  type UseQueryOptions,
  useFormations,
} from "@formations-embedded/react";
import { getWidgets, createWidget } from "../../services/widgets";

export const useWidgets = (options?: UseQueryOptions<Widget[]>) => {
  const { api, accessToken } = useFormations();
  const { data, ...rest } = useQuery<Widget[]>(
    ["widgets", accessToken],
    () => getWidgets(api),
    { enabled: !!accessToken, ...options },
  );
  return { widgets: data ?? [], ...rest };
};
Cache management

Import the singleton when you need cache operations outside React:

import { queryClient } from "@formations-embedded/react";

queryClient.invalidateQueries(["dashboard"]);

Inside hooks, prefer useQueryClient() from @formations-embedded/react.

Sharing a query client

<Formations> wraps children in its own QueryClientProvider by default. Pass queryClient to share a single client across multiple embeds, or create one with the re-exported QueryClient class:

import { Formations, QueryClient } from "@formations-embedded/react";

const sharedClient = new QueryClient();

<Formations queryClient={sharedClient} ...>
Testing
import {
  createQueryWrapper,
  mockedQueryClient,
} from "@formations-embedded/react/testing";

beforeEach(async () => {
  await mockedQueryClient.resetQueries();
});

const { result } = renderHook(() => useMe(), {
  wrapper: createQueryWrapper(),
});

Token lifecycle

  1. Your backend mints a short-lived (≤ 60 min) customer-scoped JWT by calling Formations' partner endpoints — see BACKEND_API.md section 1.
  2. You pass that JWT in as accessToken. The library decodes the exp claim locally to schedule proactive refresh.
  3. refreshLeadTimeMs before exp, the library invokes your refreshAccessToken callback. Your callback typically fetches a new token from your own backend (which performs the S2S exchange).
  4. If a request happens to race the refresh and gets a 401, the library calls refreshAccessToken (sharing the in-flight promise if one is already running) and retries the request exactly once.
  5. If refreshAccessToken rejects or isn't provided, the library fires onError with code: "AUTH_FAILED". The host app is responsible for re-authenticating the user.
<Formations
  environment="SANDBOX"
  accessToken={accessToken}
  refreshAccessToken={async ({ reason }) => {
    // Call YOUR backend, not Formations directly. Your backend then
    // does the S2S exchange with Formations.
    const res = await fetch("/api/formations-token", {
      headers: { "x-refresh-reason": reason },
    });
    if (!res.ok) throw new Error(`refresh failed: ${res.status}`);
    const body = await res.json();
    return body.accessToken;
  }}
  onError={(err) => console.error("[Formations]", err)}
>
  <Dashboard />
</Formations>
Why not have the library refresh the token itself?

Refreshing requires partner credentials, and partner credentials must never live in the browser. The library deliberately delegates refresh to your backend so secrets stay on the server.

Components

<Dashboard />

Renders the S-Corp summary view. Loads GET /api/embedded/v1/dashboard on mount and re-renders when the token rotates. See DashboardData for the response shape.

Hooks (advanced)

useMe() / useDashboard()

Typed React Query hooks for the built-in endpoints. Return domain-named data plus standard query fields (isLoading, error, refetch, etc.).

useFormations()

Returns { environment, accessToken, api, tokenManager }. Use api.request<T>(path, opts) to call any Formations endpoint with the current access token (it handles 401 → refresh → retry for you).

import { useFormations, useApiResource } from "@formations-embedded/react";

function MyEmbed() {
  const { api } = useFormations();
  const { data, loading, refetch } = useApiResource<MyShape>("/api/embedded/v1/something");
  // ...
}
useApiResource<T>(path)

React Query-backed generic fetcher for arbitrary paths. Automatically re-runs when the token rotates. Returns { data, error, loading, refetch }. Prefer typed hooks in hooks/api for known endpoints.

Errors

All recoverable failures resolve to a FormationsError:

interface FormationsError {
  code: "AUTH_FAILED" | "TOKEN_EXPIRED" | "NETWORK_ERROR" | "API_ERROR" | "CONFIG_ERROR";
  message: string;
  status?: number;
  cause?: unknown;
}

The onError callback on <Formations> receives these as they happen. Hooks like useApiResource also expose the error via their error field.

Backend dependency

The library expects specific endpoints on the Formations backend. See BACKEND_API.md for the full contract — that file is the source of truth for backend implementation work.

Development

The fastest inner loop is to run the sample app and edit library source directly — Vite aliases @formations-embedded/react to this package's src/ so HMR fires on every save:

# from repo root
npm install
npm run dev      # opens the DevUI playground at http://localhost:5173

For raw build / typecheck:

npm run build --workspace @formations-embedded/react
npm run typecheck --workspace @formations-embedded/react
Bundle size

The production build minifies and tree-shakes dependencies. Approximate sizes after npm run build --workspace @formations-embedded/react:

Artifact Minified Gzipped
Main (index.js) ~230 KB ~73 KB
Icons (icon.js + font) ~4.2 MB ~1.5 MB
Testing (testing.js) ~116 KB

The main bundle includes MUI, Emotion, and React Query. Material Symbols fonts are not loaded unless you import @formations-embedded/react/icon. Test helpers import from @formations-embedded/react/testing.