unshared-react-sdk
v2.3.0
Published
React SDK for Unshared Labs — provider + hooks over unshared-frontend-sdk with tag-manager-style publishable-key delivery
Maintainers
Readme
unshared-react-sdk
React SDK for Unshared Labs — account-sharing detection for React apps. A provider + hooks layer over unshared-frontend-sdk with automatic SPA route tracking and flagged-account handling.
Works the way Adobe Launch or BlueConic tags work: configure it with the publishable key we issue you and you're done — no backend integration required. (A proxy mode through your own backend is also supported.)
Pass
publishableKeyexplicitly for direct mode orbaseUrlexplicitly for proxy mode. Omitting both defaults to same-origin proxy mode — unless you're consuming a client-specific build with a baked-in key (see "Packaged publishable key").
Install
npm install unshared-react-sdkReact 18+ is a peer dependency.
Not on npm yet? The current build is hosted at https://unshared-public.s3.amazonaws.com/react@18/ (index.mjs for bundlers, index.umd.js for script tags, index.d.ts for types) — see "Publishing React artifacts to S3" below.
Quick start (direct mode — recommended)
Wrap your app once, near the root, with the publishable key from your Unshared Labs dashboard:
import { UnsharedProvider } from 'unshared-react-sdk';
function Root() {
const { user } = useAuth(); // your auth state
return (
<UnsharedProvider
publishableKey="upk_your_key_here"
user={user ? { userId: user.id, email: user.email } : null}
>
<App />
</UnsharedProvider>
);
}That's everything:
- A device fingerprint is collected once per tab and submitted when the user is identified.
- SPA navigations (
pushState,replaceState,popstate) are tracked automatically — Next.js, React Router, and custom routers all work without adapters. - Logout (
user→null) clears all SDK state. - All failures are silent — the SDK never throws at runtime and never blocks your UI.
The publishable key is public by design: it can only submit events, never read data. Lock it to your domains by configuring allowed origins in your dashboard.
Identifying the user from deeper in the tree
If auth state isn't available where the provider mounts:
import { useUnshared } from 'unshared-react-sdk';
function LoginForm() {
const { identify, logout } = useUnshared();
async function onLoginSuccess(user) {
identify({ userId: user.id, email: user.email });
}
async function onLogout() {
logout();
}
}Reacting to flagged accounts
<UnsharedProvider
publishableKey="upk_..."
user={user}
onFlagged={() => router.push('/account-suspended')}
>onFlagged fires when any same-page fetch returns an account_flagged 403, or when an unshared:flagged CustomEvent is dispatched. The provider installs (and removes on unmount) a window.fetch interceptor to detect this.
Interstitial modal
Instead of (or alongside) onFlagged, the provider can render a server-driven
interstitial modal — a JSON flow authored in the Unshared dashboard (e.g. email-OTP
verification) fetched from the backend and rendered in a Shadow-DOM modal whose actions
route back through the SDK's own verification calls.
<UnsharedProvider
publishableKey="upk_..."
user={user}
enableInterstitial // auto-show when flagged
interstitialFlowType="email_verification" // optional, this is the default
>With enableInterstitial, the modal renders automatically on the same flagged signals
as onFlagged (the 403 interceptor and the unshared:flagged event). To trigger it
yourself instead, call useUnshared().showInterstitial(). The modal is removed on
logout/unmount (client.destroy()).
This works in proxy mode as well: set baseUrl (instead of publishableKey) with the Node middleware mounted, and the modal is served and verified through your backend with the secret key — no publishable key in the frontend.
Props
| Prop | Type | Description |
|------|------|-------------|
| publishableKey | string | Direct mode. Publishable key (upk_…). Events go straight to the Unshared Labs platform. May only be omitted in client-specific builds with a baked-in key. |
| apiUrl | string | Direct mode platform origin. Default https://api.unshared.ai. |
| baseUrl | string | Proxy mode. Base URL of your backend running unshared-clientjs-sdk middleware. Use baseUrl="" for same-origin proxy mode. |
| user | { userId, email, isPaidSubscriber? } \| null | The logged-in user. null while logged out; the transition to null clears SDK state. isPaidSubscriber: false means the SDK collects and submits nothing for this user — non-subscriber data is never received. |
| onFlagged | () => void | Called when the platform flags the account. |
| enableInterstitial | boolean | Direct mode: auto-render the interstitial modal when flagged (on the 403 interceptor or unshared:flagged event). Default false. |
| interstitialFlowType | string | Flow type to fetch for the interstitial. Default email_verification. |
| skipPaths | string[] | Path prefixes to never submit events for. |
| includePathPrefix | string[] | If set, only these path prefixes submit events. |
| sessionId | () => string \| undefined | Supply your own session ID (e.g. your auth/analytics session) instead of the SDK-generated UUID — it becomes the session_hash correlation key on every event. |
| deviceId | () => string \| undefined | Supply your own device ID instead of the stable fingerprint hash. |
| maxRetries | number | Delivery retries per request (default 3). |
| timeout | number | Per-request timeout ms (default 30000). |
useUnshared()
Returns { client, identify, logout, showInterstitial }. client is the underlying UnsharedBrowser instance (null during SSR and before mount). showInterstitial(flowType?) fetches and renders the interstitial modal on demand (direct mode). Throws if called outside an <UnsharedProvider>.
In direct mode, client also carries the read/verification API — enough to build a fully frontend verify-wall:
const { client } = useUnshared();
const check = await client?.checkUser();
if (check?.data?.is_user_flagged) {
await client.triggerEmailVerification(); // emails a 6-digit code
// ...collect the code from the user...
const result = await client.verify(enteredCode); // → { verified: true }
}triggerEmailVerification and verify are never retried by the SDK (each trigger sends a real email; verify attempts are budgeted server-side). Rate-limited responses include error.retryAfter seconds for countdown UIs.
Script tag / UMD (no bundler)
The published dist/index.umd.js is fully self-contained except for React itself — unshared-frontend-sdk (and its fingerprint engine) are bundled in. Load React 18's UMD first, then this package; the API lands on window.UnsharedReact:
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unshared-public.s3.amazonaws.com/react@18/index.umd.js"></script>
<script>
const { UnsharedProvider, useUnshared } = window.UnsharedReact;
// Use exactly like the npm import, e.g. wrap your root render:
// ReactDOM.createRoot(el).render(
// React.createElement(UnsharedProvider, { publishableKey: 'upk_...', user }, React.createElement(App))
// );
</script>React 19 ships no UMD builds — React 19 apps must use the npm path (which is unaffected; the package targets
react >= 18either way). Pages without React at all should use the plain browser SDK //v2/sdk/{key}.jsloader — or the standalone build below.
Standalone: true one-tag-and-done (niche — read the warnings)
dist/index.standalone.js (~200K min) bundles React, ReactDOM, and the SDK and auto-mounts an invisible provider from window.__unshared — no React on the page, no glue code.
When NOT to use it:
- Never on a page that already runs React. The standalone's React renders an isolated, detached tree — it can't break the host's hooks/context (the trees never touch), but it also means
useUnshared()from the host's own components cannot see this provider. React apps must use the npm/ESM path (peer-dependency React, single instance) or the UMD. - Pages with no React should usually prefer the browser tag loader (
/v2/sdk/{key}.js) — it has no React dependency at all, is ~4× smaller, and does the same job. Reach for the standalone only when you specifically want the provider-style config surface (window.__unsharedidentity contract,identify/logout) on a React-less page.
React remains a peer dependency (react >= 18) for every module output (ESM/CJS/UMD) — the host app supplies the single React instance; only this opt-in standalone file carries its own copy.
<script>
window.__unshared = {
publishableKey: 'upk_...', // omit in client-keyed builds
identity: () => window.Arc?.identity
? { userId: Arc.identity.uuid, email: Arc.identity.email, isPaidSubscriber: true }
: null,
onFlagged: () => location.assign('/account-suspended'), // optional
// sessionId, deviceId, skipPaths, includePathPrefix also accepted
};
</script>
<script async src="https://unshared-public.s3.amazonaws.com/react@18/index.standalone.js"></script>If auth resolves after the tag loads, call window.UnsharedReact.identify({ userId, email, isPaidSubscriber }) / window.UnsharedReact.logout().
SSR / Next.js
The provider is SSR-safe: the SDK is constructed in a mount effect and all browser APIs are touched client-side only. In the Next.js App Router, mount it in a "use client" layout component.
Proxy mode
If you prefer events to flow through your own backend (secret API key stays server-side, richer middleware features like verdict caching and email verification):
// Same-origin proxy mode:
<UnsharedProvider baseUrl="" user={user}>
// Cross-origin proxy mode:
<UnsharedProvider baseUrl="https://app.example.com" user={user}>Your backend must run unshared-clientjs-sdk's createUnsharedMiddleware. See that package's docs.
Packaged publishable key
For client-specific builds, the React SDK can inherit a publishable key packaged into unshared-frontend-sdk:
- Explicit
publishableKeyprop. - Build-time
UNSHARED_PUBLISHABLE_API_KEYbaked intounshared-frontend-sdk(client-specific builds only, e.g. the S3 flow). - Otherwise no key — same-origin proxy mode. There is no hardcoded fallback; builds made without the env var (including every npm release) never carry a usable key.
Build distribution files in order (the React build bundles whatever browser dist it finds, so the browser build's env var determines whether a key is baked in):
UNSHARED_PUBLISHABLE_API_KEY=upk_test_or_client_key npm --prefix sdks/javascript/browser run build
npm --prefix sdks/javascript/frameworks/react run buildPassing baseUrl, including baseUrl="", keeps proxy mode and suppresses the packaged-key fallback.
Publishing React artifacts to S3
The canonical public distribution lives at s3://unshared-public/react@18/ (versioned per major):
https://unshared-public.s3.amazonaws.com/react@18/index.mjs ← ESM (primary; bundlers / import maps)
https://unshared-public.s3.amazonaws.com/react@18/index.umd.js ← UMD (script tag; page provides window.React)
https://unshared-public.s3.amazonaws.com/react@18/index.standalone.js ← one-tag: bundles React, auto-mounts from window.__unshared
https://unshared-public.s3.amazonaws.com/react@18/index.cjs ← CommonJS
https://unshared-public.s3.amazonaws.com/react@18/index.d.ts ← TypeScript typesEvery file is self-contained except for React itself (unshared-frontend-sdk and the fingerprint engine are bundled at build time). The publish script uploads dist/index.mjs; pass the prefix as part of the bucket value to target the versioned path:
S3_BUCKET_NAME=unshared-public/react@18 npm --prefix sdks/javascript/frameworks/react run publish:s3S3_BUCKET_NAME is intentionally deployment config rather than a hardcoded value in the script; the additional dist files (index.umd.js, index.cjs, index.d.ts) are uploaded alongside when a full set is wanted.
Before uploading, the script runs aws sts get-caller-identity. If credentials are missing or expired, it runs aws login and checks identity again. For SSO/profile setups, override the login command:
AWS_LOGIN_COMMAND="aws sso login --profile client-profile" \
S3_BUCKET_NAME=client-sdk-bucket \
npm --prefix sdks/javascript/frameworks/react run publish:s3Force publishing is available for local or emergency use, but should not be used for normal releases:
FORCE_PUBLISH=1 \
S3_BUCKET_NAME=client-sdk-bucket \
npm --prefix sdks/javascript/frameworks/react run publish:s3Only react remains an external dependency of the S3 artifacts — index.umd.js is the script-tag option (see "Script tag / UMD" above) and index.mjs suits bundlers or browser import maps that map react.
Releasing
unshared-frontend-sdk is a file: devDependency that gets bundled into every dist output (ESM/CJS/UMD) at build time — consumers only ever need React. No dependency rewriting is required at publish; release-js.yml (and the S3 publish script) build the browser package first so rollup can resolve it.
