@getrheo/rheo-skill 2.3.0 → 2.5.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 +1 -1
- package/package.json +3 -3
- package/rheo/SKILL.md +1 -1
- package/rheo/rheo-best-practices/SKILL.md +2 -1
- package/rheo/rheo-best-practices/references/integrations.md +40 -2
- package/rheo/rheo-best-practices/references/react-native-bare.md +8 -2
- package/rheo/rheo-best-practices/references/react-native-expo.md +1 -1
- package/rheo/rheo-best-practices/references/swiftui.md +2 -1
- package/rheo/rheo-best-practices/references/troubleshooting.md +1 -1
- package/rheo/rheo-flow-import/SKILL.md +1 -1
- package/rheo/rheo-flow-import/references/capabilities.md +16 -4
- package/rheo/rheo-flow-import/references/carousel-import.md +64 -7
- package/rheo/rheo-flow-import/references/flow-spec.md +20 -5
- package/rheo/rheo-flow-import/references/import-workflow.md +2 -2
- package/rheo/rheo-flow-import/references/manifest-agent-profile-fallback.md +4 -4
- package/rheo/rheo-flow-import/references/manifest-rules.md +26 -7
- package/rheo/rheo-flow-import/references/publish-gates.md +6 -2
- package/rheo/rheo-flow-import/references/react-native-source-patterns.md +7 -5
- package/rheo/rheo-flow-import/references/swiftui-source-patterns.md +6 -4
- package/rheo/rheo-flow-import/scripts/lib/rheo-cli.mjs +679 -286
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @getrheo/rheo-skill
|
|
2
2
|
|
|
3
|
-
Current release: **`2.
|
|
3
|
+
Current release: **`2.5.0`** on npm (`PLATFORM_SDK_VERSION` in `scripts/publish-package-registry.mjs`). Public source: [getrheo/rheo-skill](https://github.com/getrheo/rheo-skill) (mirrored from this private monorepo via `pnpm extract:oss-repos`).
|
|
4
4
|
|
|
5
5
|
Source and build tooling for the **`rheo`** agent skill — a single, self-contained
|
|
6
6
|
skill with two sub-skills:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getrheo/rheo-skill",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Rheo agent skill — SDK install guidance and flow manifest import tooling.",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
".": "./src/index.ts"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@getrheo/contracts": "2.
|
|
13
|
-
"@getrheo/flow-runtime": "2.
|
|
12
|
+
"@getrheo/contracts": "2.5.0",
|
|
13
|
+
"@getrheo/flow-runtime": "2.5.0",
|
|
14
14
|
"zod": "^3.23.8"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
package/rheo/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: rheo
|
|
|
3
3
|
description: Work with Rheo, the headless onboarding/paywall flow engine for mobile apps. Use when a user wants to install or wire the Rheo SDK (React Native, Expo, or SwiftUI), follow Rheo SDK best practices, configure integrations (RevenueCat, AppsFlyer), wire auth/permissions/terminal callbacks, OR import/migrate an existing mobile flow into a compliant Rheo FlowManifest and validate it. Routes to the `rheo-best-practices` and `rheo-flow-import` sub-skills.
|
|
4
4
|
compatibility: Requires Node.js 20+. rheo-flow-import scripts are fully self-contained (no install step). Internet access fetches the latest Manifest Agent Profile; a bundled fallback works offline.
|
|
5
5
|
metadata:
|
|
6
|
-
rheo-version: "2.
|
|
6
|
+
rheo-version: "2.5.0"
|
|
7
7
|
manifest-schema-version: "7"
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -30,7 +30,8 @@ Only install packages or edit host code when the user explicitly asks for implem
|
|
|
30
30
|
- **Pass the channel public id**, not a flow id, to `Flow` / `FlowView`.
|
|
31
31
|
- **Preserve the existing onboarding** as a fallback/rollback path (feature flag or route swap) unless the user explicitly asks to remove it.
|
|
32
32
|
- **Read local conventions first** (package manager, navigation, env handling) and keep edits localized to the integration entry point and app config.
|
|
33
|
-
- **RevenueCat and AppsFlyer are host integrations**, not SDK peers — the host configures and owns those SDKs and their secrets. Wire `fallback` for every
|
|
33
|
+
- **RevenueCat and AppsFlyer are host integrations**, not SDK peers — the host configures and owns those SDKs and their secrets. Wire `fallback` for every Integration / External Surface Node.
|
|
34
|
+
- **External Surface Nodes** need a host `externalSurfaces` registry keyed by Host key (`config.hostKey` or `surf_*` id; callbacks `onComplete` / `onBack` / `onDismiss`). No App settings toggle.
|
|
34
35
|
- **Do not add `request_app_review`** prompts unless the user explicitly asks; Apple discourages prompting from raw button taps.
|
|
35
36
|
- Run the **narrowest useful verification** (typecheck or a build of the touched module), not a full app build, unless asked.
|
|
36
37
|
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
# Integrations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
In the flow builder, partner paywalls and host UI are **two add-menu nodes** that share the `externalSurfaceNodes` schema:
|
|
4
|
+
|
|
5
|
+
| Builder node | Manifest `config.provider` | App settings toggle |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| **Integration Node** | `revenuecat` (and future partners) | Required |
|
|
8
|
+
| **External Surface Node** | `headless` | None (host registry) |
|
|
9
|
+
|
|
10
|
+
## RevenueCat (Integration Node)
|
|
4
11
|
|
|
5
12
|
Detect source calls such as `Purchases.configure`, `react-native-purchases`, `react-native-purchases-ui`, or existing paywall presentation code.
|
|
6
13
|
|
|
7
14
|
Manifest mapping:
|
|
8
15
|
|
|
9
|
-
- Create an external surface with `config.provider: "revenuecat"`.
|
|
16
|
+
- Create an **Integration Node** / external surface with `config.provider: "revenuecat"`.
|
|
10
17
|
- Preserve offering or placement identifiers when visible in source.
|
|
11
18
|
- Wire known outcomes:
|
|
12
19
|
- `purchase_completed`
|
|
@@ -17,6 +24,37 @@ Manifest mapping:
|
|
|
17
24
|
|
|
18
25
|
The host remains responsible for configuring RevenueCat. Rheo does not own purchase SDK secrets or receipt validation.
|
|
19
26
|
|
|
27
|
+
## External Surface Node (headless host UI)
|
|
28
|
+
|
|
29
|
+
Use when a source screen is owned by the host (custom native UI, third-party screen Rheo does not ship) rather than a Rheo layer tree or RevenueCat paywall.
|
|
30
|
+
|
|
31
|
+
Manifest mapping:
|
|
32
|
+
|
|
33
|
+
- Create an **External Surface Node** with `config.provider: "headless"`.
|
|
34
|
+
- Keep a stable `surf_*` id for graph routing / analytics. Optionally set `config.hostKey` for the host `externalSurfaces` registry (defaults to the node id).
|
|
35
|
+
- Wire outcomes:
|
|
36
|
+
- `completed` ← host `onComplete`
|
|
37
|
+
- `back` ← host `onBack`
|
|
38
|
+
- `dismissed` ← host `onDismiss`
|
|
39
|
+
- `failed` ← missing host component (SDK)
|
|
40
|
+
- Always wire `fallback`.
|
|
41
|
+
|
|
42
|
+
Host wiring (required for success):
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// React Native — key matches Host key (hostKey or surf_* id)
|
|
46
|
+
<Flow
|
|
47
|
+
channelId="…"
|
|
48
|
+
externalSurfaces={{
|
|
49
|
+
onboardingQuiz: ({ onComplete, onBack, onDismiss }) => /* host UI */,
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
SwiftUI / Flutter: pass the same map on `FlowView` as `externalSurfaces` (builders receive `onComplete` / `onBack` / `onDismiss`). Docs: product Developer Guide → Headless external surfaces.
|
|
55
|
+
|
|
56
|
+
Do **not** use an External Surface Node to wrap RevenueCat when you need `iap_purchase` commerce events — use an Integration Node with `provider: "revenuecat"`.
|
|
57
|
+
|
|
20
58
|
## AppsFlyer
|
|
21
59
|
|
|
22
60
|
Detect `react-native-appsflyer` or Swift attribution setup. Do not include AppsFlyer secrets in manifests.
|
|
@@ -15,7 +15,7 @@ pnpm add @getrheo/react-native-bare \
|
|
|
15
15
|
react-native-safe-area-context react-native-in-app-review react-native-video
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
**Integrations (host only):** `react-native-appsflyer`, `react-native-purchases`, `react-native-purchases-ui` when needed.
|
|
18
|
+
**Integrations (host only):** `react-native-appsflyer`, `react-native-purchases`, `react-native-purchases-ui` when needed. External Surface Nodes use `Flow` `externalSurfaces` (no extra native peer).
|
|
19
19
|
|
|
20
20
|
## Minimal Runtime
|
|
21
21
|
|
|
@@ -24,7 +24,13 @@ import { Flow, RheoProvider } from '@getrheo/react-native-bare';
|
|
|
24
24
|
|
|
25
25
|
export const OnboardingHost = () => (
|
|
26
26
|
<RheoProvider config={{ publishableKey: '…', userId: '…' }}>
|
|
27
|
-
<Flow
|
|
27
|
+
<Flow
|
|
28
|
+
channelId="ch_…"
|
|
29
|
+
onFlowCompleted={() => {}}
|
|
30
|
+
externalSurfaces={{
|
|
31
|
+
// surf_*: ({ onComplete, onBack, onDismiss }) => host UI
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
28
34
|
</RheoProvider>
|
|
29
35
|
);
|
|
30
36
|
```
|
|
@@ -15,7 +15,7 @@ pnpm add @getrheo/react-native-expo \
|
|
|
15
15
|
react-native-safe-area-context expo-store-review expo-video
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
**Integrations (host only, not SDK peers):** `react-native-appsflyer`, `react-native-purchases`, `react-native-purchases-ui` when the flow uses attribution or RevenueCat
|
|
18
|
+
**Integrations (host only, not SDK peers):** `react-native-appsflyer`, `react-native-purchases`, `react-native-purchases-ui` when the flow uses attribution or RevenueCat Integration Nodes. External Surface Nodes use `Flow` / `useFlow` `externalSurfaces` (no extra native peer).
|
|
19
19
|
|
|
20
20
|
## Minimal Runtime
|
|
21
21
|
|
|
@@ -46,7 +46,8 @@ struct OnboardingHost: View {
|
|
|
46
46
|
|
|
47
47
|
## Notes
|
|
48
48
|
|
|
49
|
-
- Use `RheoSwiftUIRevenueCat` for RevenueCat
|
|
49
|
+
- Use `RheoSwiftUIRevenueCat` for RevenueCat Integration Node presenter helpers.
|
|
50
|
+
- External Surface Nodes: pass `externalSurfaces: [surfId: { ctx in AnyView(...) }]` on `FlowView` (`ctx.onComplete` / `onBack` / `onDismiss`).
|
|
50
51
|
- Use `RheoSwiftUIAppsFlyer` for AppsFlyer attribution providers.
|
|
51
52
|
- Host apps must include Info.plist usage strings for authored permission prompts.
|
|
52
53
|
- Host apps must register branding fonts if relying on downloaded font families.
|
|
@@ -38,7 +38,7 @@ Fix workflow:
|
|
|
38
38
|
|
|
39
39
|
## Missing Fallback Edge
|
|
40
40
|
|
|
41
|
-
Every
|
|
41
|
+
Every Integration / External Surface Node needs `fallback`. Outcomes that are not explicitly mapped (RevenueCat purchase outcomes or headless `completed` / `back` / `dismissed`) fall through to this target.
|
|
42
42
|
|
|
43
43
|
## Integration Disabled
|
|
44
44
|
|
|
@@ -57,7 +57,7 @@ All `<manifest.json>` / `<flow-spec.json>` paths and outputs resolve from the **
|
|
|
57
57
|
- **Choice inputs:** `single_choice`/`multiple_choice` require `fieldKey` (snake_case), `children` (≥2 option stacks), `optionBindings` (one per option, `rootLayerId` = child stack `lyr_*` id), and `branching` (`{ "enabled": false, "conditions": [] }` when no branches). Never `"options"`/`"choices"`.
|
|
58
58
|
- **Styling:** when the audit reports colors, populate `manifest.theme` and layer `style` (including `style.color` on every text layer and nested button label). No black-and-white defaults when color evidence exists.
|
|
59
59
|
- **Gradients:** map `LinearGradient`/gradient stops to `screen.containerStyle.backgroundFill.color` as a `linear-gradient(...)` CSS string.
|
|
60
|
-
- **Carousels:** pager/carousel evidence (`infoSteps`, horizontal pager, `pagingEnabled`) → `kind: "carousel"`, one slide per page
|
|
60
|
+
- **Carousels:** pager/carousel evidence (`infoSteps`, horizontal pager, `pagingEnabled`) → `kind: "carousel"`, one slide per page; swipe by default, or a `button` with `action.kind: "advance_carousel"` when the source has an explicit Next control. See [references/carousel-import.md](references/carousel-import.md).
|
|
61
61
|
- **Fonts:** custom fonts go in `rheo-import.fonts.json` under `assets/fonts/` and `manifest.theme.fontFamily`; **never** in `rheo-import.assets.json`. See [references/font-import.md](references/font-import.md).
|
|
62
62
|
- **Localization:** resolve **default-locale** strings into every `text.default` — never raw translation keys. Set `manifest.defaultLocale`. See [references/localization-import.md](references/localization-import.md).
|
|
63
63
|
- **Animations:** map motion from the audit only when intake Q6 is yes and the plan includes animations ([references/animation-import.md](references/animation-import.md)); otherwise omit all `animations`, `stagger`, `restingMotion`.
|
|
@@ -35,10 +35,17 @@ Every layer `kind` accepted by the manifest:
|
|
|
35
35
|
- `carousel`
|
|
36
36
|
- `hyperlink`
|
|
37
37
|
- `checkbox`
|
|
38
|
+
- `conditional`
|
|
38
39
|
|
|
39
40
|
Container layers that **must** include a `children` array (or `slides` for carousel): `stack`, `carousel`,
|
|
40
41
|
`button`, `back_button`, `hyperlink`, `single_choice`, `multiple_choice`, `oauth_login`,
|
|
41
|
-
`oauth_provider` (custom variant), `email_password_auth`, `email_password_field`, `email_password_submit
|
|
42
|
+
`oauth_provider` (custom variant), `email_password_auth`, `email_password_field`, `email_password_submit`,
|
|
43
|
+
`conditional`.
|
|
44
|
+
|
|
45
|
+
`conditional` picks which of its child stacks renders: ordered `cases[]` (each with a `DecisionExpr`
|
|
46
|
+
and a `rootLayerId`) plus a required `elseRootLayerId`. Every bound id must be a distinct direct child
|
|
47
|
+
`stack`. Cases may only read fields answered above the conditional, every case needs at least one rule
|
|
48
|
+
before publish, and the one-input limit applies per active path so sibling branches may each own an input.
|
|
42
49
|
|
|
43
50
|
## Button / back_button variants
|
|
44
51
|
|
|
@@ -60,11 +67,13 @@ Valid `action.kind` values on `button` layers:
|
|
|
60
67
|
- `request_os_permission`
|
|
61
68
|
- `play_media`
|
|
62
69
|
- `request_app_review`
|
|
70
|
+
- `advance_carousel`
|
|
63
71
|
|
|
64
72
|
- `FlowGraphNodeJumpTarget` (`scr_*` | `dec_*` | `surf_*`): `go_to_step.screenId`, choice `branching.conditions[].goTo`, loader/lottie/video `onComplete` when mode is `screen`, and `request_os_permission` outcomes (except `continue`/`end`).
|
|
65
73
|
- `go_back_one_screen` and `back_button` accept optional `fallbackScreenId` (`scr_*` only).
|
|
66
74
|
- `request_os_permission` requires `permissionKey` and `outcomes` (`granted`/`denied`/`blocked`).
|
|
67
75
|
- `play_media` requires `targetLayerIds` (≥1) pointing at Lottie/video layers on the same screen.
|
|
76
|
+
- `advance_carousel` requires `targetLayerId` (exactly one `carousel` layer on the same screen) and takes optional `onLast` (`noop` default, or `complete` to finish the carousel when already on the last slide).
|
|
68
77
|
- `back_button` takes **no** `action` (back navigation is built in).
|
|
69
78
|
|
|
70
79
|
## OS permission keys
|
|
@@ -102,7 +111,10 @@ Valid `permissionKey` values for `request_os_permission`:
|
|
|
102
111
|
- `email_password_auth` modes: `sign_in`, `sign_up` (sign_up requires email + password + confirm fields).
|
|
103
112
|
- `icon` families: `ionicons`.
|
|
104
113
|
|
|
105
|
-
## External surface outcomes
|
|
114
|
+
## External surface outcomes
|
|
115
|
+
|
|
116
|
+
Builder add-menu kinds share `externalSurfaceNodes`: **Integration Node** (partner providers such as RevenueCat) and **External Surface Node** (`provider: "headless"`). Every node needs a `fallback` jump target.
|
|
117
|
+
|
|
118
|
+
**Integration Node / RevenueCat** (`provider: "revenuecat"`): `purchase_completed`, `purchase_cancelled`, `dismissed`, `failed`, `restore_completed`.
|
|
106
119
|
|
|
107
|
-
|
|
108
|
-
Every external surface also needs a `fallback` jump target.
|
|
120
|
+
**External Surface Node / Headless** (`provider: "headless"`): `completed`, `back`, `dismissed`, `failed`. Host apps register UI via `externalSurfaces[hostKey]` (`config.hostKey` or node id) with `onComplete` / `onBack` / `onDismiss`.
|
|
@@ -2,15 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
Use when the source flow has an **in-screen pager** (`infoSteps`, `currentInfoStep`, horizontal `FlatList` / `PagerView`, dot indicators).
|
|
4
4
|
|
|
5
|
-
## Rheo carousel behavior (swipe
|
|
5
|
+
## Rheo carousel behavior (swipe by default)
|
|
6
6
|
|
|
7
|
-
- Users move between slides by **swiping** (horizontal scroll with snap).
|
|
7
|
+
- Users move between slides by **swiping** (horizontal scroll with snap). Swipe is always available.
|
|
8
8
|
- Optional **`pageControl`** adds dot indicators — not buttons.
|
|
9
|
-
-
|
|
10
|
-
- On the **last slide** (when `loop` is false and there are 2+ slides), swiping to that slide emits a carousel completion so `screen.next` can run.
|
|
9
|
+
- On the **last slide** (when `loop` is false and there are 2+ slides), arriving at that slide emits a carousel completion so `screen.next` can run.
|
|
11
10
|
- **Single-slide** carousels do not auto-complete; add a `regions.footer` **Continue** (or other) button to advance the flow.
|
|
12
11
|
- **`loop: true`** carousels never auto-complete; pair with a separate screen-level CTA when the flow should move on.
|
|
13
12
|
|
|
13
|
+
## Optional Next button (`advance_carousel`)
|
|
14
|
+
|
|
15
|
+
A button may page the carousel when the source screen genuinely has a Next control:
|
|
16
|
+
|
|
17
|
+
- Action shape: `{ "kind": "advance_carousel", "targetLayerId": "<carousel layer id>", "onLast": "noop" | "complete" }`.
|
|
18
|
+
- `targetLayerId` must name **exactly one** `carousel` layer on the **same screen**.
|
|
19
|
+
- `onLast` is optional and defaults to `"noop"` (stay on the last slide). Use `"complete"` when tapping Next on the last slide should finish the carousel and follow `screen.next`.
|
|
20
|
+
- `loop: true` and single-slide carousels never complete through this action — they wrap or do nothing.
|
|
21
|
+
- Advancing from second-to-last to last emits carousel completion just like a swipe, so a `"complete"` `onLast` only matters when the user is **already** on the last slide.
|
|
22
|
+
- Runtime support: **Web and React Native**. Flutter and SwiftUI decode the action but do nothing on tap, so do not rely on it for flow exit on those platforms — use a `continue` button instead.
|
|
23
|
+
|
|
14
24
|
## Manifest shape
|
|
15
25
|
|
|
16
26
|
- One `kind: "carousel"` in `regions.body` (or inside a body stack).
|
|
@@ -24,8 +34,9 @@ Use when the source flow has an **in-screen pager** (`infoSteps`, `currentInfoSt
|
|
|
24
34
|
| Pager pages | `carousel.slides[]` |
|
|
25
35
|
| Dot indicators | `pageControl` only |
|
|
26
36
|
| Swipe between pages | Default — no extra buttons |
|
|
27
|
-
| Footer that only increments pager index | **Omit** —
|
|
28
|
-
|
|
|
37
|
+
| Footer that only increments pager index, with no visible Next affordance | **Omit** — swipe already covers it |
|
|
38
|
+
| Explicit **Next** button in the source pager | `button` with `advance_carousel` targeting the carousel |
|
|
39
|
+
| Footer / CTA that exits to **next route** | `regions.footer` with `continue` / `go_to_step`, or rely on last-slide completion + `screen.next` |
|
|
29
40
|
|
|
30
41
|
## Example (swipe + dots)
|
|
31
42
|
|
|
@@ -57,8 +68,54 @@ Use when the source flow has an **in-screen pager** (`infoSteps`, `currentInfoSt
|
|
|
57
68
|
}
|
|
58
69
|
```
|
|
59
70
|
|
|
71
|
+
## Example (explicit Next button)
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"regions": {
|
|
76
|
+
"body": {
|
|
77
|
+
"kind": "stack",
|
|
78
|
+
"direction": "vertical",
|
|
79
|
+
"children": [
|
|
80
|
+
{
|
|
81
|
+
"id": "lyr_car_intro",
|
|
82
|
+
"kind": "carousel",
|
|
83
|
+
"slides": [
|
|
84
|
+
{
|
|
85
|
+
"kind": "stack",
|
|
86
|
+
"direction": "vertical",
|
|
87
|
+
"children": [
|
|
88
|
+
{ "kind": "text", "text": { "default": "Slide 1" }, "style": { "color": "#111111" } }
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"kind": "stack",
|
|
93
|
+
"direction": "vertical",
|
|
94
|
+
"children": [
|
|
95
|
+
{ "kind": "text", "text": { "default": "Slide 2" }, "style": { "color": "#111111" } }
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"pageControl": { "position": "bottom" }
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
"footer": {
|
|
104
|
+
"kind": "button",
|
|
105
|
+
"variant": "primary",
|
|
106
|
+
"action": { "kind": "advance_carousel", "targetLayerId": "lyr_car_intro", "onLast": "complete" },
|
|
107
|
+
"children": [
|
|
108
|
+
{ "kind": "text", "text": { "default": "Next" }, "style": { "color": "#FFFFFF" } }
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"next": { "default": "scr_next" }
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
60
116
|
## Completion gate
|
|
61
117
|
|
|
62
|
-
- [ ] No `button` layers
|
|
118
|
+
- [ ] No `button` layers that only mimic paging without an `advance_carousel` action.
|
|
119
|
+
- [ ] Every `advance_carousel` button targets a `carousel` on the same screen.
|
|
63
120
|
- [ ] `screen.next` targets the next **screen** in the flow, not “next slide”.
|
|
64
121
|
- [ ] Single-slide carousel screens include a flow-level CTA if the flow must advance without swiping.
|
|
@@ -91,7 +91,10 @@ Action shorthands: `"none"`, `"continue"`, `"skip"`, `"end_flow"`,
|
|
|
91
91
|
`"go_back_one_screen"`, `"request_app_review"`. Object forms:
|
|
92
92
|
`{ "kind": "go_to_step", "screenId": "scr_x" }` (or `dec_*` / `surf_*`),
|
|
93
93
|
`{ "kind": "request_os_permission", "permissionKey": "…", "outcomes": { "granted": "scr_a", "denied": "dec_b", "blocked": "surf_c" } }` (targets may be `scr_*`, `dec_*`, `surf_*`, `"continue"`, or `"end"`),
|
|
94
|
-
`{ "kind": "play_media", "targetLayerIds": ["lyr_video"] }
|
|
94
|
+
`{ "kind": "play_media", "targetLayerIds": ["lyr_video"] }`,
|
|
95
|
+
`{ "kind": "advance_carousel", "targetLayerId": "lyr_carousel", "onLast": "noop" }`
|
|
96
|
+
(`onLast` is optional and defaults to `"noop"`; use `"complete"` to finish the carousel when the
|
|
97
|
+
button is tapped on the last slide).
|
|
95
98
|
|
|
96
99
|
### Stacks (layout)
|
|
97
100
|
|
|
@@ -161,7 +164,7 @@ screens with `text_input`, `multiple_choice`, `scale_input`, or `wheel_picker`.
|
|
|
161
164
|
|
|
162
165
|
Keep OAuth, email/password, and questionnaire inputs on **separate screens**.
|
|
163
166
|
|
|
164
|
-
### Carousel (
|
|
167
|
+
### Carousel (onboarding pager)
|
|
165
168
|
|
|
166
169
|
```jsonc
|
|
167
170
|
{
|
|
@@ -180,7 +183,9 @@ Keep OAuth, email/password, and questionnaire inputs on **separate screens**.
|
|
|
180
183
|
}
|
|
181
184
|
```
|
|
182
185
|
|
|
183
|
-
|
|
186
|
+
Paging is swipe by default. When the source screen has an explicit Next control, add a
|
|
187
|
+
`button` with `action: { "kind": "advance_carousel", "targetLayerId": "<carousel id>" }`
|
|
188
|
+
instead of a plain footer CTA ([carousel-import.md](carousel-import.md)).
|
|
184
189
|
|
|
185
190
|
### Counter / progress / loader
|
|
186
191
|
|
|
@@ -194,7 +199,8 @@ No buttons inside a carousel — paging is swipe-only ([carousel-import.md](caro
|
|
|
194
199
|
## Decisions and external surfaces
|
|
195
200
|
|
|
196
201
|
`decisions` accepts full `@getrheo/contracts` `DecisionNode` objects (the scaffold
|
|
197
|
-
passes them through).
|
|
202
|
+
passes them through). External surfaces use `externalSurfaces` (builder:
|
|
203
|
+
**Integration Node** for `revenuecat`, **External Surface Node** for `headless`):
|
|
198
204
|
|
|
199
205
|
```jsonc
|
|
200
206
|
{
|
|
@@ -203,7 +209,16 @@ passes them through). RevenueCat paywalls use `externalSurfaces`:
|
|
|
203
209
|
"offeringId": "default",
|
|
204
210
|
"presentation": "paywall",
|
|
205
211
|
"outcomes": { "purchase_completed": "scr_done", "restore_completed": "scr_done", "dismissed": "scr_offer2" },
|
|
206
|
-
"fallback": "scr_offer2" // required
|
|
212
|
+
"fallback": "scr_offer2" // required — Integration Node
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```jsonc
|
|
217
|
+
{
|
|
218
|
+
"id": "surf_custom_step",
|
|
219
|
+
"provider": "headless",
|
|
220
|
+
"outcomes": { "completed": "scr_done", "back": "scr_welcome", "dismissed": "scr_offer2" },
|
|
221
|
+
"fallback": "scr_offer2" // required — External Surface Node; host registers externalSurfaces.surf_custom_step
|
|
207
222
|
}
|
|
208
223
|
```
|
|
209
224
|
|
|
@@ -63,8 +63,8 @@ If the user has not named an entry point, stop after question 1 and wait for an
|
|
|
63
63
|
- When source uses `infoSteps`, `currentInfoStep`, horizontal `translateX` pagers, or `pagingEnabled` lists, emit `kind: "carousel"`.
|
|
64
64
|
- Each slide is a vertical `stack` with image, title, and body text from that slide.
|
|
65
65
|
- Add `pageControl: { "position": "bottom" }` when dot indicators exist (dots only).
|
|
66
|
-
- Carousels
|
|
67
|
-
- Use `regions.footer`
|
|
66
|
+
- Carousels page by swipe. When the source has an explicit Next control, keep it as a `button` with `action.kind: "advance_carousel"` targeting that carousel; never add a plain footer/body button that only bumps the pager index.
|
|
67
|
+
- Use `regions.footer` when the source CTA advances the **next screen in the flow**, or for single-slide carousels that need an explicit Continue.
|
|
68
68
|
- Bundle every slide image; every asset referenced in the carousel must appear in `rheo-import.assets.json`.
|
|
69
69
|
- Do not collapse multi-slide routes into one static screen with a single image.
|
|
70
70
|
11. Map layout, alignment, borders, and shadows:
|
|
@@ -43,7 +43,7 @@ Before zipping, read [layer-schema-pitfalls.md](layer-schema-pitfalls.md) and ru
|
|
|
43
43
|
|
|
44
44
|
## Layer Kinds
|
|
45
45
|
|
|
46
|
-
Use only: `stack`, `text`, `image`, `lottie`, `video`, `icon`, `button`, `back_button`, `progress`, `loader`, `counter`, `single_choice`, `multiple_choice`, `text_input`, `scale_input`, `wheel_picker`, `oauth_provider`, `oauth_login`, `email_password_auth`, `email_password_field`, `email_password_submit`, `carousel`, `hyperlink`, `checkbox`.
|
|
46
|
+
Use only: `stack`, `text`, `image`, `lottie`, `video`, `icon`, `button`, `back_button`, `progress`, `loader`, `counter`, `single_choice`, `multiple_choice`, `text_input`, `scale_input`, `wheel_picker`, `oauth_provider`, `oauth_login`, `email_password_auth`, `email_password_field`, `email_password_submit`, `carousel`, `hyperlink`, `checkbox`, `conditional`.
|
|
47
47
|
|
|
48
48
|
## Rules
|
|
49
49
|
|
|
@@ -55,7 +55,7 @@ Use only: `stack`, `text`, `image`, `lottie`, `video`, `icon`, `button`, `back_b
|
|
|
55
55
|
- Inspect theme/style/token files, StyleSheet, and Tailwind classes before using black-and-white defaults.
|
|
56
56
|
- Set `style.color` on text for dark/saturated screen backgrounds.
|
|
57
57
|
- Gradients: `screen.containerStyle.backgroundFill.color` as `linear-gradient(...)` CSS when `kind` is `color`.
|
|
58
|
-
- In-screen pagers → `kind: "carousel"` with one slide per page; swipe
|
|
58
|
+
- In-screen pagers → `kind: "carousel"` with one slide per page; swipe by default, or an `advance_carousel` button when the source has an explicit Next control; bundle every slide asset. See carousel-import.md in references.
|
|
59
59
|
- Center images with parent stack `align: "center"`; map card borders/shadows to wrapping stacks.
|
|
60
60
|
- Custom fonts: bundle files in `rheo-import.fonts.json` only (never `rheo-import.assets.json`), `manifest.theme.fontFamily`. See `font-import.md`.
|
|
61
61
|
- Choice options: each option stack bakes default chrome into `style` and selected overrides into `selectedStyle` (optional `selectedStyleBreakpoints`).
|
|
@@ -65,9 +65,9 @@ Use only: `stack`, `text`, `image`, `lottie`, `video`, `icon`, `button`, `back_b
|
|
|
65
65
|
- Icons default to 24×24 unless source specifies another size.
|
|
66
66
|
- Publish gates: explicit `style.color` on all text (including button labels), Continue on manual-submit screens, valid entry/completion path. Run `scripts/audit-publish-manifest.mjs` before finishing.
|
|
67
67
|
- Black-and-white fallback is acceptable only when the audit finds no style/token evidence and the user confirms no theme source.
|
|
68
|
-
- Use at most one input layer kind per
|
|
68
|
+
- Use at most one input layer kind per **active path** (`single_choice`, `multiple_choice`, `text_input`, `scale_input`, `wheel_picker`) — one per screen unless sibling `conditional` branches split the path.
|
|
69
69
|
- Non-reserved `sdk.*` decision keys must be listed in `sdkAttributeKeys`.
|
|
70
|
-
- RevenueCat
|
|
70
|
+
- RevenueCat **Integration Nodes** and headless **External Surface Nodes** both live in `externalSurfaceNodes` and always need `fallback`.
|
|
71
71
|
- Emit complete graph edges for imported flows.
|
|
72
72
|
- Use placeholder UUID media ids and `rheo-import.assets.json`; never put file paths directly in `mediaAssetId`.
|
|
73
73
|
- Do not silently drop media layers. If a traced asset cannot be copied, report the missing file and do not call the import complete.
|
|
@@ -23,7 +23,7 @@ See [layer-schema-pitfalls.md](layer-schema-pitfalls.md) for common id mistakes
|
|
|
23
23
|
|
|
24
24
|
## Layer Kinds
|
|
25
25
|
|
|
26
|
-
Allowed kinds: `stack`, `text`, `image`, `lottie`, `video`, `icon`, `button`, `back_button`, `progress`, `loader`, `counter`, `single_choice`, `multiple_choice`, `text_input`, `scale_input`, `wheel_picker`, `oauth_provider`, `oauth_login`, `email_password_auth`, `email_password_field`, `email_password_submit`, `carousel`, `hyperlink`, `checkbox`.
|
|
26
|
+
Allowed kinds: `stack`, `text`, `image`, `lottie`, `video`, `icon`, `button`, `back_button`, `progress`, `loader`, `counter`, `single_choice`, `multiple_choice`, `text_input`, `scale_input`, `wheel_picker`, `oauth_provider`, `oauth_login`, `email_password_auth`, `email_password_field`, `email_password_submit`, `carousel`, `hyperlink`, `checkbox`, `conditional`.
|
|
27
27
|
|
|
28
28
|
## Regions
|
|
29
29
|
|
|
@@ -56,8 +56,9 @@ Map clear brand values into `manifest.theme` and layer styles. Set `style.color`
|
|
|
56
56
|
|
|
57
57
|
- In-screen pagers (`infoSteps`, horizontal pager, dot indicators) → `kind: "carousel"` with one slide stack per page.
|
|
58
58
|
- Do not collapse multi-slide routes to one static screen.
|
|
59
|
-
- `pageControl` is optional dot chrome only
|
|
60
|
-
-
|
|
59
|
+
- `pageControl` is optional dot chrome only; the carousel layer has no built-in paging buttons.
|
|
60
|
+
- An explicit source Next control becomes a `button` with `action: { "kind": "advance_carousel", "targetLayerId": "<carousel id>" }` on the same screen.
|
|
61
|
+
- Do not duplicate paging with a `regions.footer` Continue when the source footer only increments pager index. See [carousel-import.md](carousel-import.md).
|
|
61
62
|
|
|
62
63
|
## Layout
|
|
63
64
|
|
|
@@ -143,6 +144,7 @@ When source uses a chevron-only back control, still nest an `icon` child (and op
|
|
|
143
144
|
- **`back_button`** has no `action` field; navigation is built-in. Use in `regions.header` for back/close chrome.
|
|
144
145
|
- Prefer `continue`, `skip`, and `end_flow` actions for imported first drafts (on `button` only).
|
|
145
146
|
- **`request_app_review`** is allowed for human/explicit requests only (not default imports): empty action object, requires `screen.next.default`, single CTA after a positive moment.
|
|
147
|
+
- **`advance_carousel`** is for a source pager's own Next control: `targetLayerId` must be a `carousel` on the same screen, and optional `onLast` is `noop` (default) or `complete`.
|
|
146
148
|
|
|
147
149
|
## Custom Fonts
|
|
148
150
|
|
|
@@ -167,7 +169,7 @@ Full template: [layer-schema-pitfalls.md](layer-schema-pitfalls.md#single_choice
|
|
|
167
169
|
|
|
168
170
|
## Inputs
|
|
169
171
|
|
|
170
|
-
- Use at most one input layer kind per
|
|
172
|
+
- Use at most one input layer kind per **active path** (`single_choice`, `multiple_choice`, `text_input`, `scale_input`, `wheel_picker`) — one per screen unless sibling `conditional` branches split the path.
|
|
171
173
|
- Use stable snake_case `fieldKey` values.
|
|
172
174
|
- Mark text input classification as `safe` or `sensitive`.
|
|
173
175
|
- `wheel_picker` captures a string (options or date part); no layer branching — use `dec_*` with string predicates.
|
|
@@ -178,11 +180,28 @@ Full template: [layer-schema-pitfalls.md](layer-schema-pitfalls.md#single_choice
|
|
|
178
180
|
- Non-reserved `sdk.*` keys used in decisions must be listed in `sdkAttributeKeys`.
|
|
179
181
|
- Prefer source semantics over visual guessing.
|
|
180
182
|
|
|
183
|
+
## Conditionals (same-screen variants)
|
|
184
|
+
|
|
185
|
+
Use `conditional` when the source renders different content on **one** screen based on locale, platform, an SDK attribute, or an answer already captured. Use `dec_*` when the source picks a different **screen**.
|
|
186
|
+
|
|
187
|
+
- Ordered `cases[]` (1–16), each with a `DecisionExpr` `expression` and a `rootLayerId`; first match wins.
|
|
188
|
+
- Required `elseRootLayerId` for the fallback. Every bound id is a distinct direct child `stack`.
|
|
189
|
+
- Cases read only fields answered **above** the conditional — upstream screens, or same-screen inputs earlier in tree order. Never a field captured inside its own branches.
|
|
190
|
+
- One input / `oauth_login` / `email_password_auth` per active path; sibling branches each get their own.
|
|
191
|
+
- `fieldKey` values stay unique across the whole screen, including across branches.
|
|
192
|
+
- Every case needs at least one rule before publish.
|
|
193
|
+
- No `style` or layout of its own; the winning branch renders in place.
|
|
194
|
+
- Do not use a conditional to reproduce a source screen that was genuinely a separate route.
|
|
195
|
+
|
|
181
196
|
## External Surfaces
|
|
182
197
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
-
|
|
198
|
+
In the builder UI these are two add-menu kinds sharing `externalSurfaceNodes`:
|
|
199
|
+
|
|
200
|
+
- **Integration Node** — RevenueCat paywalls become nodes with provider `revenuecat`.
|
|
201
|
+
- **External Surface Node** — Host-owned custom screens become nodes with provider `headless` (stable `surf_*` id; optional `config.hostKey` for the host `externalSurfaces` registry).
|
|
202
|
+
- Every node needs a `fallback`.
|
|
203
|
+
- Map paywall outcomes to `purchase_completed`, `restore_completed`, `dismissed`, and `failed`.
|
|
204
|
+
- Map headless outcomes to `completed`, `back`, `dismissed`, and `failed`.
|
|
186
205
|
|
|
187
206
|
## Assets
|
|
188
207
|
|
|
@@ -27,10 +27,12 @@ These mirror `apps/web/src/features/builder/validateFlow.ts` and API `preflightP
|
|
|
27
27
|
| **Container `children`** | `back_button`, `button`, or `hyperlink` emitted without a `children` array (or with label text on the parent instead of nested `text` children). Crashes import on Indie plans during motion strip; fails Zod validation. |
|
|
28
28
|
| **Text/icon `style.color`** | Body text or **button label** (nested text child) left without `style.color` — native does not inherit CSS colors. |
|
|
29
29
|
| **Continue button** | `text_input`, `multiple_choice`, `scale_input`, or `wheel_picker` without a `button` with `action.kind: "continue"`. |
|
|
30
|
-
| **One input per
|
|
30
|
+
| **One input per active path** | Multiple inputs on the same path, or OAuth/email-password combined with inputs. Sibling `conditional` branches each get their own budget. |
|
|
31
|
+
| **Conditional bindings** | `cases[].rootLayerId` / `elseRootLayerId` not pointing at distinct direct child stacks, or a case reading a field captured inside its own branch. |
|
|
31
32
|
| **fieldKey** | Missing or non–snake_case on input layers. |
|
|
32
33
|
| **Graph targets** | `go_to_step`, choice `goTo`, loader/lottie/video `onComplete` (screen mode), permission outcomes, and fallbacks point at missing `scr_*` / `dec_*` / `surf_*` ids. |
|
|
33
34
|
| **Media triggers** | Lottie/video with `autoPlay: false` needs a `play_media` button targeting that layer. |
|
|
35
|
+
| **Carousel targets** | `advance_carousel` must point `targetLayerId` at a `carousel` layer on the same screen. |
|
|
34
36
|
| **Screen backgrounds** | Image/video fills need `mediaAssetId`; manual background video needs trigger wiring. |
|
|
35
37
|
|
|
36
38
|
### Publishable graph (`validatePublishable`)
|
|
@@ -39,12 +41,14 @@ These mirror `apps/web/src/features/builder/validateFlow.ts` and API `preflightP
|
|
|
39
41
|
- `entryScreenId` set and valid.
|
|
40
42
|
- A **completion path** from entry (`end_flow`, terminal `next`, or external surface end).
|
|
41
43
|
- Decision nodes: every case and `elseNext` connected.
|
|
44
|
+
- `conditional` layers: every case has at least one rule (`conditional.incomplete_cases`).
|
|
42
45
|
|
|
43
46
|
### Integrations (default: enabled)
|
|
44
47
|
|
|
45
|
-
- External surfaces need `config.provider` (not `unspecified`).
|
|
48
|
+
- External surfaces need `config.provider` (not `unspecified`). Integration Nodes use partner providers such as `revenuecat`; External Surface Nodes use `headless`.
|
|
46
49
|
- Every external surface needs `fallback`.
|
|
47
50
|
- RevenueCat surfaces require RevenueCat integration enabled (import assumes enabled).
|
|
51
|
+
- External Surface Nodes (`provider: "headless"`) do not require an App settings toggle; the host must supply `externalSurfaces`.
|
|
48
52
|
|
|
49
53
|
### Canvas editor gates (default: all enabled)
|
|
50
54
|
|
|
@@ -76,21 +76,23 @@ Hero images that render through wrapper components (`<Illustration>`, `<Logo>`,
|
|
|
76
76
|
`react-native-pager-view`, a `FlatList horizontal pagingEnabled`, a
|
|
77
77
|
`ScrollView horizontal` snap pager, `Animated` `translateX` paging, or an
|
|
78
78
|
`infoSteps`/`currentStep` index → `kind: "carousel"` with one slide per page.
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
Paging is swipe by default; map the pager's own next button to a `button` with
|
|
80
|
+
`action.kind: "advance_carousel"` rather than a footer CTA
|
|
81
|
+
([carousel-import.md](carousel-import.md)).
|
|
81
82
|
|
|
82
83
|
## Integrations and native steps
|
|
83
84
|
|
|
84
85
|
- `react-native-purchases` / `react-native-purchases-ui` / `Purchases.configure`
|
|
85
|
-
/ a `<Paywall>` → a RevenueCat **
|
|
86
|
-
required `fallback`).
|
|
86
|
+
/ a `<Paywall>` → a RevenueCat **Integration Node** (`provider: "revenuecat"`,
|
|
87
|
+
required `fallback`). Host-owned custom screens → an **External Surface Node**
|
|
88
|
+
(`provider: "headless"`). See [integrations](../../rheo-best-practices/references/integrations.md).
|
|
87
89
|
- `react-native-appsflyer` → represent stable attribution branches via decision
|
|
88
90
|
nodes; add keys to `sdkAttributeKeys`. Never include AppsFlyer/RevenueCat secrets.
|
|
89
91
|
- OAuth / email-password screens → `oauth_login` / `email_password_auth` (host
|
|
90
92
|
owns the actual auth logic).
|
|
91
93
|
- `react-native-permissions` prompts → a `request_os_permission` button action.
|
|
92
94
|
- Signature pads, camera capture, custom WebViews → confirm with the user whether
|
|
93
|
-
to keep native (host-owned) or approximate; flag unmappable behavior in chat.
|
|
95
|
+
to keep native (External Surface Node / host-owned) or approximate; flag unmappable behavior in chat.
|
|
94
96
|
|
|
95
97
|
## i18n
|
|
96
98
|
|
|
@@ -74,13 +74,15 @@ real asset — follow the struct to its `Image("…")` to find the asset name.
|
|
|
74
74
|
|
|
75
75
|
A paging `TabView { … }.tabViewStyle(.page)`, a horizontal `ScrollView` with snap
|
|
76
76
|
paging, or an `infoSteps`/`currentStep` index → `kind: "carousel"`, one slide per
|
|
77
|
-
page
|
|
77
|
+
page. Paging is swipe by default; an explicit Next button becomes a `button` with
|
|
78
|
+
`action.kind: "advance_carousel"` ([carousel-import.md](carousel-import.md)).
|
|
78
79
|
|
|
79
80
|
## Integrations and native steps
|
|
80
81
|
|
|
81
82
|
- RevenueCat (`Purchases.configure`, `RevenueCatUI` `PaywallView`, `.presentPaywall`)
|
|
82
|
-
→ a RevenueCat **
|
|
83
|
-
`fallback`).
|
|
83
|
+
→ a RevenueCat **Integration Node** (`provider: "revenuecat"`, required
|
|
84
|
+
`fallback`). Host-owned custom screens → an **External Surface Node**
|
|
85
|
+
(`provider: "headless"`). See [integrations](../../rheo-best-practices/references/integrations.md).
|
|
84
86
|
- AppsFlyer (`AppsFlyerLib`) → represent stable attribution branches via decision
|
|
85
87
|
nodes; add keys to `sdkAttributeKeys`. Never include secrets.
|
|
86
88
|
- "Sign in with Apple" (`SignInWithAppleButton`) / OAuth / email-password →
|
|
@@ -89,7 +91,7 @@ page, swipe-only (no in-pager button) ([carousel-import.md](carousel-import.md))
|
|
|
89
91
|
`UNUserNotificationCenter`) → a `request_os_permission` button action; the host
|
|
90
92
|
must declare the matching `Info.plist` usage strings.
|
|
91
93
|
- `PencilKit` signature, camera capture, custom `WKWebView` → confirm whether to
|
|
92
|
-
keep native (host-owned) or approximate; flag unmappable behavior in chat.
|
|
94
|
+
keep native (External Surface Node / host-owned) or approximate; flag unmappable behavior in chat.
|
|
93
95
|
|
|
94
96
|
## Localization
|
|
95
97
|
|