@revenuecat/purchases-ui-js 4.7.6 → 4.8.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.
@@ -175,9 +175,6 @@
175
175
  action: {
176
176
  type: "workflow",
177
177
  },
178
- triggers: {
179
- on_press: "workflow-action-id",
180
- },
181
178
  stack: {
182
179
  ...stackProps(5),
183
180
  background_color: {
@@ -67,12 +67,6 @@
67
67
  componentName: props.name,
68
68
  componentValue: "navigate_back",
69
69
  };
70
- case "navigate_to_page":
71
- return {
72
- componentType: "button",
73
- componentName: props.name,
74
- componentValue: "navigate_to_page",
75
- };
76
70
  case "close_workflow":
77
71
  return {
78
72
  componentType: "button",
@@ -149,7 +143,9 @@
149
143
 
150
144
  const onclick = () => {
151
145
  if (isDisabled) return;
152
- const actionId = props.triggers?.on_press;
146
+ // For "workflow" actions, pass the component's own id so Workflow.svelte
147
+ // can look it up in the step's triggers array (component ID → action ID).
148
+ const actionId = action.type === "workflow" ? props.id : undefined;
153
149
  if (action.type === "navigate_to" && action.destination === "sheet") {
154
150
  if (action.sheet != null) {
155
151
  emitComponentInteraction(getSheetOpenInteractionData(action));
@@ -173,8 +169,6 @@
173
169
  return false;
174
170
  case "navigate_back":
175
171
  return !hideBackButtons;
176
- case "navigate_to_page":
177
- return true;
178
172
  case "close_workflow":
179
173
  return true;
180
174
  case "navigate_to":
@@ -25,6 +25,7 @@
25
25
  import { waitForAnimations } from "storybook/internal/preview-api";
26
26
  import { viewportDecorator } from "../../stories/viewport-decorator";
27
27
  import { BACKGROUND_PAYWALL } from "./fixtures/background-paywall";
28
+ import { FIXED_HEIGHT_STACK_PAYWALL } from "./fixtures/fixed-height-stack-paywall";
28
29
  import { OVERRIDE_PAYWALL } from "./fixtures/override-paywall";
29
30
  import { SHEET_PAYWALL } from "./fixtures/sheet-paywall";
30
31
  import { SHEET_PAYWALL_VIDEO_STACKING } from "./fixtures/sheet-video-stacking-paywall";
@@ -74,6 +75,20 @@
74
75
  }}
75
76
  />
76
77
 
78
+ <!--
79
+ Regression story for fixed-height stacks being squeezed by the flex algorithm.
80
+ The root stack uses `space_between` and children's fixed heights sum to more than
81
+ the 400px viewport, so the empty spacer would collapse without `flex-shrink: 0`.
82
+ Chromatic diffs this story to block regressions of Stack.svelte's flex-shrink rule.
83
+ -->
84
+ <Story
85
+ name="Fixed-height stack (regression)"
86
+ decorators={[viewportDecorator(320, 400, 0)]}
87
+ args={{
88
+ paywallData: FIXED_HEIGHT_STACK_PAYWALL,
89
+ }}
90
+ />
91
+
77
92
  <Story
78
93
  name="Override paywall"
79
94
  args={{
@@ -71,11 +71,6 @@
71
71
  preferredColorMode?: ColorMode;
72
72
  onPurchaseClicked?: (selectedPackageId: string, actionId: string) => void;
73
73
  onBackClicked?: () => void;
74
- /**
75
- * Called when a `navigate_to_page` button is pressed and there is no
76
- * active nav_host context to handle it internally.
77
- */
78
- onNavigateToPage?: (pageId: string) => void;
79
74
  /** Called when a `close_workflow` button is pressed to dismiss the paywall/workflow. */
80
75
  onClose?: () => void;
81
76
  onVisitCustomerCenterClicked?: () => void;
@@ -135,7 +130,6 @@
135
130
  preferredColorMode,
136
131
  onPurchaseClicked,
137
132
  onBackClicked,
138
- onNavigateToPage,
139
133
  onClose,
140
134
  onVisitCustomerCenterClicked,
141
135
  onRestorePurchasesClicked,
@@ -248,9 +242,6 @@
248
242
  case "navigate_back":
249
243
  onBackClicked?.();
250
244
  return;
251
- case "navigate_to_page":
252
- onNavigateToPage?.(action.page_id);
253
- return;
254
245
  case "close_workflow":
255
246
  onClose?.();
256
247
  return;
@@ -28,11 +28,6 @@ interface Props {
28
28
  preferredColorMode?: ColorMode;
29
29
  onPurchaseClicked?: (selectedPackageId: string, actionId: string) => void;
30
30
  onBackClicked?: () => void;
31
- /**
32
- * Called when a `navigate_to_page` button is pressed and there is no
33
- * active nav_host context to handle it internally.
34
- */
35
- onNavigateToPage?: (pageId: string) => void;
36
31
  /** Called when a `close_workflow` button is pressed to dismiss the paywall/workflow. */
37
32
  onClose?: () => void;
38
33
  onVisitCustomerCenterClicked?: () => void;
@@ -0,0 +1,2 @@
1
+ import type { PaywallData } from "../../../types/paywall";
2
+ export declare const FIXED_HEIGHT_STACK_PAYWALL: PaywallData;
@@ -0,0 +1,87 @@
1
+ // Regression fixture: fixed-height empty stack collapses under flex pressure.
2
+ // Root `space_between` with children summing above viewport height. Pair with
3
+ // `viewportDecorator` in the story so the parent is bounded; otherwise the
4
+ // paywall grows to content height and no shrinkage happens.
5
+ const solidStack = (id, height, hex) => ({
6
+ type: "stack",
7
+ id,
8
+ name: id,
9
+ components: [],
10
+ dimension: {
11
+ type: "vertical",
12
+ alignment: "center",
13
+ distribution: "center",
14
+ },
15
+ size: {
16
+ width: { type: "fill" },
17
+ height: { type: "fixed", value: height },
18
+ },
19
+ background: null,
20
+ background_color: { light: { type: "hex", value: hex } },
21
+ badge: null,
22
+ border: null,
23
+ margin: { bottom: 0, leading: 0, top: 0, trailing: 0 },
24
+ padding: { bottom: 0, leading: 0, top: 0, trailing: 0 },
25
+ shadow: null,
26
+ shape: {
27
+ type: "rectangle",
28
+ corners: {
29
+ bottom_leading: 0,
30
+ bottom_trailing: 0,
31
+ top_leading: 0,
32
+ top_trailing: 0,
33
+ },
34
+ },
35
+ spacing: 0,
36
+ });
37
+ export const FIXED_HEIGHT_STACK_PAYWALL = {
38
+ id: "fixed_height_stack_paywall",
39
+ default_locale: "en_US",
40
+ components_localizations: { en_US: {} },
41
+ components_config: {
42
+ base: {
43
+ background: {
44
+ type: "color",
45
+ value: { light: { type: "hex", value: "#ffffff" } },
46
+ },
47
+ stack: {
48
+ type: "stack",
49
+ id: "root",
50
+ name: "Root",
51
+ components: [
52
+ solidStack("hero", 180, "#ff5f5f"),
53
+ solidStack("empty_spacer", 50, "#11d483"),
54
+ solidStack("card", 200, "#6366f1"),
55
+ solidStack("cta", 48, "#000000"),
56
+ ],
57
+ dimension: {
58
+ type: "vertical",
59
+ alignment: "center",
60
+ distribution: "space_between",
61
+ },
62
+ size: {
63
+ width: { type: "fill" },
64
+ height: { type: "fill" },
65
+ },
66
+ background: null,
67
+ background_color: null,
68
+ badge: null,
69
+ border: null,
70
+ margin: { bottom: 0, leading: 0, top: 0, trailing: 0 },
71
+ padding: { bottom: 16, leading: 16, top: 16, trailing: 16 },
72
+ shadow: null,
73
+ shape: {
74
+ type: "rectangle",
75
+ corners: {
76
+ bottom_leading: 0,
77
+ bottom_trailing: 0,
78
+ top_leading: 0,
79
+ top_trailing: 0,
80
+ },
81
+ },
82
+ spacing: 12,
83
+ },
84
+ sticky_footer: null,
85
+ },
86
+ },
87
+ };
@@ -32,7 +32,13 @@
32
32
  getHoverStateProps,
33
33
  } from "../../utils/style-utils";
34
34
  import type { Snippet } from "svelte";
35
- import { mapBadge, mapDimension, mapLayerAlignment } from "./stack-utils";
35
+ import {
36
+ mapBadge,
37
+ mapDimension,
38
+ mapFixedHeightBounds,
39
+ mapFixedWidthBounds,
40
+ mapLayerAlignment,
41
+ } from "./stack-utils";
36
42
 
37
43
  interface MiscProps {
38
44
  onclick?: () => void;
@@ -89,6 +95,8 @@
89
95
  position: "relative",
90
96
  width: mapSize(size.width),
91
97
  height: mapSize(size.height),
98
+ ...mapFixedWidthBounds(size.width),
99
+ ...mapFixedHeightBounds(size.height),
92
100
  ...mapDimension(dimension),
93
101
  gap: px(spacing),
94
102
  margin: mapSpacing(margin),
@@ -1,6 +1,9 @@
1
+ import type { Size } from "../../types";
1
2
  import type { Dimension, ZAlignment } from "../../types/alignment";
2
3
  import type { Badge } from "../../types/components/stack";
3
4
  import { type CSS } from "../../utils/base-utils";
5
+ export declare function mapFixedWidthBounds(width: Size): CSS;
6
+ export declare function mapFixedHeightBounds(height: Size): CSS;
4
7
  export declare function mapDimension(dimension: Dimension): Record<string, string>;
5
8
  type JustifyAlign = {
6
9
  justifyContent: string;
@@ -14,6 +14,20 @@ const DISTRIBUTION_MAP = Object.freeze({
14
14
  center: "center",
15
15
  end: "flex-end",
16
16
  });
17
+ export function mapFixedWidthBounds(width) {
18
+ if (width.type !== "fixed") {
19
+ return {};
20
+ }
21
+ const value = px(width.value);
22
+ return { "min-width": value, "max-width": value };
23
+ }
24
+ export function mapFixedHeightBounds(height) {
25
+ if (height.type !== "fixed") {
26
+ return {};
27
+ }
28
+ const value = px(height.value);
29
+ return { "min-height": value, "max-height": value };
30
+ }
17
31
  export function mapDimension(dimension) {
18
32
  if (dimension.type === "zlayer") {
19
33
  return {};
@@ -21,7 +21,6 @@
21
21
  actionId: string,
22
22
  ) => void | Promise<void>;
23
23
  onBackClicked?: () => void;
24
- onNavigateToPage?: (pageId: string) => void;
25
24
  onClose?: () => void;
26
25
  containerId?: string;
27
26
  maxContentWidth?: string;
@@ -55,7 +54,6 @@
55
54
  onComponentInteraction,
56
55
  onPurchaseClicked,
57
56
  onBackClicked,
58
- onNavigateToPage,
59
57
  onClose,
60
58
  containerId = "screen-container",
61
59
  maxContentWidth,
@@ -94,7 +92,6 @@
94
92
  {onCompleteWorkflowNavigate}
95
93
  onVisitCustomerCenterClicked={() => onVisitCustomerCenterClicked?.()}
96
94
  {onBackClicked}
97
- {onNavigateToPage}
98
95
  {onClose}
99
96
  onRestorePurchasesClicked={() => onRestorePurchasesClicked?.()}
100
97
  onActionTriggered={(actionId: string) => {
@@ -16,7 +16,6 @@ interface Props {
16
16
  onComponentInteraction?: OnComponentInteraction;
17
17
  onPurchaseClicked?: (packageId: string, actionId: string) => void | Promise<void>;
18
18
  onBackClicked?: () => void;
19
- onNavigateToPage?: (pageId: string) => void;
20
19
  onClose?: () => void;
21
20
  containerId?: string;
22
21
  maxContentWidth?: string;
@@ -9,6 +9,21 @@
9
9
  import type { UIConfig } from "../../types/ui-config";
10
10
  import type { ReservedAttribute } from "../../types/components/input-text";
11
11
  import type { WorkflowNavData } from "../../types/workflow-nav";
12
+ import type {
13
+ WorkflowStepTriggerAction,
14
+ WorkflowStep,
15
+ } from "../../types/workflow";
16
+
17
+ type TriggerActionValue = WorkflowStep["trigger_actions"][string];
18
+
19
+ function isStepTriggerAction(
20
+ a: TriggerActionValue,
21
+ ): a is WorkflowStepTriggerAction {
22
+ return (
23
+ a.type === "step" &&
24
+ typeof (a as WorkflowStepTriggerAction).step_id === "string"
25
+ );
26
+ }
12
27
 
13
28
  interface Props {
14
29
  workflow: WorkflowNavData;
@@ -71,34 +86,115 @@
71
86
  }: Props = $props();
72
87
 
73
88
  // ── Nav stack ─────────────────────────────────────────────────────────────
74
- // The stack always has at least the initial page. The current page is the
75
- // last entry. navigate_to_page pushes, navigate_back pops.
89
+ // Each entry pairs the screen ID (used to look up the page to render) with
90
+ // the step ID (used to resolve trigger_actions on button press). The current
91
+ // page is always the last entry; workflow actions push, navigate_back pops.
76
92
  //
77
93
  // We key the reset off workflow.initial_page_id (a stable string) rather
78
94
  // than workflow object identity, so hosts that recompute the prop object on
79
95
  // unrelated reactive updates don't accidentally snap the user back to the
80
96
  // first page mid-flow.
81
- let navStack = $state<string[]>([]);
97
+ interface NavEntry {
98
+ pageId: string;
99
+ stepId: string;
100
+ }
101
+ let navStack = $state<NavEntry[]>([]);
82
102
  let trackedInitialPageId = $state<string>();
83
103
 
84
- const currentPageId = $derived(
85
- navStack[navStack.length - 1] ?? workflow.initial_page_id,
104
+ const current = $derived(
105
+ navStack[navStack.length - 1] ?? {
106
+ pageId: workflow.initial_page_id,
107
+ stepId: workflow.initial_step_id,
108
+ },
86
109
  );
87
- const currentScreen = $derived(workflow.pages[currentPageId]);
110
+ const currentScreen = $derived(workflow.pages[current.pageId]);
88
111
 
89
112
  $effect.pre(() => {
90
113
  if (trackedInitialPageId !== workflow.initial_page_id) {
91
114
  trackedInitialPageId = workflow.initial_page_id;
92
- navStack = [workflow.initial_page_id];
115
+ navStack = [
116
+ {
117
+ pageId: workflow.initial_page_id,
118
+ stepId: workflow.initial_step_id,
119
+ },
120
+ ];
93
121
  }
94
122
  });
95
123
 
96
- function handleNavigateToPage(pageId: string) {
97
- if (!workflow.pages[pageId]) {
98
- console.warn(`[Workflow] navigate_to_page: unknown page_id "${pageId}"`);
124
+ /**
125
+ * Resolves a "workflow"-type button action and navigates to the next screen.
126
+ *
127
+ * Flow: component ID → step.triggers[].action_id → trigger_actions[action_id]
128
+ * → step_id → step.screen_id → push onto nav stack.
129
+ */
130
+ function handleActionTriggered(actionId: string) {
131
+ const currentStep = workflow.steps[current.stepId];
132
+ if (!currentStep) {
133
+ console.warn(
134
+ `[Workflow] onActionTriggered: no current step found for step_id "${current.stepId}"`,
135
+ );
136
+ return;
137
+ }
138
+
139
+ // Resolve the component ID to an action ID via the step's triggers array.
140
+ const resolvedActionId = currentStep.triggers.find(
141
+ (t) => t.component_id === actionId,
142
+ )?.action_id;
143
+
144
+ if (!resolvedActionId) {
145
+ console.warn(
146
+ `[Workflow] onActionTriggered: no trigger entry found for component "${actionId}" on step "${current.stepId}"`,
147
+ );
148
+ onActionTriggered?.(actionId);
149
+ return;
150
+ }
151
+
152
+ const triggerAction = currentStep.trigger_actions[resolvedActionId];
153
+ if (!triggerAction) {
154
+ console.warn(
155
+ `[Workflow] onActionTriggered: no trigger_action found for action_id "${resolvedActionId}" on step "${current.stepId}"`,
156
+ );
157
+ onActionTriggered?.(actionId);
158
+ return;
159
+ }
160
+
161
+ if (!isStepTriggerAction(triggerAction)) {
162
+ // Condition-based actions require a full workflow engine — fall back to
163
+ // the host's onActionTriggered for anything we can't resolve client-side.
164
+ console.warn(
165
+ `[Workflow] onActionTriggered: trigger_action is not a simple step action, falling back to onActionTriggered`,
166
+ );
167
+ onActionTriggered?.(actionId);
168
+ return;
169
+ }
170
+
171
+ const nextStep = workflow.steps[triggerAction.step_id];
172
+ if (!nextStep) {
173
+ console.warn(
174
+ `[Workflow] onActionTriggered: step_id "${triggerAction.step_id}" not found in workflow steps`,
175
+ );
176
+ return;
177
+ }
178
+
179
+ const nextPageId = nextStep.screen_id;
180
+ if (!nextPageId) {
181
+ console.warn(
182
+ `[Workflow] onActionTriggered: step "${triggerAction.step_id}" has no screen_id (logic/experiment step?)`,
183
+ );
184
+ return;
185
+ }
186
+
187
+ if (!workflow.pages[nextPageId]) {
188
+ console.warn(
189
+ `[Workflow] onActionTriggered: screen_id "${nextPageId}" not found in workflow pages`,
190
+ );
99
191
  return;
100
192
  }
101
- navStack = [...navStack, pageId];
193
+
194
+ navStack = [
195
+ ...navStack,
196
+ { pageId: nextPageId, stepId: triggerAction.step_id },
197
+ ];
102
198
  }
103
199
 
104
200
  function handleBackClicked() {
@@ -111,7 +207,7 @@
111
207
  }
112
208
  </script>
113
209
 
114
- {#key currentPageId}
210
+ {#key current.pageId}
115
211
  <Screen
116
212
  paywallComponents={currentScreen}
117
213
  {uiConfig}
@@ -124,7 +220,7 @@
124
220
  {walletButtonRender}
125
221
  {safeAreaFallbackColor}
126
222
  {onPurchaseClicked}
127
- {onActionTriggered}
223
+ onActionTriggered={handleActionTriggered}
128
224
  {onInputChanged}
129
225
  {onReservedAttributeChanged}
130
226
  {onCompleteWorkflowNavigate}
@@ -132,7 +228,6 @@
132
228
  {onRestorePurchasesClicked}
133
229
  {onVisitCustomerCenterClicked}
134
230
  {onComponentInteraction}
135
- onNavigateToPage={handleNavigateToPage}
136
231
  onBackClicked={handleBackClicked}
137
232
  {onClose}
138
233
  />
@@ -1,12 +1,10 @@
1
1
  import type { WorkflowNavData } from "../../../types/workflow-nav";
2
2
  /**
3
- * Two-page workflow fixture that exercises navigate_to_page, navigate_back,
4
- * and close_workflow button actions via the Workflow component.
3
+ * Two-page workflow fixture. The "Next" button uses a "workflow" action, whose
4
+ * destination is resolved client-side via the step's trigger_actions map.
5
5
  *
6
6
  * Page layout:
7
- * "rcpaywall_welcome" — welcome screen with a "Next →" navigate_to_page button
7
+ * "rcpaywall_welcome" — welcome screen with a "Next →" button
8
8
  * "rcpaywall_details" — details screen with "← Back" and "✕ Close" buttons
9
- *
10
- * Page IDs match a paywall rc_public_id as they would in a real SDK response.
11
9
  */
12
10
  export declare const TWO_PAGE_WORKFLOW: WorkflowNavData;
@@ -1,15 +1,48 @@
1
1
  /**
2
- * Two-page workflow fixture that exercises navigate_to_page, navigate_back,
3
- * and close_workflow button actions via the Workflow component.
2
+ * Two-page workflow fixture. The "Next" button uses a "workflow" action, whose
3
+ * destination is resolved client-side via the step's trigger_actions map.
4
4
  *
5
5
  * Page layout:
6
- * "rcpaywall_welcome" — welcome screen with a "Next →" navigate_to_page button
6
+ * "rcpaywall_welcome" — welcome screen with a "Next →" button
7
7
  * "rcpaywall_details" — details screen with "← Back" and "✕ Close" buttons
8
- *
9
- * Page IDs match a paywall rc_public_id as they would in a real SDK response.
10
8
  */
11
9
  export const TWO_PAGE_WORKFLOW = {
12
10
  initial_page_id: "rcpaywall_welcome",
11
+ initial_step_id: "step_welcome",
12
+ steps: {
13
+ step_welcome: {
14
+ id: "step_welcome",
15
+ screen_id: "rcpaywall_welcome",
16
+ type: "screen",
17
+ param_values: {},
18
+ trigger_actions: {
19
+ // "next_btn" is the action_id referenced by the triggers array entry
20
+ // for the "next_btn" button component. It resolves to step_details,
21
+ // whose screen_id is rcpaywall_details.
22
+ next_btn: { type: "step", step_id: "step_details" },
23
+ },
24
+ triggers: [
25
+ {
26
+ action_id: "next_btn",
27
+ component_id: "next_btn",
28
+ type: "on_press",
29
+ name: "Next button",
30
+ },
31
+ ],
32
+ outputs: {},
33
+ metadata: null,
34
+ },
35
+ step_details: {
36
+ id: "step_details",
37
+ screen_id: "rcpaywall_details",
38
+ type: "screen",
39
+ param_values: {},
40
+ trigger_actions: {},
41
+ triggers: [],
42
+ outputs: {},
43
+ metadata: null,
44
+ },
45
+ },
13
46
  pages: {
14
47
  rcpaywall_welcome: {
15
48
  id: "rcpaywall_welcome",
@@ -58,7 +91,6 @@ export const TWO_PAGE_WORKFLOW = {
58
91
  shadow: null,
59
92
  shape: null,
60
93
  components: [
61
- // ── Title ────────────────────────────────────────────────
62
94
  {
63
95
  type: "text",
64
96
  id: "welcome_title",
@@ -79,7 +111,6 @@ export const TWO_PAGE_WORKFLOW = {
79
111
  dark: { type: "hex", value: "transparent" },
80
112
  },
81
113
  },
82
- // ── Body ─────────────────────────────────────────────────
83
114
  {
84
115
  type: "text",
85
116
  id: "welcome_body",
@@ -100,15 +131,11 @@ export const TWO_PAGE_WORKFLOW = {
100
131
  dark: { type: "hex", value: "transparent" },
101
132
  },
102
133
  },
103
- // ── navigate_to_page button ───────────────────────────
104
134
  {
105
135
  type: "button",
106
136
  id: "next_btn",
107
137
  name: "Next",
108
- action: {
109
- type: "navigate_to_page",
110
- page_id: "rcpaywall_details",
111
- },
138
+ action: { type: "workflow" },
112
139
  stack: {
113
140
  type: "stack",
114
141
  id: "next_btn_stack",
@@ -121,7 +148,12 @@ export const TWO_PAGE_WORKFLOW = {
121
148
  },
122
149
  spacing: 0,
123
150
  margin: { top: 0, bottom: 0, leading: 24, trailing: 24 },
124
- padding: { top: 16, bottom: 16, leading: 24, trailing: 24 },
151
+ padding: {
152
+ top: 16,
153
+ bottom: 16,
154
+ leading: 24,
155
+ trailing: 24,
156
+ },
125
157
  background: {
126
158
  type: "color",
127
159
  value: {
@@ -187,8 +219,8 @@ export const TWO_PAGE_WORKFLOW = {
187
219
  background: {
188
220
  type: "color",
189
221
  value: {
190
- light: { type: "hex", value: "#fff4f0ff" },
191
- dark: { type: "hex", value: "#2e1a1aff" },
222
+ light: { type: "hex", value: "#f0fff4ff" },
223
+ dark: { type: "hex", value: "#1a2e1aff" },
192
224
  },
193
225
  },
194
226
  stack: {
@@ -211,7 +243,6 @@ export const TWO_PAGE_WORKFLOW = {
211
243
  shadow: null,
212
244
  shape: null,
213
245
  components: [
214
- // ── Title ────────────────────────────────────────────────
215
246
  {
216
247
  type: "text",
217
248
  id: "details_title",
@@ -224,15 +255,14 @@ export const TWO_PAGE_WORKFLOW = {
224
255
  margin: { top: 80, bottom: 16, leading: 24, trailing: 24 },
225
256
  padding: { top: 0, bottom: 0, leading: 0, trailing: 0 },
226
257
  color: {
227
- light: { type: "hex", value: "#2e1a1aff" },
228
- dark: { type: "hex", value: "#fff4f0ff" },
258
+ light: { type: "hex", value: "#1a2e1aff" },
259
+ dark: { type: "hex", value: "#f0fff4ff" },
229
260
  },
230
261
  background_color: {
231
262
  light: { type: "hex", value: "transparent" },
232
263
  dark: { type: "hex", value: "transparent" },
233
264
  },
234
265
  },
235
- // ── Body ─────────────────────────────────────────────────
236
266
  {
237
267
  type: "text",
238
268
  id: "details_body",
@@ -245,15 +275,14 @@ export const TWO_PAGE_WORKFLOW = {
245
275
  margin: { top: 0, bottom: 48, leading: 24, trailing: 24 },
246
276
  padding: { top: 0, bottom: 0, leading: 0, trailing: 0 },
247
277
  color: {
248
- light: { type: "hex", value: "#775555ff" },
249
- dark: { type: "hex", value: "#ccaaaaff" },
278
+ light: { type: "hex", value: "#337733ff" },
279
+ dark: { type: "hex", value: "#88cc88ff" },
250
280
  },
251
281
  background_color: {
252
282
  light: { type: "hex", value: "transparent" },
253
283
  dark: { type: "hex", value: "transparent" },
254
284
  },
255
285
  },
256
- // ── navigate_back button ──────────────────────────────
257
286
  {
258
287
  type: "button",
259
288
  id: "back_btn",
@@ -271,7 +300,12 @@ export const TWO_PAGE_WORKFLOW = {
271
300
  },
272
301
  spacing: 0,
273
302
  margin: { top: 0, bottom: 12, leading: 24, trailing: 24 },
274
- padding: { top: 16, bottom: 16, leading: 24, trailing: 24 },
303
+ padding: {
304
+ top: 16,
305
+ bottom: 16,
306
+ leading: 24,
307
+ trailing: 24,
308
+ },
275
309
  background: {
276
310
  type: "color",
277
311
  value: {
@@ -314,7 +348,6 @@ export const TWO_PAGE_WORKFLOW = {
314
348
  ],
315
349
  },
316
350
  },
317
- // ── close button ──────────────────────────────────────
318
351
  {
319
352
  type: "button",
320
353
  id: "close_btn",
@@ -332,7 +365,12 @@ export const TWO_PAGE_WORKFLOW = {
332
365
  },
333
366
  spacing: 0,
334
367
  margin: { top: 0, bottom: 40, leading: 24, trailing: 24 },
335
- padding: { top: 16, bottom: 16, leading: 24, trailing: 24 },
368
+ padding: {
369
+ top: 16,
370
+ bottom: 16,
371
+ leading: 24,
372
+ trailing: 24,
373
+ },
336
374
  background: {
337
375
  type: "color",
338
376
  value: {
@@ -11,10 +11,6 @@ interface RestorePurchasesAction {
11
11
  interface NavigateBackAction {
12
12
  type: "navigate_back";
13
13
  }
14
- interface NavigateToPageAction {
15
- type: "navigate_to_page";
16
- page_id: string;
17
- }
18
14
  interface CloseAction {
19
15
  type: "close_workflow";
20
16
  }
@@ -60,15 +56,12 @@ interface CompleteWorkflowAction {
60
56
  };
61
57
  url_query_params?: CompleteWorkflowUrlQueryParams;
62
58
  }
63
- export type Action = WorkflowAction | RestorePurchasesAction | NavigateBackAction | NavigateToPageAction | CloseAction | NavigateToAction | NavigateToSheetAction | NavigateToWebPurchase | NavigateToUrlAction | CompleteWorkflowAction;
59
+ export type Action = WorkflowAction | RestorePurchasesAction | NavigateBackAction | CloseAction | NavigateToAction | NavigateToSheetAction | NavigateToWebPurchase | NavigateToUrlAction | CompleteWorkflowAction;
64
60
  export interface ButtonProps extends BaseComponent {
65
61
  type: "button";
66
62
  action: Action;
67
63
  stack: StackProps;
68
64
  transition?: null;
69
- triggers?: {
70
- on_press?: string;
71
- };
72
65
  overrides?: Overrides<ButtonProps>;
73
66
  }
74
67
  export {};
@@ -16,7 +16,7 @@ interface PackageTransition {
16
16
  destinationPackageId: string;
17
17
  defaultPackageId?: string;
18
18
  }
19
- type ButtonInteractionValue = "workflow" | "navigate_to_url" | "navigate_back" | "navigate_to_page" | "close_workflow" | "restore_purchases" | "navigate_to_customer_center" | "screen_redirect" | "navigate_to_privacy_policy" | "navigate_to_terms" | "navigate_to_sheet" | "navigate_to_offer_code" | "navigate_to_web_paywall_link";
19
+ type ButtonInteractionValue = "workflow" | "navigate_to_url" | "navigate_back" | "close_workflow" | "restore_purchases" | "navigate_to_customer_center" | "screen_redirect" | "navigate_to_privacy_policy" | "navigate_to_terms" | "navigate_to_sheet" | "navigate_to_offer_code" | "navigate_to_web_paywall_link";
20
20
  export type TabInteractionData = ComponentInteractionBase<"tab"> & IndexedTransition;
21
21
  export type SwitchInteractionData = ComponentInteractionBase<"switch", "on" | "off">;
22
22
  export type CarouselInteractionData = ComponentInteractionBase<"carousel"> & IndexedTransition;
@@ -1,27 +1,33 @@
1
- import type { WorkflowData, WorkflowScreen } from "./workflow";
1
+ import type { WorkflowData, WorkflowScreen, WorkflowStep } from "./workflow";
2
2
  /**
3
3
  * A self-contained multi-page workflow payload for use with the Workflow
4
4
  * component. This mirrors the shape of rc-workflows' WorkflowData but only
5
- * includes the fields needed for client-side page navigation — it does NOT
6
- * include step trigger_actions or experiment logic, which remain rc-workflows
7
- * concerns.
5
+ * includes the fields needed for client-side page navigation.
8
6
  */
9
7
  export interface WorkflowNavData {
10
8
  /** ID of the first page to display. Must be a key in `pages`. */
11
9
  initial_page_id: string;
12
10
  /**
13
- * All pages in this workflow keyed by page ID (the screen's rc_public_id).
14
- * `navigate_to_page` actions reference these IDs directly.
11
+ * All pages in this workflow keyed by screen ID.
12
+ * Workflow step actions resolve to these IDs via `steps[stepId].screen_id`.
15
13
  */
16
14
  pages: Record<string, WorkflowScreen>;
15
+ /**
16
+ * All steps in this workflow keyed by step ID.
17
+ * Used to resolve `workflow`-type button actions: the button's component ID
18
+ * is looked up in the current step's `triggers` array to get an action_id,
19
+ * which maps to a `trigger_actions` entry pointing to the next step_id,
20
+ * whose screen_id is then used to navigate to the correct page.
21
+ */
22
+ steps: Record<string, WorkflowStep>;
23
+ /** The step ID of the first step, corresponding to `initial_page_id`. */
24
+ initial_step_id: string;
17
25
  }
18
26
  /**
19
27
  * Map a full `WorkflowData` SDK response to the `WorkflowNavData` shape
20
28
  * consumed by the `Workflow` component.
21
29
  *
22
- * - `pages` is taken directly from `WorkflowData.screens` (already keyed by
23
- * screen/paywall rc_public_id, which is what `navigate_to_page` page_ids
24
- * reference).
30
+ * - `pages` is taken directly from `WorkflowData.screens`, keyed by screen ID.
25
31
  * - `initial_page_id` is resolved by looking up the screen_id of the initial
26
32
  * step, since `initial_step_id` is a step ID, not a screen ID.
27
33
  *
@@ -2,9 +2,7 @@
2
2
  * Map a full `WorkflowData` SDK response to the `WorkflowNavData` shape
3
3
  * consumed by the `Workflow` component.
4
4
  *
5
- * - `pages` is taken directly from `WorkflowData.screens` (already keyed by
6
- * screen/paywall rc_public_id, which is what `navigate_to_page` page_ids
7
- * reference).
5
+ * - `pages` is taken directly from `WorkflowData.screens`, keyed by screen ID.
8
6
  * - `initial_page_id` is resolved by looking up the screen_id of the initial
9
7
  * step, since `initial_step_id` is a step ID, not a screen ID.
10
8
  *
@@ -27,8 +25,10 @@ export function workflowDataToNavData(data) {
27
25
  }
28
26
  return {
29
27
  initial_page_id: initialPageId,
30
- // Shared by reference intentionally — screens are never mutated after
28
+ initial_step_id: data.initial_step_id,
29
+ // Shared by reference intentionally — screens/steps are never mutated after
31
30
  // WorkflowData is received from the SDK, so a copy would be wasteful.
32
31
  pages: data.screens,
32
+ steps: data.steps,
33
33
  };
34
34
  }
@@ -13,6 +13,27 @@ export interface WorkflowScreen extends PaywallData {
13
13
  revision: number;
14
14
  template_name: string;
15
15
  }
16
+ /**
17
+ * A trigger action that navigates directly to another step.
18
+ */
19
+ export interface WorkflowStepTriggerAction {
20
+ type: "step";
21
+ step_id: string;
22
+ }
23
+ /**
24
+ * An entry in a step's `triggers` array that maps a component interaction
25
+ * (e.g. button press) to a named action_id, which in turn maps to a
26
+ * `trigger_actions` entry.
27
+ */
28
+ export interface WorkflowStepTrigger {
29
+ /** The action ID to look up in `trigger_actions`. */
30
+ action_id: string;
31
+ /** The component (e.g. button) id that fires this trigger. */
32
+ component_id: string;
33
+ /** The interaction type, e.g. "on_press". */
34
+ type: string;
35
+ name?: string;
36
+ }
16
37
  /**
17
38
  * A single step in a workflow, referencing a screen by ID.
18
39
  * Aligns with the SDK response shape from khepri.
@@ -22,8 +43,19 @@ export interface WorkflowStep {
22
43
  screen_id?: string;
23
44
  type: string;
24
45
  param_values: Record<string, unknown>;
25
- trigger_actions: Record<string, unknown>;
26
- triggers: Record<string, unknown>;
46
+ /**
47
+ * Maps action IDs (from `triggers[].action_id`) to the next step to navigate
48
+ * to. Currently only `type: "step"` is supported for client-side resolution;
49
+ * condition-based actions are ignored.
50
+ */
51
+ trigger_actions: Record<string, WorkflowStepTriggerAction | Record<string, unknown>>;
52
+ /**
53
+ * Array of trigger entries mapping component interactions to action IDs.
54
+ * Resolve a button press by matching its component `id` against
55
+ * `component_id`, then use the entry's `action_id` to look up
56
+ * `trigger_actions`.
57
+ */
58
+ triggers: WorkflowStepTrigger[];
27
59
  outputs: Record<string, unknown>;
28
60
  metadata: Record<string, unknown> | null;
29
61
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@revenuecat/purchases-ui-js",
3
3
  "description": "Web components for Paywalls. Powered by RevenueCat",
4
4
  "private": false,
5
- "version": "4.7.6",
5
+ "version": "4.8.0",
6
6
  "author": {
7
7
  "name": "RevenueCat, Inc."
8
8
  },