HatchedDocs
Guides

React Native integration

Embed Hatched gamification in Expo and bare React Native apps with @hatched/react-native — native widgets, a WebView fallback, and one provider.

@hatched/react-native brings the widget layer to mobile. Seven surfaces render as native React Native components; the complex and ceremony surfaces render through a <HatchedWebView> that loads the same production widget bundle you use on the web. One <HatchedProvider> owns the shared data layer — polling, theme, and event tracking — so every widget on the screen stays in sync.

It targets Expo (managed) and bare React Native, and shares its data and theme core (@hatched/widget-core) with the web widgets, so behaviour matches across platforms.

Install

npx expo install @hatched/react-native
# optional peers — install only the ones you use:
npx expo install react-native-webview react-native-svg
  • react-native-webview — required only to mount <HatchedWebView>.
  • react-native-svg — used by the buddy skill radar; without it the buddy widget falls back to skill bars.

Bare React Native: install the same packages with your package manager and run pod install for iOS. Both optional peers are autolinked; no config plugin is needed for Expo.

The short version

import {
  HatchedProvider,
  BuddyWidget,
  LeaderboardWidget,
  MarketplaceWidget,
  StreakWidget,
} from '@hatched/react-native';

export function GamificationScreen({ token }: { token: string }) {
  return (
    <HatchedProvider
      token={token}
      apiBaseUrl="https://api.hatched.live/api/v1"
      theme="light"
    >
      <BuddyWidget />
      <StreakWidget streakKey="daily_lesson" />
      <LeaderboardWidget scope="team" />
      <MarketplaceWidget />
    </HatchedProvider>
  );
}

The provider polls /widget/state, hydrates a shared store, resolves the tenant's theme, and exposes a track() dispatcher. It pauses polling while the app is backgrounded (AppState), honors the OS reduced-motion setting, and refreshes the theme when the app returns to the foreground — the mobile equivalent of the web loader's focus-based live theme sync.

Token choice

The provider's token is a Hatched widget token — the same JWT the web loader uses, minted on your server with a secret API key. There are two kinds, and the choice is identical to the web widget token model:

Use a widget session token for anything interactive (tracking, purchases, equips):

import { HatchedClient } from '@hatched/sdk-js';

const hatched = new HatchedClient({ apiKey: process.env.HATCHED_API_KEY! });

const session = await hatched.widgetSessions.create({
  buddyId: 'bdy_abc',
  userId: 'user_42',
  scopes: ['read', 'events:track'],
  ttlSeconds: 60 * 15,
});
// send session.token down to the device

Add the marketplace scopes — marketplace:browse, marketplace:purchase, items:equip — for a shopping screen. They are plan-gated, so requesting them on a plan without the marketplace capability fails the mint with 403 plan_feature_locked.

Use a read-only embed token when nothing on the screen is interactive:

const embed = await hatched.embedTokens.create({
  buddyId: 'bdy_abc',
  userId: 'user_42',
  ttlSeconds: 60 * 60,
});

Never ship a secret API key (or HatchedClient with one) inside the app bundle. Mint tokens on your backend and hand the short-lived JWT to the device. There is no publishable-key path for the widget runtime — the /widget/* endpoints accept only session and embed tokens.

MarketplaceWidget reads the token's scopes from /widget/state: on a read-only mount the buy/equip CTAs are hidden automatically, matching the web read-only behaviour.

The widgets

ComponentSurface
<BuddyWidget allowRadar? />Buddy image, name, coins, skills (bars or SVG radar), badge/rank counters, evolution-ready hint.
<LeaderboardWidget scope? viewMode? limit? />Ranked board; the current player's row is highlighted.
<MarketplaceWidget />Browse, buy, and equip cosmetics. Scope-gated.
<StreakWidget streakKey? />Current streak, best run, and next milestone.
<BadgesWidget />Earned badges with an earned/total counter.
<TokensWidget />Primary spendable balance plus capped progression tokens.
<PathWidget pathKey? />Journey steps and sub-steps; available sub-steps with manual completion are tappable and update optimistically.

BuddyWidget, BadgesWidget, TokensWidget, and MarketplaceWidget read state hydrated by the provider's /widget/state poll. StreakWidget, PathWidget, and LeaderboardWidget additionally fetch their own endpoint. Per-instance identity is a prop (streakKey, pathKey), the native equivalent of the web mount's data-streak-key / data-path-key.

WebView fallback

The long-tail and ceremony surfaces render through <HatchedWebView>, which loads the production CDN widget and injects the token after load (never in the HTML). The loader's bridge forwards track effects and celebrations back into the shared store, so a native BuddyWidget reflects a purchase made inside the WebView marketplace.

import { HatchedWebView } from '@hatched/react-native';

<HatchedWebView widget="hexad-survey" />

Supported widget values: kudos, group-quest, feed, mystery-box, league, council, hexad-survey, and celebrate. The buddy hatch ceremony and the marketplace dress-up flow are also WebView-only — they are CSS- and video-heavy, so they keep full fidelity inside the WebView rather than degrading natively.

<HatchedWebView> reads its token, API base, and theme from the enclosing <HatchedProvider>; you can also pass them as props to mount it standalone.

Tracking events

import { useTrack } from '@hatched/react-native';

function CompleteLessonButton() {
  const track = useTrack();
  // Effects (coins, badges, streaks) reconcile into every mounted widget.
  return <Button title="Done" onPress={() => track('lesson_completed', { lessonId })} />;
}

The session must include events:track. The API derives user_id, buddy_id, and audience from the token, so the device cannot spoof another player — the same guarantee as the web track().

Hooks

HookReturns
useTrack()(type, properties?) => Promise<TrackResult | null>
useHatched(){ store, token, apiBaseUrl, ready, track, refresh }
useTheme()the resolved ResolvedTheme
useWorkspaceStore(selector)a reactive slice of the shared store

Theming

The provider fetches /widget/theme and resolves the tenant's --hw-* palette and six personality axes into a flat, StyleSheet-friendly theme — the same personality dimensions and design tokens as the web widgets. Edits a teammate saves in Widget Studio reach the app on next foreground.

CSS features React Native can't express degrade to the nearest token rather than crashing: SVG surface textures, glass/crt surfaces, color-mix()/oklab() colors, and gradients fall back to a flat token. For pixel-exact fidelity on a given surface, use <HatchedWebView>.

Fonts

The personality typography pair maps to font family names (e.g. Gluten, Bebas Neue). React Native silently falls back to the system font when a family is not loaded, so load the families you want with expo-font (or linked assets) if you need exact brand type; otherwise the widget renders in the system font.

Common pitfalls

  • Secret key in the app bundle: never. Mint session/embed tokens on your backend and pass the JWT to the device.
  • No publishable-key path: the widget runtime accepts only session and embed tokens. A publishable key (hatch_pk_…) is for server/browser HatchedClient SDK calls, not for <HatchedProvider>.
  • Missing optional peer: <HatchedWebView> throws a clear error if react-native-webview is not installed; the buddy radar silently falls back to bars without react-native-svg.
  • Read-only token on an interactive screen: purchases/equips/track return 403 on an embed token. Use a session token with the right scopes.
  • Expired token: widget tokens are short-lived. Re-mint on resume / route change and pass the fresh token to the provider.
  • Origin allowlist: a native app sends no browser Origin header. The widget API treats a tokened request with no Origin as a non-browser client and authorizes it on the JWT alone (the origin allowlist only constrains browser requests, which always send Origin). <HatchedWebView> loads from a Hatched origin and is constrained to Hatched origins for navigation.