@getrheo/react-native-expo 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,241 @@
1
+ # @getrheo/react-native-expo
2
+
3
+ Expo and Expo dev-client entry for the Rheo React Native SDK. Re-exports `@getrheo/react-native-core` and registers **Expo** adapters (`expo-video`, `expo-store-review`). Install **one** flavor: not `@getrheo/react-native-bare`.
4
+
5
+ React + React Native SDK for Rheo. The state machine lives in `@getrheo/flow-runtime` and schemas live in `@getrheo/contracts`; only rendering is platform-specific.
6
+
7
+ ## Usage
8
+
9
+ The publishable key identifies an app and environment (test or live). Pick a
10
+ **channel** per flow surface (`channelId` on `<Flow />` or on
11
+ `useFlow({ channelId })`); that channel decides which flow version the SDK
12
+ serves. You no longer pass a flow id to `useFlow`.
13
+
14
+ ### Production
15
+
16
+ The SDK default **`apiBaseUrl`** is **`https://api.getrheo.io`**. Omit it in production unless you self-host. When using **`ob_pk_live_*`** keys, never point at localhost.
17
+
18
+ ```tsx
19
+ <RheoProvider
20
+ config={{
21
+ publishableKey: 'ob_pk_live_xxx',
22
+ apiBaseUrl: 'https://api.getrheo.io', // optional — matches default
23
+ userId: 'u_1',
24
+ }}
25
+ >
26
+ ```
27
+
28
+ ### Identity
29
+
30
+ - **`userId`** (optional): Primary id for analytics and experiment bucketing.
31
+ When omitted on **web**, the SDK generates a UUID once and stores it under
32
+ `localStorage['rheo_app_user_id']`. In other runtimes (Node, SSR) it
33
+ uses an in-memory singleton until you pass an explicit `userId` — **React
34
+ Native** hosts should set `userId` (e.g. from AsyncStorage) until a storage
35
+ adapter ships.
36
+ - **`attribution`** (optional): When **not** set to `{ enabled: false }`, the SDK
37
+ can listen for **`react-native-appsflyer`** when your app installs it (not an SDK peer — integration only) and uses required peer **`@react-native-async-storage/async-storage`** after **channel resolve succeeds**,
38
+ but only when the workspace plan allows it (`features.attribution` on the resolve
39
+ response — **Indie** plans receive `false`) **and** AppsFlyer is enabled for the app
40
+ in **App settings → Integrations** (`integrations.appsflyer.enabled` on the resolve
41
+ response). Normalized install + deep-link payloads
42
+ become **universal `sdkAttributes` keys** (`acquisition.*`, `link.*`, `attribution.*`).
43
+ Values merge **on top of** host `sdkAttributes` for decision nodes. A **24h device cache**
44
+ (namespaced by `userId`) fills gaps when a cold open delivers no MMP payload;
45
+ **live callbacks always override** the cache. Pass `attribution: { storage: null }`
46
+ to disable persistence, or `providers: []` to disable all MMP adapters while
47
+ keeping the hook dormant. Add more providers later via `attribution.providers`.
48
+ - **`customUserId`** (optional): Your backend’s user id; sent as
49
+ `identity.customUserId` on every flushed event alongside `appUserId`, so the
50
+ dashboard can join to your systems. Optionally set once on `RheoProvider`;
51
+ **`useRheoCustomUserId()`** exposes `setCustomUserId()` so CRM ids can change
52
+ at runtime (updates apply to queued events **at flush** time).
53
+
54
+ ```tsx
55
+ import { Flow, RheoProvider, useRheoCustomUserId } from '@getrheo/react-native-expo';
56
+
57
+ function Screen() {
58
+ const { setCustomUserId } = useRheoCustomUserId();
59
+ // ...
60
+ return (
61
+ <Flow
62
+ channelId="ch_test_a1b2c3d4"
63
+ theme="dark"
64
+ onFlowCompleted={() => setCustomUserId('crm_known_user')}
65
+ />
66
+ );
67
+ }
68
+
69
+ const App = () => (
70
+ <RheoProvider
71
+ config={{
72
+ publishableKey: 'ob_pk_test_xxx',
73
+ customUserId: 'crm_initial',
74
+ userId: 'u_1',
75
+ sessionId: 'sess_xyz',
76
+ appVersion: '1.4.0',
77
+ }}
78
+ >
79
+ <Screen />
80
+ </RheoProvider>
81
+ );
82
+ ```
83
+
84
+ `<Flow />` is the fully managed React Native renderer — it resolves
85
+ the channel's flow, draws every layer kind (stack / text / image / button /
86
+ single-choice / multi-choice / text input / carousel), and emits the full
87
+ event taxonomy. Drop down to `useFlow({ channelId })` + `<LayerRenderer />` if
88
+ you need custom chrome.
89
+
90
+ ### Terminal payloads (`onFlowCompleted` / `onFlowAbandoned`)
91
+
92
+ Callbacks receive a versioned **`FlowTerminalSnapshot`** (`schemaVersion: 1`) meant for **`JSON.stringify` → your API**, CRM, or LLM prompts:
93
+
94
+ - **`terminal`**: `completed` | `abandoned`
95
+ - **`occurredAt`**: terminal timestamp (`FlowState.completedAt`)
96
+ - **`correlation`**: join keys only — `channelId`, `flowId`, `versionId`, `assignmentVersion`, `environment`, `experimentId`, `variantId` (no dashboard integration flags)
97
+ - **`subject`**: `appUserId`, optional `customUserId`, optional `sessionId`
98
+ - **`device`**: `locale`, `platform`, optional `appVersion`, optional `customProperties`
99
+ - **`answers`**: normalized field-key → value using `stepResponseToCompletionValue` for every stored step response (except stripped auth keys), plus **`null`** for any capture field on a **visited** screen (history + current screen) that never received a response—**input layers** (`single_choice`, `multiple_choice`, `text_input`, `scale_input`), **checkbox** field keys, **OS permission** synthetic keys (`permission:*`), and **app review** keys (`app_review:{layerId}`). Same completion rules as `buildCompletionResponses` for keys that do have responses.
100
+ - **`traits`**: single merged map used for decisions at terminal time (host + attribution + runtime patches)
101
+
102
+ Optional (all default **false** on `useFlow` / `<Flow />`):
103
+
104
+ - **`includeManifestInTerminalPayload`** → `manifest`
105
+ - **`includePathInTerminalPayload`** → `path` (walked screen / surface ids)
106
+ - **`includeAnswerDetailInTerminalPayload`** → `answersDetail` (raw step responses minus auth keys)
107
+
108
+ Migration from **`onFlowFinished`**: use **`onFlowCompleted`** / **`onFlowAbandoned`**; use **`payload.terminal`** if one shared handler must branch.
109
+
110
+ Explicit **`abandon()`** transitions to **`abandoned`**, enqueues **`flow_abandoned`** once, then runs **`onFlowAbandoned`**. Unmounting mid-flow still enqueues **`flow_abandoned`** when status was not already terminal (no duplicate when already **`abandoned`**).
111
+
112
+ ## How resolution works
113
+
114
+ 1. `useFlow({ channelId })` `POST /v1/sdk/resolve` with the SDK identity. Two headers are
115
+ required: `Authorization: Bearer <publishableKey>` and
116
+ `X-Rheo-Channel: <channelId>`.
117
+ 2. The server looks up the channel and returns the manifest from either:
118
+ - the channel's pinned version (`assignmentKind: "direct"`), or
119
+ - one of the running experiment's variant arms, deterministically bucketed
120
+ by `(experimentId, appUserId)` (`assignmentKind: "experiment"`).
121
+ 3. The response always includes `flowId`, `versionId`, `versionNumber`,
122
+ `assignmentVersion`, `channelId` and (when applicable) `experimentId` /
123
+ `variantId`. These are forwarded on every analytics event so funnels stay
124
+ attributable across version changes.
125
+ 4. **`mediaMap`** maps each referenced `mediaAssetId` (image / lottie layers) to
126
+ its public CDN URL. `<Flow />` and `useFlow` pass this through to
127
+ `LayerRenderer` automatically; custom UIs should pass `mediaMap` from
128
+ `useFlow` (or your resolve result) into `LayerRenderer`.
129
+
130
+ ### Caching
131
+
132
+ The resolve response carries an `ETag` of the form `"{assignmentVersion}-{versionId}"`.
133
+ `useFlow` loads a per-channel cache from AsyncStorage and sends `If-None-Match` only
134
+ when a validated entry exists; `304` reuses the cached manifest. `assignmentVersion`
135
+ is incremented on every channel pointer change, so re-validation is essentially free.
136
+
137
+ ### Resolve fallback
138
+
139
+ When `POST /v1/sdk/resolve` fails (network outage, 4xx/5xx, or any resolve error), `<Flow />` can show a **host-owned escape hatch** instead of blocking the user:
140
+
141
+ - Pass optional **`fallback`** (`ReactNode`) — your hardcoded offline onboarding (or any UI). Full-bleed; **no Rheo telemetry** on that surface; the SDK does not wire retry into host fallback (remount the screen to re-resolve).
142
+ - Omit **`fallback`** — the SDK shows a default screen: **"Error to load the content"** and a **Try again** button that calls **`useFlow().retry()`** (loading spinner while resolve runs).
143
+
144
+ `useFlow` exposes **`resolveFailed`** (`!loading && error && !manifest`), **`error`** (for logging), and **`retry()`**. Custom UIs branch on `resolveFailed` and render their own fallback.
145
+
146
+ Resolve still re-runs automatically on mount when `channelId` or identity deps change. After a successful resolve, terminal states (`completed` / `abandoned`) render nothing in `<Flow />` — navigate away in `onFlowCompleted` / `onFlowAbandoned`.
147
+
148
+ ### Failure modes
149
+
150
+ | Status | code | Typed error | Meaning |
151
+ |--------|------------------------------|-----------------------------------|----------------------------------------------------------|
152
+ | 400 | `channel_required` | `RheoChannelRequiredError` | The `X-Rheo-Channel` header was missing. |
153
+ | 404 | `channel_not_found` | `RheoChannelNotFoundError` | Unknown channel id, or wrong env for this publishable key. |
154
+ | 410 | `channel_archived` | `RheoChannelArchivedError` | Channel was archived in the dashboard. Unarchive to resume. |
155
+ | 404 | `channel_unassigned` | — | Channel has no flow assigned yet — set one in the dashboard. |
156
+ | 404 | `version_missing` | — | Channel pin references a version that was deleted. |
157
+ | 404 | `experiment_not_running` | — | Channel pin references an experiment that's draft/stopped. |
158
+ | 400 | `variant_pin_invalid` | — | One of the experiment's variants has no version pinned. |
159
+
160
+ > Experiments in `pending_decision` (auto-paused at their end date) are still
161
+ > served — bucketing stays frozen until an operator promotes a winner or
162
+ > extends the end date.
163
+
164
+ ## Events & batching
165
+
166
+ Every interaction emits a typed event. Events are queued in memory and
167
+ POSTed to `/v1/sdk/events` either every 5 seconds, when the buffer hits
168
+ 500 events, or immediately on `flow_completed` / `flow_abandoned` (so
169
+ funnel terminals are never lost to a flush window). When the queue holds
170
+ events for more than one channel, the SDK splits them into separate POSTs
171
+ (one `X-Rheo-Channel` header per batch, per API contract).
172
+
173
+ | Event | When it fires | Properties |
174
+ |-------------------|-------------------------------------------------------|-------------------------------------|
175
+ | `flow_started` | Once per resolved flow on first mount | — |
176
+ | `step_viewed` | When the rendered screen changes | — |
177
+ | `step_completed` | After a non-terminal screen submission (not skip) | — |
178
+ | `step_skipped` | When the flow records a skip response (e.g. skip button) | — |
179
+ | `choice_selected` | Per option for single-/multi-choice submissions | `field_key`, `value` |
180
+ | `text_submitted` | When a text input screen is submitted | `field_key`, `value` (+ classification) |
181
+ | `flow_completed` | When the flow reaches its terminal screen | `responseCount` |
182
+ | `flow_abandoned` | When **`useFlow().abandon()`** runs while running, **or** when the surface unmounts before completion (`completed` / `abandoned`) | — |
183
+
184
+ `text_submitted` events carry a `fieldClassification` so the API redacts
185
+ sensitive values before they reach ClickHouse.
186
+
187
+ ## In-app review (`request_app_review`)
188
+
189
+ Uses required peer **`expo-store-review`**: `hasAction()` → `requestReview()`, telemetry events, **~1.5s** delay when shown, then advance via default next. OS declines → **`not_shown`**.
190
+
191
+ ```bash
192
+ npx expo install expo-store-review
193
+ ```
194
+
195
+ ## Breaking change (built-in OS permissions)
196
+
197
+ Host apps must **stop** configuring `onOsPermission` on `RheoProvider` — it no longer exists. When a flow button uses **`request_os_permission`**, the SDK invokes [`react-native-permissions`](https://github.com/zoontek/react-native-permissions) and advances the manifest using **granted**, **denied**, or **blocked** outcomes:
198
+
199
+ | `permissionKey` | Native call (summary) |
200
+ |-------------------|------------------------|
201
+ | `notifications` | `requestNotifications` (`POST_NOTIFICATIONS` on Android 13+) |
202
+ | `camera` | `request` (`CAMERA`) |
203
+ | `microphone` | `request` (`RECORD_AUDIO` / microphone) |
204
+ | `photo_library` | `request` (`READ_MEDIA_IMAGES` on Android; `PHOTO_LIBRARY` on iOS) |
205
+ | `contacts` | `request` (`READ_CONTACTS` / `CONTACTS`) |
206
+ | `calendar` | `request` (`READ_CALENDAR`; `CALENDARS` on iOS — read/write access tier per Apple setup) |
207
+
208
+ Missing peer, missing native setup for the specific capability, or **web** → the SDK resolves **denied** (development builds log hints where applicable).
209
+
210
+ Additional built-in handlers will ship per `permissionKey` without bringing back a host hook.
211
+
212
+ ### Native checklist (engineering, per app)
213
+
214
+ - **Bare React Native**: add optional peer **`react-native-permissions`**. For each capability you ship in flows, add the matching entries to iOS **`setup_permissions`** and Android **`AndroidManifest.xml`**, plus the **Info.plist** usage descriptions the upstream README lists (for example **`NSPhotoLibraryUsageDescription`**, **`NSCalendarsFullAccessUsageDescription`** for calendar). Notifications still need **`POST_NOTIFICATIONS`** on Android 13+.
215
+ - **Expo**: configure plugin **`react-native-permissions`** and **`ios.infoPlist` / `android.permissions`** for everything you author (see [`apps/example-expo/app.json`](../../apps/example-expo/app.json)), run **`expo prebuild`** when regenerating native projects, **`pod install`**, rebuild the dev client. Older Android releases may still need **`READ_EXTERNAL_STORAGE`** for photo-library–style prompts if your `minSdk`/`targetSdk` require it. Push token registration stays app-specific beyond notification authorization.
216
+ - **Growth / dashboard**: branches are authored only in the builder; **no handler code**.
217
+
218
+ Requesting authorization is separate from registering for remote push tokens; token plumbing stays in the application if you alert server-side campaigns.
219
+
220
+ ---
221
+
222
+ ## Required peer dependencies (install with the SDK)
223
+
224
+ One install — all peers are **required** for the Expo flavor (no optional meta). **`@react-native-community/slider`** ships as a direct dependency of core.
225
+
226
+ ```bash
227
+ pnpm add @getrheo/react-native-expo \
228
+ react react-native \
229
+ react-native-permissions react-native-gesture-handler react-native-reanimated \
230
+ react-native-linear-gradient react-native-svg lottie-react-native \
231
+ react-native-vector-icons @react-native-async-storage/async-storage \
232
+ react-native-safe-area-context expo-store-review expo-video
233
+ ```
234
+
235
+ **Integrations (not SDK peers):** install **`react-native-appsflyer`** and/or **`react-native-purchases`** + **`react-native-purchases-ui`** only when you use attribution or RevenueCat paywall steps.
236
+
237
+ **Branding fonts:** use `buildBrandingFontLoadMap(branding)` from this package, then register faces with `expo-font` or linked assets.
238
+
239
+ ## Runnable example
240
+
241
+ [`apps/example-expo`](../../../apps/example-expo) — `pnpm dev:local:app:react-native` from repo root.
@@ -0,0 +1 @@
1
+ export * from '@getrheo/react-native-core';
package/dist/index.js ADDED
@@ -0,0 +1,265 @@
1
+ import { registerAppReviewAdapter, registerVideoAdapter } from '@getrheo/react-native-core/platform';
2
+ import * as StoreReview from 'expo-store-review';
3
+ import { useRef, useCallback, useEffect } from 'react';
4
+ import { View, Platform, Text } from 'react-native';
5
+ import { useVideoPlayer, VideoView } from 'expo-video';
6
+ import { screenBackgroundPlaybackId } from '@getrheo/contracts';
7
+ import { DEFAULT_PREVIEW_VIEWPORT_WIDTH_PX, resolveImageStyleAtWidth } from '@getrheo/flow-runtime';
8
+ import { ChromeView } from '@getrheo/react-native-core/ui/LayerRendererShared';
9
+ import { useMediaPlayback, useMediaPlaySignal, mediaAutoPlayOnMount } from '@getrheo/react-native-core/ui/mediaPlayback';
10
+ import { mediaLayerOuterLayoutPair, mediaLayerInnerFillStyle } from '@getrheo/react-native-core/ui/styles';
11
+ import { fireMediaOnComplete } from '@getrheo/react-native-core/ui/layers/mediaLayers';
12
+ import { jsx } from 'react/jsx-runtime';
13
+ export * from '@getrheo/react-native-core';
14
+
15
+ // src/registerExpoAdapters.ts
16
+ var APP_REVIEW_POST_PROMPT_DELAY_MS = 1500;
17
+ var delay = (ms) => new Promise((resolve) => {
18
+ setTimeout(resolve, ms);
19
+ });
20
+ var expoAppReviewAdapter = {
21
+ requestReview: async (sessionPlatform) => {
22
+ if (sessionPlatform === "web") {
23
+ return { shown: false };
24
+ }
25
+ let canShow;
26
+ try {
27
+ canShow = await StoreReview.hasAction();
28
+ } catch {
29
+ return { shown: false };
30
+ }
31
+ if (!canShow) {
32
+ return { shown: false };
33
+ }
34
+ try {
35
+ await StoreReview.requestReview();
36
+ await delay(APP_REVIEW_POST_PROMPT_DELAY_MS);
37
+ return { shown: true };
38
+ } catch {
39
+ return { shown: false };
40
+ }
41
+ }
42
+ };
43
+ var backdropLayout = {
44
+ position: "absolute",
45
+ top: 0,
46
+ left: 0,
47
+ right: 0,
48
+ bottom: 0,
49
+ zIndex: 0
50
+ };
51
+ var videoContentFitFor = (fit) => {
52
+ if (fit === "contain") return "contain";
53
+ if (fit === "fill") return "fill";
54
+ return "cover";
55
+ };
56
+ var ExpoVideoLayerView = ({ layer, ctx }) => {
57
+ const w = ctx.previewWidthPx ?? DEFAULT_PREVIEW_VIEWPORT_WIDTH_PX;
58
+ const resolvedStyle = resolveImageStyleAtWidth(layer.style, layer.styleBreakpoints, w);
59
+ const url = layer.media ? ctx.mediaMap?.[layer.media.mediaAssetId] : void 0;
60
+ const playback = useMediaPlayback();
61
+ const playSignal = useMediaPlaySignal(layer.id);
62
+ const completedRef = useRef(false);
63
+ const manualPlayRef = useRef(false);
64
+ const shouldAutoplay = mediaAutoPlayOnMount(layer);
65
+ const loopPlay = layer.loop !== false;
66
+ const muted = layer.audioEnabled !== true;
67
+ const player = useVideoPlayer(url ?? null, (p) => {
68
+ p.loop = loopPlay;
69
+ p.muted = muted;
70
+ if (!shouldAutoplay) {
71
+ p.pause();
72
+ try {
73
+ p.currentTime = 0;
74
+ } catch {
75
+ }
76
+ }
77
+ });
78
+ const pauseAtStart = useCallback(() => {
79
+ try {
80
+ player.pause();
81
+ player.currentTime = 0;
82
+ } catch {
83
+ }
84
+ }, [player]);
85
+ const play = useCallback(() => {
86
+ if (player.playing) return;
87
+ completedRef.current = false;
88
+ manualPlayRef.current = true;
89
+ try {
90
+ player.currentTime = 0;
91
+ } catch {
92
+ }
93
+ void player.play();
94
+ }, [player]);
95
+ useEffect(() => {
96
+ if (!playback) return;
97
+ return playback.register(layer.id, { play });
98
+ }, [playback, layer.id, play]);
99
+ useEffect(() => {
100
+ player.loop = loopPlay;
101
+ player.muted = muted;
102
+ }, [loopPlay, muted, player]);
103
+ useEffect(() => {
104
+ if (!url) return;
105
+ if (!shouldAutoplay) {
106
+ manualPlayRef.current = false;
107
+ pauseAtStart();
108
+ return;
109
+ }
110
+ void player.play();
111
+ }, [shouldAutoplay, url, player, ctx.screen.id, layer.id, pauseAtStart]);
112
+ useEffect(() => {
113
+ if (shouldAutoplay || playSignal === 0 || !url) return;
114
+ play();
115
+ }, [playSignal, shouldAutoplay, play, url]);
116
+ useEffect(() => {
117
+ if (shouldAutoplay || !url) return;
118
+ const sub = player.addListener("playingChange", ({ isPlaying }) => {
119
+ if (isPlaying && !manualPlayRef.current) {
120
+ player.pause();
121
+ try {
122
+ player.currentTime = 0;
123
+ } catch {
124
+ }
125
+ }
126
+ if (!isPlaying) manualPlayRef.current = false;
127
+ });
128
+ return () => sub.remove();
129
+ }, [shouldAutoplay, url, player]);
130
+ useEffect(() => {
131
+ if (!ctx.interactive || loopPlay) return;
132
+ const sub = player.addListener("playToEnd", () => {
133
+ if (completedRef.current) return;
134
+ completedRef.current = true;
135
+ fireMediaOnComplete(ctx, layer);
136
+ });
137
+ return () => sub.remove();
138
+ }, [ctx, layer, loopPlay, player]);
139
+ const { outerStyle, linearGradient } = mediaLayerOuterLayoutPair(
140
+ resolvedStyle,
141
+ ctx.manifest.theme,
142
+ ctx.theme,
143
+ ctx.branding
144
+ );
145
+ const placeholderBg = ctx.theme === "dark" ? "#18181b" : "#f4f4f5";
146
+ const hasAuthorBg = linearGradient != null || outerStyle.backgroundColor !== void 0;
147
+ const innerStyle = {
148
+ ...mediaLayerInnerFillStyle(resolvedStyle),
149
+ borderRadius: outerStyle.borderRadius ?? 10,
150
+ ...!hasAuthorBg && !url ? { backgroundColor: placeholderBg } : {}
151
+ };
152
+ const fit = resolvedStyle?.fit ?? "contain";
153
+ const contentFit = fit === "cover" ? "cover" : fit === "fill" ? "fill" : "contain";
154
+ const r = innerStyle.borderRadius;
155
+ return /* @__PURE__ */ jsx(ChromeView, { style: outerStyle, linearGradient, children: url ? /* @__PURE__ */ jsx(View, { style: innerStyle, children: /* @__PURE__ */ jsx(
156
+ VideoView,
157
+ {
158
+ player,
159
+ contentFit,
160
+ nativeControls: false,
161
+ style: {
162
+ width: "100%",
163
+ height: "100%",
164
+ ...r !== void 0 ? { borderRadius: r } : {}
165
+ }
166
+ }
167
+ ) }) : /* @__PURE__ */ jsx(View, { style: [innerStyle, { alignItems: "center", justifyContent: "center" }], children: /* @__PURE__ */ jsx(Text, { style: { color: "#71717a", fontSize: 11 }, children: "No media" }) }) });
168
+ };
169
+ var ExpoScreenShellVideoBackdrop = ({
170
+ screenId,
171
+ url,
172
+ fill,
173
+ ctx
174
+ }) => {
175
+ const playbackId = screenBackgroundPlaybackId(screenId);
176
+ const playback = useMediaPlayback();
177
+ const playSignal = useMediaPlaySignal(playbackId);
178
+ const completedRef = useRef(false);
179
+ const manualPlayRef = useRef(false);
180
+ const shouldAutoplay = mediaAutoPlayOnMount(fill);
181
+ const loopPlay = fill.loop !== false;
182
+ const muted = fill.audioEnabled !== true;
183
+ const player = useVideoPlayer(url, (p) => {
184
+ p.loop = loopPlay;
185
+ p.muted = muted;
186
+ if (!shouldAutoplay) {
187
+ p.pause();
188
+ try {
189
+ p.currentTime = 0;
190
+ } catch {
191
+ }
192
+ }
193
+ });
194
+ const pauseAtStart = useCallback(() => {
195
+ try {
196
+ player.pause();
197
+ player.currentTime = 0;
198
+ } catch {
199
+ }
200
+ }, [player]);
201
+ const play = useCallback(() => {
202
+ if (player.playing) return;
203
+ completedRef.current = false;
204
+ manualPlayRef.current = true;
205
+ try {
206
+ player.currentTime = 0;
207
+ } catch {
208
+ }
209
+ void player.play();
210
+ }, [player]);
211
+ useEffect(() => {
212
+ if (!playback) return;
213
+ return playback.register(playbackId, { play });
214
+ }, [playback, playbackId, play]);
215
+ useEffect(() => {
216
+ player.loop = loopPlay;
217
+ player.muted = muted;
218
+ }, [loopPlay, muted, player]);
219
+ useEffect(() => {
220
+ if (!shouldAutoplay) {
221
+ manualPlayRef.current = false;
222
+ pauseAtStart();
223
+ return;
224
+ }
225
+ void player.play();
226
+ }, [shouldAutoplay, url, player, pauseAtStart]);
227
+ useEffect(() => {
228
+ if (shouldAutoplay || playSignal === 0 || !url) return;
229
+ play();
230
+ }, [playSignal, shouldAutoplay, play, url]);
231
+ useEffect(() => {
232
+ if (!ctx.interactive || loopPlay) return;
233
+ const sub = player.addListener("playToEnd", () => {
234
+ if (completedRef.current) return;
235
+ completedRef.current = true;
236
+ const mode = fill.onComplete?.mode ?? "none";
237
+ if (mode === "next") ctx.onRespond?.({ kind: "cta", action: "primary" });
238
+ });
239
+ return () => sub.remove();
240
+ }, [ctx, fill.onComplete, loopPlay, player]);
241
+ return /* @__PURE__ */ jsx(View, { style: backdropLayout, pointerEvents: "none", collapsable: false, children: /* @__PURE__ */ jsx(
242
+ VideoView,
243
+ {
244
+ player,
245
+ pointerEvents: "none",
246
+ ...Platform.OS === "android" ? { surfaceType: "textureView" } : {},
247
+ style: {
248
+ width: "100%",
249
+ height: "100%",
250
+ opacity: fill.opacity ?? 1
251
+ },
252
+ contentFit: videoContentFitFor(fill.fit),
253
+ nativeControls: false
254
+ }
255
+ ) });
256
+ };
257
+
258
+ // src/registerExpoAdapters.ts
259
+ registerAppReviewAdapter(expoAppReviewAdapter);
260
+ registerVideoAdapter({
261
+ VideoLayerView: ExpoVideoLayerView,
262
+ ScreenShellVideoBackdrop: ExpoScreenShellVideoBackdrop
263
+ });
264
+ //# sourceMappingURL=index.js.map
265
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/expoAppReview.ts","../src/adapters/expoVideo.tsx","../src/registerExpoAdapters.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import * as StoreReview from 'expo-store-review';\nimport type { AppReviewAdapter } from '@getrheo/react-native-core/platform';\nimport type { AppReviewRequestResult } from '@getrheo/react-native-core';\n\nconst APP_REVIEW_POST_PROMPT_DELAY_MS = 1500;\n\nconst delay = (ms: number): Promise<void> =>\n new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n\nexport const expoAppReviewAdapter: AppReviewAdapter = {\n requestReview: async (sessionPlatform: string): Promise<AppReviewRequestResult> => {\n if (sessionPlatform === 'web') {\n return { shown: false };\n }\n\n let canShow: boolean;\n try {\n canShow = await StoreReview.hasAction();\n } catch {\n return { shown: false };\n }\n\n if (!canShow) {\n return { shown: false };\n }\n\n try {\n await StoreReview.requestReview();\n await delay(APP_REVIEW_POST_PROMPT_DELAY_MS);\n return { shown: true };\n } catch {\n return { shown: false };\n }\n },\n};\n","import { useCallback, useEffect, useRef } from 'react';\nimport { Platform, Text, View } from 'react-native';\nimport type { ViewStyle } from 'react-native';\nimport { VideoView, useVideoPlayer, type VideoContentFit } from 'expo-video';\nimport { screenBackgroundPlaybackId } from '@getrheo/contracts';\nimport { DEFAULT_PREVIEW_VIEWPORT_WIDTH_PX, resolveImageStyleAtWidth } from '@getrheo/flow-runtime';\nimport { ChromeView } from '@getrheo/react-native-core/ui/LayerRendererShared';\nimport {\n mediaAutoPlayOnMount,\n useMediaPlayback,\n useMediaPlaySignal,\n} from '@getrheo/react-native-core/ui/mediaPlayback';\nimport {\n mediaLayerInnerFillStyle,\n mediaLayerOuterLayoutPair,\n} from '@getrheo/react-native-core/ui/styles';\nimport { fireMediaOnComplete } from '@getrheo/react-native-core/ui/layers/mediaLayers';\nimport type {\n ScreenShellVideoBackdropProps,\n VideoLayerViewProps,\n} from '@getrheo/react-native-core/platform';\n\nconst backdropLayout: ViewStyle = {\n position: 'absolute',\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n zIndex: 0,\n};\n\nconst videoContentFitFor = (\n fit: 'cover' | 'contain' | 'fill' | undefined,\n): VideoContentFit => {\n if (fit === 'contain') return 'contain';\n if (fit === 'fill') return 'fill';\n return 'cover';\n};\n\nexport const ExpoVideoLayerView = ({ layer, ctx }: VideoLayerViewProps) => {\n const w = ctx.previewWidthPx ?? DEFAULT_PREVIEW_VIEWPORT_WIDTH_PX;\n const resolvedStyle = resolveImageStyleAtWidth(layer.style, layer.styleBreakpoints, w);\n const url = layer.media ? ctx.mediaMap?.[layer.media.mediaAssetId] : undefined;\n const playback = useMediaPlayback();\n const playSignal = useMediaPlaySignal(layer.id);\n const completedRef = useRef(false);\n const manualPlayRef = useRef(false);\n const shouldAutoplay = mediaAutoPlayOnMount(layer);\n const loopPlay = layer.loop !== false;\n const muted = layer.audioEnabled !== true;\n\n const player = useVideoPlayer(url ?? null, (p) => {\n p.loop = loopPlay;\n p.muted = muted;\n if (!shouldAutoplay) {\n p.pause();\n try {\n p.currentTime = 0;\n } catch {\n // metadata not ready\n }\n }\n });\n\n const pauseAtStart = useCallback((): void => {\n try {\n player.pause();\n player.currentTime = 0;\n } catch {\n // player not ready\n }\n }, [player]);\n\n const play = useCallback(() => {\n if (player.playing) return;\n completedRef.current = false;\n manualPlayRef.current = true;\n try {\n player.currentTime = 0;\n } catch {\n // metadata not ready\n }\n void player.play();\n }, [player]);\n\n useEffect(() => {\n if (!playback) return;\n return playback.register(layer.id, { play });\n }, [playback, layer.id, play]);\n\n useEffect(() => {\n player.loop = loopPlay;\n player.muted = muted;\n }, [loopPlay, muted, player]);\n\n useEffect(() => {\n if (!url) return;\n if (!shouldAutoplay) {\n manualPlayRef.current = false;\n pauseAtStart();\n return;\n }\n void player.play();\n }, [shouldAutoplay, url, player, ctx.screen.id, layer.id, pauseAtStart]);\n\n useEffect(() => {\n if (shouldAutoplay || playSignal === 0 || !url) return;\n play();\n }, [playSignal, shouldAutoplay, play, url]);\n\n useEffect(() => {\n if (shouldAutoplay || !url) return;\n const sub = player.addListener('playingChange', ({ isPlaying }) => {\n if (isPlaying && !manualPlayRef.current) {\n player.pause();\n try {\n player.currentTime = 0;\n } catch {\n // metadata not ready\n }\n }\n if (!isPlaying) manualPlayRef.current = false;\n });\n return () => sub.remove();\n }, [shouldAutoplay, url, player]);\n\n useEffect(() => {\n if (!ctx.interactive || loopPlay) return;\n const sub = player.addListener('playToEnd', () => {\n if (completedRef.current) return;\n completedRef.current = true;\n fireMediaOnComplete(ctx, layer);\n });\n return () => sub.remove();\n }, [ctx, layer, loopPlay, player]);\n\n const { outerStyle, linearGradient } = mediaLayerOuterLayoutPair(\n resolvedStyle,\n ctx.manifest.theme,\n ctx.theme,\n ctx.branding,\n );\n const placeholderBg = ctx.theme === 'dark' ? '#18181b' : '#f4f4f5';\n const hasAuthorBg =\n linearGradient != null || outerStyle.backgroundColor !== undefined;\n const innerStyle = {\n ...mediaLayerInnerFillStyle(resolvedStyle),\n borderRadius: outerStyle.borderRadius ?? 10,\n ...(!hasAuthorBg && !url ? { backgroundColor: placeholderBg } : {}),\n };\n const fit = resolvedStyle?.fit ?? 'contain';\n const contentFit =\n fit === 'cover' ? 'cover' : fit === 'fill' ? 'fill' : 'contain';\n const r = innerStyle.borderRadius as number | undefined;\n\n return (\n <ChromeView style={outerStyle} linearGradient={linearGradient}>\n {url ? (\n <View style={innerStyle}>\n <VideoView\n player={player}\n contentFit={contentFit}\n nativeControls={false}\n style={{\n width: '100%',\n height: '100%',\n ...(r !== undefined ? { borderRadius: r } : {}),\n }}\n />\n </View>\n ) : (\n <View style={[innerStyle, { alignItems: 'center', justifyContent: 'center' }]}>\n <Text style={{ color: '#71717a', fontSize: 11 }}>No media</Text>\n </View>\n )}\n </ChromeView>\n );\n};\n\nexport const ExpoScreenShellVideoBackdrop = ({\n screenId,\n url,\n fill,\n ctx,\n}: ScreenShellVideoBackdropProps) => {\n const playbackId = screenBackgroundPlaybackId(screenId);\n const playback = useMediaPlayback();\n const playSignal = useMediaPlaySignal(playbackId);\n const completedRef = useRef(false);\n const manualPlayRef = useRef(false);\n const shouldAutoplay = mediaAutoPlayOnMount(fill);\n const loopPlay = fill.loop !== false;\n const muted = fill.audioEnabled !== true;\n\n const player = useVideoPlayer(url, (p) => {\n p.loop = loopPlay;\n p.muted = muted;\n if (!shouldAutoplay) {\n p.pause();\n try {\n p.currentTime = 0;\n } catch {\n // not ready\n }\n }\n });\n\n const pauseAtStart = useCallback((): void => {\n try {\n player.pause();\n player.currentTime = 0;\n } catch {\n // not ready\n }\n }, [player]);\n\n const play = useCallback((): void => {\n if (player.playing) return;\n completedRef.current = false;\n manualPlayRef.current = true;\n try {\n player.currentTime = 0;\n } catch {\n // not ready\n }\n void player.play();\n }, [player]);\n\n useEffect(() => {\n if (!playback) return;\n return playback.register(playbackId, { play });\n }, [playback, playbackId, play]);\n\n useEffect(() => {\n player.loop = loopPlay;\n player.muted = muted;\n }, [loopPlay, muted, player]);\n\n useEffect(() => {\n if (!shouldAutoplay) {\n manualPlayRef.current = false;\n pauseAtStart();\n return;\n }\n void player.play();\n }, [shouldAutoplay, url, player, pauseAtStart]);\n\n useEffect(() => {\n if (shouldAutoplay || playSignal === 0 || !url) return;\n play();\n }, [playSignal, shouldAutoplay, play, url]);\n\n useEffect(() => {\n if (!ctx.interactive || loopPlay) return;\n const sub = player.addListener('playToEnd', () => {\n if (completedRef.current) return;\n completedRef.current = true;\n const mode = fill.onComplete?.mode ?? 'none';\n if (mode === 'next') ctx.onRespond?.({ kind: 'cta', action: 'primary' });\n });\n return () => sub.remove();\n }, [ctx, fill.onComplete, loopPlay, player]);\n\n return (\n <View style={backdropLayout} pointerEvents=\"none\" collapsable={false}>\n <VideoView\n player={player}\n pointerEvents=\"none\"\n {...(Platform.OS === 'android' ? { surfaceType: 'textureView' as const } : {})}\n style={{\n width: '100%',\n height: '100%',\n opacity: fill.opacity ?? 1,\n }}\n contentFit={videoContentFitFor(fill.fit)}\n nativeControls={false}\n />\n </View>\n );\n};\n","import { registerAppReviewAdapter, registerVideoAdapter } from '@getrheo/react-native-core/platform';\nimport { expoAppReviewAdapter } from './adapters/expoAppReview.js';\nimport { ExpoScreenShellVideoBackdrop, ExpoVideoLayerView } from './adapters/expoVideo.js';\n\nregisterAppReviewAdapter(expoAppReviewAdapter);\nregisterVideoAdapter({\n VideoLayerView: ExpoVideoLayerView,\n ScreenShellVideoBackdrop: ExpoScreenShellVideoBackdrop,\n});\n"]}
@@ -0,0 +1,6 @@
1
+ [
2
+ {
3
+ "exportKey": ".",
4
+ "distFile": "./dist/index.js"
5
+ }
6
+ ]
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@getrheo/react-native-expo",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "@getrheo/react-native-core": "1.0.0"
16
+ },
17
+ "peerDependencies": {
18
+ "@react-native-async-storage/async-storage": ">=2",
19
+ "expo-store-review": "*",
20
+ "expo-video": "*",
21
+ "lottie-react-native": "*",
22
+ "react": ">=18",
23
+ "react-native": ">=0.79",
24
+ "react-native-gesture-handler": "*",
25
+ "react-native-linear-gradient": "*",
26
+ "react-native-permissions": ">=5",
27
+ "react-native-reanimated": "*",
28
+ "react-native-safe-area-context": "*",
29
+ "react-native-svg": "*",
30
+ "react-native-vector-icons": ">=10"
31
+ },
32
+ "devDependencies": {
33
+ "@react-native-async-storage/async-storage": "2.2.0",
34
+ "@types/react": "^19.0.1",
35
+ "expo-store-review": "~9.0.0",
36
+ "expo-video": "~3.0.0",
37
+ "lottie-react-native": "^7.2.2",
38
+ "react": "^19.0.0",
39
+ "react-native": "0.83.6",
40
+ "react-native-gesture-handler": "~2.30.1",
41
+ "react-native-linear-gradient": "^2.8.3",
42
+ "react-native-permissions": "^5.5.1",
43
+ "react-native-reanimated": "~4.2.1",
44
+ "react-native-safe-area-context": "~5.6.1",
45
+ "react-native-svg": "15.15.4",
46
+ "react-native-vector-icons": "^10.2.0",
47
+ "typescript": "^5.6.3",
48
+ "vitest": "^3.2.6",
49
+ "@getrheo/contracts": "1.0.0",
50
+ "@getrheo/react-native-core": "1.0.0",
51
+ "@getrheo/flow-runtime": "1.0.0",
52
+ "@rheo/config": "0.1.0"
53
+ },
54
+ "license": "MIT",
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "git+https://github.com/madeinusmate/onboardly.git",
58
+ "directory": "packages/sdks/react-native-expo"
59
+ },
60
+ "files": [
61
+ "dist",
62
+ "README.md"
63
+ ],
64
+ "publishConfig": {
65
+ "access": "public"
66
+ },
67
+ "scripts": {
68
+ "lint": "eslint .",
69
+ "typecheck": "tsc --noEmit",
70
+ "test": "vitest run --passWithNoTests --config ../react-native-core/vitest.config.ts",
71
+ "build": "node ../../../scripts/build-publishable-package.mjs"
72
+ }
73
+ }