@getrheo/rheo-skill 2.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @getrheo/rheo-skill
2
2
 
3
- Current release: **`2.3.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`).
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.4.0",
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.4.0",
13
- "@getrheo/flow-runtime": "2.4.0",
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.4.0"
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 external surface.
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
- ## RevenueCat
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 channelId="ch_…" onFlowCompleted={() => {}} />
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 paywalls.
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 external surface presenter helpers.
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 external surface needs `fallback`. RevenueCat outcomes that are not explicitly mapped fall through to this target.
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
 
@@ -111,7 +111,10 @@ Valid `permissionKey` values for `request_os_permission`:
111
111
  - `email_password_auth` modes: `sign_in`, `sign_up` (sign_up requires email + password + confirm fields).
112
112
  - `icon` families: `ionicons`.
113
113
 
114
- ## External surface outcomes (RevenueCat)
114
+ ## External surface outcomes
115
115
 
116
- Normalized outcome keys for `externalSurfaceNodes[].outcomes`: `purchase_completed`, `purchase_cancelled`, `dismissed`, `failed`, `restore_completed`.
117
- Every external surface also needs a `fallback` jump target.
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`.
119
+
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`.
@@ -199,7 +199,8 @@ instead of a plain footer CTA ([carousel-import.md](carousel-import.md)).
199
199
  ## Decisions and external surfaces
200
200
 
201
201
  `decisions` accepts full `@getrheo/contracts` `DecisionNode` objects (the scaffold
202
- passes them through). RevenueCat paywalls use `externalSurfaces`:
202
+ passes them through). External surfaces use `externalSurfaces` (builder:
203
+ **Integration Node** for `revenuecat`, **External Surface Node** for `headless`):
203
204
 
204
205
  ```jsonc
205
206
  {
@@ -208,7 +209,16 @@ passes them through). RevenueCat paywalls use `externalSurfaces`:
208
209
  "offeringId": "default",
209
210
  "presentation": "paywall",
210
211
  "outcomes": { "purchase_completed": "scr_done", "restore_completed": "scr_done", "dismissed": "scr_offer2" },
211
- "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
212
222
  }
213
223
  ```
214
224
 
@@ -67,7 +67,7 @@ Use only: `stack`, `text`, `image`, `lottie`, `video`, `icon`, `button`, `back_b
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
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 paywalls are external surface nodes and always need `fallback`.
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.
@@ -195,9 +195,13 @@ Use `conditional` when the source renders different content on **one** screen ba
195
195
 
196
196
  ## External Surfaces
197
197
 
198
- - RevenueCat paywalls become `externalSurfaceNodes` with provider `revenuecat`.
199
- - Every external surface needs a `fallback`.
200
- - Map known paywall outcomes to `purchase_completed`, `restore_completed`, `dismissed`, and `failed`.
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`.
201
205
 
202
206
  ## Assets
203
207
 
@@ -45,9 +45,10 @@ These mirror `apps/web/src/features/builder/validateFlow.ts` and API `preflightP
45
45
 
46
46
  ### Integrations (default: enabled)
47
47
 
48
- - 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`.
49
49
  - Every external surface needs `fallback`.
