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-svgreact-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 deviceAdd 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
HatchedClientwith 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
| Component | Surface |
|---|---|
<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
| Hook | Returns |
|---|---|
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/browserHatchedClientSDK calls, not for<HatchedProvider>. - Missing optional peer:
<HatchedWebView>throws a clear error ifreact-native-webviewis not installed; the buddy radar silently falls back to bars withoutreact-native-svg. - Read-only token on an interactive screen: purchases/equips/track return
403on 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
Originheader. The widget API treats a tokened request with noOriginas a non-browser client and authorizes it on the JWT alone (the origin allowlist only constrains browser requests, which always sendOrigin).<HatchedWebView>loads from a Hatched origin and is constrained to Hatched origins for navigation.