50
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`.
51
52
 
52
53
  ### Canvas editor gates (default: all enabled)
53
54
 
@@ -83,15 +83,16 @@ Paging is swipe by default; map the pager's own next button to a `button` with
83
83
  ## Integrations and native steps
84
84
 
85
85
  - `react-native-purchases` / `react-native-purchases-ui` / `Purchases.configure`
86
- / a `<Paywall>` → a RevenueCat **external surface** (`provider: "revenuecat"`,
87
- required `fallback`). See [integrations](../../rheo-best-practices/references/integrations.md).
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).
88
89
  - `react-native-appsflyer` → represent stable attribution branches via decision
89
90
  nodes; add keys to `sdkAttributeKeys`. Never include AppsFlyer/RevenueCat secrets.
90
91
  - OAuth / email-password screens → `oauth_login` / `email_password_auth` (host
91
92
  owns the actual auth logic).
92
93
  - `react-native-permissions` prompts → a `request_os_permission` button action.
93
94
  - Signature pads, camera capture, custom WebViews → confirm with the user whether
94
- 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.
95
96
 
96
97
  ## i18n
97
98
 
@@ -80,8 +80,9 @@ page. Paging is swipe by default; an explicit Next button becomes a `button` wit
80
80
  ## Integrations and native steps
81
81
 
82
82
  - RevenueCat (`Purchases.configure`, `RevenueCatUI` `PaywallView`, `.presentPaywall`)
83
- → a RevenueCat **external surface** (`provider: "revenuecat"`, required
84
- `fallback`). See [integrations](../../rheo-best-practices/references/integrations.md).
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).
85
86
  - AppsFlyer (`AppsFlyerLib`) → represent stable attribution branches via decision
86
87
  nodes; add keys to `sdkAttributeKeys`. Never include secrets.
87
88
  - "Sign in with Apple" (`SignInWithAppleButton`) / OAuth / email-password →
@@ -90,7 +91,7 @@ page. Paging is swipe by default; an explicit Next button becomes a `button` wit
90
91
  `UNUserNotificationCenter`) → a `request_os_permission` button action; the host
91
92
  must declare the matching `Info.plist` usage strings.
92
93
  - `PencilKit` signature, camera capture, custom `WKWebView` → confirm whether to
93
- 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.
94
95
 
95
96
  ## Localization
96
97
 
@@ -8422,15 +8422,20 @@ var conditionalFieldScopeViolations = (screen) => {
8422
8422
  return violations;
8423
8423
  };
8424
8424
  var ExternalSurfaceNodeIdSchema = external_exports.string().min(1).max(64).regex(/^surf_[a-z0-9_]+$/i, "external surface node id must look like surf_<id>");
8425
- var NORMALIZED_SURFACE_OUTCOMES = [
8425
+ var IAP_SURFACE_OUTCOMES = [
8426
8426
  "purchase_completed",
8427
8427
  "purchase_cancelled",
8428
8428
  "dismissed",
8429
8429
  "failed",
8430
8430
  "restore_completed"
8431
8431
  ];
8432
+ var NORMALIZED_SURFACE_OUTCOMES = [
8433
+ ...IAP_SURFACE_OUTCOMES,
8434
+ "completed",
8435
+ "back"
8436
+ ];
8432
8437
  var NormalizedSurfaceOutcomeSchema = external_exports.enum(NORMALIZED_SURFACE_OUTCOMES);
8433
- var SurfaceProviderSchema = external_exports.enum(["unspecified", "revenuecat"]);
8438
+ var SurfaceProviderSchema = external_exports.enum(["unspecified", "revenuecat", "headless"]);
8434
8439
  var UnspecifiedExternalSurfaceConfigSchema = external_exports.object({
8435
8440
  provider: external_exports.literal("unspecified")
8436
8441
  });
@@ -8441,16 +8446,28 @@ var RevenueCatSurfaceConfigSchema = external_exports.object({
8441
8446
  placementId: external_exports.string().min(1).max(128).optional(),
8442
8447
  presentation: RevenueCatSurfacePresentationSchema.optional()
8443
8448
  });
8449
+ var ExternalSurfaceHostKeySchema = external_exports.string().min(1).max(64).regex(
8450
+ /^[a-zA-Z][a-zA-Z0-9_]*$/,
8451
+ "host key must start with a letter and contain only letters, digits, or underscores"
8452
+ );
8453
+ var HeadlessExternalSurfaceConfigSchema = external_exports.object({
8454
+ provider: external_exports.literal("headless"),
8455
+ /** Registry key for `Flow` / `FlowView` `externalSurfaces`. Defaults to the node id. */
8456
+ hostKey: ExternalSurfaceHostKeySchema.optional()
8457
+ });
8444
8458
  var ExternalSurfaceConfigSchema = external_exports.discriminatedUnion("provider", [
8445
8459
  UnspecifiedExternalSurfaceConfigSchema,
8446
- RevenueCatSurfaceConfigSchema
8460
+ RevenueCatSurfaceConfigSchema,
8461
+ HeadlessExternalSurfaceConfigSchema
8447
8462
  ]);
8448
8463
  var ExternalSurfaceOutcomesMapSchema = external_exports.object({
8449
8464
  purchase_completed: FlowJumpTargetSchema2.optional(),
8450
8465
  purchase_cancelled: FlowJumpTargetSchema2.optional(),
8451
8466
  dismissed: FlowJumpTargetSchema2.optional(),
8452
8467
  failed: FlowJumpTargetSchema2.optional(),
8453
- restore_completed: FlowJumpTargetSchema2.optional()
8468
+ restore_completed: FlowJumpTargetSchema2.optional(),
8469
+ completed: FlowJumpTargetSchema2.optional(),
8470
+ back: FlowJumpTargetSchema2.optional()
8454
8471
  }).strict();
8455
8472
  var ExternalSurfaceNodeSchema = external_exports.object({
8456
8473
  id: ExternalSurfaceNodeIdSchema,
@@ -8461,6 +8478,12 @@ var ExternalSurfaceNodeSchema = external_exports.object({
8461
8478
  /** Required: used for any outcome not in `outcomes` (e.g. provider quirks, unmapped events). */
8462
8479
  fallback: FlowJumpTargetSchema2
8463
8480
  });
8481
+ var resolveExternalSurfaceHostKey = (node) => {
8482
+ if (node.config.provider === "headless" && node.config.hostKey) {
8483
+ return node.config.hostKey;
8484
+ }
8485
+ return node.id;
8486
+ };
8464
8487
  var RESERVED_RC_SDK_KEYS = [
8465
8488
  /** Last RC event observed by the SDK (e.g. `purchase_completed`, `purchase_cancelled`). */
8466
8489
  "onb_rc_last_event",
@@ -8710,6 +8733,7 @@ var refineManifestGraph = (manifest, ctx, jumpTargets) => {
8710
8733
  }
8711
8734
  });
8712
8735
  const seenSurfaceId = /* @__PURE__ */ new Set();
8736
+ const seenHostKey = /* @__PURE__ */ new Set();
8713
8737
  manifest.externalSurfaceNodes.forEach((sn, si) => {
8714
8738
  if (seenSurfaceId.has(sn.id)) {
8715
8739
  ctx.addIssue({
@@ -8733,6 +8757,17 @@ var refineManifestGraph = (manifest, ctx, jumpTargets) => {
8733
8757
  path: ["externalSurfaceNodes", si]
8734
8758
  });
8735
8759
  }
8760
+ if (sn.config.provider === "headless") {
8761
+ const hostKey = resolveExternalSurfaceHostKey(sn);
8762
+ if (seenHostKey.has(hostKey)) {
8763
+ ctx.addIssue({
8764
+ code: external_exports.ZodIssueCode.custom,
8765
+ message: `duplicate external surface host key "${hostKey}"`,
8766
+ path: ["externalSurfaceNodes", si, "config", "hostKey"]
8767
+ });
8768
+ }
8769
+ seenHostKey.add(hostKey);
8770
+ }
8736
8771
  for (const [outcome, target] of Object.entries(sn.outcomes)) {
8737
8772
  if (target != null && !jumpTargets.has(target)) {
8738
8773
  ctx.addIssue({
@@ -12435,7 +12470,7 @@ var fixForBuilderMessage = (message) => {
12435
12470
  return "Set externalSurfaceNodes[].config.provider to a real provider (e.g. revenuecat), not unspecified.";
12436
12471
  }
12437
12472
  if (message.includes("Fallback edge")) {
12438
- return "Connect every external surface fallback to the next screen/decision/surface when the paywall dismisses or fails.";
12473
+ return "Connect every external surface fallback to the next screen/decision/surface when no specific outcome is mapped.";
12439
12474
  }
12440
12475
  return `Resolve builder publish rule: ${message}`;
12441
12476
  };
@@ -12449,7 +12484,7 @@ var collectIntegrationIssues = (manifest, integrations) => {
12449
12484
  }
12450
12485
  if (node.fallback == null) {
12451
12486
  issues.push(
12452
- `External surface "${node.name ?? node.id}" needs a connected Fallback edge \u2014 every paywall must route somewhere when no specific outcome is mapped.`
12487
+ `External surface "${node.name ?? node.id}" needs a connected Fallback edge \u2014 every external surface must route somewhere when no specific outcome is mapped.`
12453
12488
  );
12454
12489
  }
12455
12490
  if (node.config.provider === "revenuecat" && !integrations.revenuecat.enabled) {
@@ -15004,16 +15039,19 @@ var ScreenSpecSchema = external_exports.object({
15004
15039
  var ExternalSurfaceSpecSchema = external_exports.object({
15005
15040
  id: external_exports.string().min(1).max(64),
15006
15041
  name: external_exports.string().min(1).max(80).optional(),
15007
- provider: external_exports.enum(["revenuecat", "unspecified"]),
15042
+ provider: external_exports.enum(["revenuecat", "headless", "unspecified"]),
15008
15043
  offeringId: external_exports.string().min(1).max(128).optional(),
15009
15044
  placementId: external_exports.string().min(1).max(128).optional(),
15010
15045
  presentation: external_exports.enum(["paywall", "paywall_if_needed"]).optional(),
15046
+ hostKey: external_exports.string().min(1).max(64).regex(/^[a-zA-Z][a-zA-Z0-9_]*$/).optional(),
15011
15047
  outcomes: external_exports.object({
15012
15048
  purchase_completed: external_exports.string().min(1).nullable().optional(),
15013
15049
  purchase_cancelled: external_exports.string().min(1).nullable().optional(),
15014
15050
  dismissed: external_exports.string().min(1).nullable().optional(),
15015
15051
  failed: external_exports.string().min(1).nullable().optional(),
15016
- restore_completed: external_exports.string().min(1).nullable().optional()
15052
+ restore_completed: external_exports.string().min(1).nullable().optional(),
15053
+ completed: external_exports.string().min(1).nullable().optional(),
15054
+ back: external_exports.string().min(1).nullable().optional()
15017
15055
  }).optional(),
15018
15056
  fallback: external_exports.string().min(1).nullable()
15019
15057
  });
@@ -15518,6 +15556,9 @@ var buildExternalSurface = (surface) => {
15518
15556
  ...surface.offeringId ? { offeringId: surface.offeringId } : {},
15519
15557
  ...surface.placementId ? { placementId: surface.placementId } : {},
15520
15558
  ...surface.presentation ? { presentation: surface.presentation } : {}
15559
+ } : surface.provider === "headless" ? {
15560
+ provider: "headless",
15561
+ ...surface.hostKey ? { hostKey: surface.hostKey } : {}
15521
15562
  } : { provider: "unspecified" };
15522
15563
  const outcomes = {};
15523
15564
  for (const [key, value] of Object.entries(surface.outcomes ?? {})) {