@revenuecat/purchases-ui-js 4.5.2 → 4.7.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.
@@ -13,6 +13,10 @@
13
13
  import { readable } from "svelte/store";
14
14
 
15
15
  const props: ButtonProps = $props();
16
+ type SheetAction = Extract<
17
+ ButtonProps["action"],
18
+ { type: "navigate_to"; destination: "sheet" }
19
+ >;
16
20
 
17
21
  const selectedState = getSelectedStateContext();
18
22
  const { action } = $derived.by(() => {
@@ -63,6 +67,18 @@
63
67
  componentName: props.name,
64
68
  componentValue: "navigate_back",
65
69
  };
70
+ case "navigate_to_page":
71
+ return {
72
+ componentType: "button",
73
+ componentName: props.name,
74
+ componentValue: "navigate_to_page",
75
+ };
76
+ case "close_workflow":
77
+ return {
78
+ componentType: "button",
79
+ componentName: props.name,
80
+ componentValue: "close_workflow",
81
+ };
66
82
  case "restore_purchases":
67
83
  return {
68
84
  componentType: "button",
@@ -122,28 +138,28 @@
122
138
  }
123
139
  };
124
140
 
125
- const getSheetOpenInteractionData =
126
- (): PackageSelectionSheetInteractionData => ({
127
- componentType: "package_selection_sheet",
128
- componentName: action.sheet?.name ?? props.name,
129
- componentValue: "open",
130
- currentPackageId: $selectedPackageId,
131
- });
141
+ const getSheetOpenInteractionData = (
142
+ sheetAction: SheetAction,
143
+ ): PackageSelectionSheetInteractionData => ({
144
+ componentType: "package_selection_sheet",
145
+ componentName: sheetAction.sheet?.name ?? props.name,
146
+ componentValue: "open",
147
+ currentPackageId: $selectedPackageId,
148
+ });
132
149
 
133
150
  const onclick = () => {
134
151
  if (isDisabled) return;
135
152
  const actionId = props.triggers?.on_press;
136
- const isSheetAction =
137
- action.type === "navigate_to" && action.destination === "sheet";
138
- const isSheetOpenAction = isSheetAction && action.sheet != null;
139
- const isSheetCloseAction = isSheetAction && action.sheet == null;
153
+ if (action.type === "navigate_to" && action.destination === "sheet") {
154
+ if (action.sheet != null) {
155
+ emitComponentInteraction(getSheetOpenInteractionData(action));
156
+ }
140
157
 
141
- if (isSheetOpenAction) {
142
- emitComponentInteraction(getSheetOpenInteractionData());
143
- } else if (!isSheetCloseAction) {
144
- emitComponentInteraction(getButtonInteractionData());
158
+ onButtonAction(action, actionId);
159
+ return;
145
160
  }
146
161
 
162
+ emitComponentInteraction(getButtonInteractionData());
147
163
  onButtonAction(action, actionId);
148
164
  };
149
165
 
@@ -157,6 +173,10 @@
157
173
  return false;
158
174
  case "navigate_back":
159
175
  return !hideBackButtons;
176
+ case "navigate_to_page":
177
+ return true;
178
+ case "close_workflow":
179
+ return true;
160
180
  case "navigate_to":
161
181
  return action.destination !== "web_paywall_link";
162
182
  default:
@@ -46,6 +46,7 @@
46
46
  size,
47
47
  padding,
48
48
  margin,
49
+ vertical_alignment,
49
50
  border,
50
51
  shape,
51
52
  shadow,
@@ -107,9 +108,18 @@
107
108
  } = getPaywallContext();
108
109
  const packageInfo = getOptionalPackageInfoContext();
109
110
 
111
+ const verticalAlignItems = $derived(
112
+ vertical_alignment === "top"
113
+ ? "flex-start"
114
+ : vertical_alignment === "bottom"
115
+ ? "flex-end"
116
+ : null,
117
+ );
118
+
110
119
  const wrapperStyle = $derived(
111
120
  css({
112
121
  display: "flex",
122
+ ...(verticalAlignItems ? { "align-items": verticalAlignItems } : {}),
113
123
  width: mapSize(size.width),
114
124
  height: mapSize(size.height),
115
125
  margin: mapSpacing(margin),
@@ -128,6 +138,7 @@
128
138
  const placeholderColor = mapTextColor(colorMode, placeholder_color);
129
139
 
130
140
  return css({
141
+ ...(verticalAlignItems ? { height: "auto" } : {}),
131
142
  padding: mapSpacing(padding),
132
143
  ...mapTextColor(colorMode, color),
133
144
  "text-align":
@@ -33,6 +33,11 @@
33
33
  nodeData: Component;
34
34
  }
35
35
 
36
+ type RenderableComponent = Exclude<
37
+ Component,
38
+ { type: "tab" | "fallback_header" }
39
+ >;
40
+
36
41
  const ComponentTypes = {
37
42
  button: ButtonNode,
38
43
  carousel: Carousel,
@@ -60,11 +65,15 @@
60
65
  video: Video,
61
66
  wallet_button: WalletButton,
62
67
  } satisfies {
63
- [key in Component["type"]]: SvelteComponent<
64
- Extract<Component, { type: key }>
68
+ [key in RenderableComponent["type"]]: SvelteComponent<
69
+ Extract<RenderableComponent, { type: key }>
65
70
  >;
66
71
  };
67
72
 
73
+ const isRenderableComponent = (
74
+ component: Component,
75
+ ): component is RenderableComponent => component.type in ComponentTypes;
76
+
68
77
  /**
69
78
  * This function returns the component class and the node data for a given paywall component.
70
79
  * It first checks if the component type is supported and returns the corresponding component class.
@@ -79,7 +88,7 @@
79
88
  ) => [SvelteComponent<Component>, Component] | undefined = (
80
89
  nodeData: Component,
81
90
  ) => {
82
- if (ComponentTypes[nodeData.type]) {
91
+ if (isRenderableComponent(nodeData)) {
83
92
  return [
84
93
  ComponentTypes[nodeData.type] as SvelteComponent<Component>,
85
94
  nodeData,
@@ -91,7 +100,7 @@
91
100
  }
92
101
 
93
102
  const { fallback } = nodeData;
94
- if (fallback && ComponentTypes[fallback?.type]) {
103
+ if (fallback && isRenderableComponent(fallback)) {
95
104
  return [
96
105
  ComponentTypes[fallback.type] as SvelteComponent<Component>,
97
106
  fallback,
@@ -71,6 +71,13 @@
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
+ /** Called when a `close_workflow` button is pressed to dismiss the paywall/workflow. */
80
+ onClose?: () => void;
74
81
  onVisitCustomerCenterClicked?: () => void;
75
82
  onRestorePurchasesClicked?: () => void;
76
83
  onNavigateToUrlClicked?: (url: string) => void;
@@ -128,6 +135,8 @@
128
135
  preferredColorMode,
129
136
  onPurchaseClicked,
130
137
  onBackClicked,
138
+ onNavigateToPage,
139
+ onClose,
131
140
  onVisitCustomerCenterClicked,
132
141
  onRestorePurchasesClicked,
133
142
  onNavigateToUrlClicked,
@@ -239,6 +248,12 @@
239
248
  case "navigate_back":
240
249
  onBackClicked?.();
241
250
  return;
251
+ case "navigate_to_page":
252
+ onNavigateToPage?.(action.page_id);
253
+ return;
254
+ case "close_workflow":
255
+ onClose?.();
256
+ return;
242
257
  case "restore_purchases":
243
258
  onRestorePurchasesClicked?.();
244
259
  return;
@@ -28,6 +28,13 @@ 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
+ /** Called when a `close_workflow` button is pressed to dismiss the paywall/workflow. */
37
+ onClose?: () => void;
31
38
  onVisitCustomerCenterClicked?: () => void;
32
39
  onRestorePurchasesClicked?: () => void;
33
40
  onNavigateToUrlClicked?: (url: string) => void;
@@ -21,6 +21,8 @@
21
21
  actionId: string,
22
22
  ) => void | Promise<void>;
23
23
  onBackClicked?: () => void;
24
+ onNavigateToPage?: (pageId: string) => void;
25
+ onClose?: () => void;
24
26
  containerId?: string;
25
27
  maxContentWidth?: string;
26
28
  variablesPerPackage?: Record<string, VariableDictionary>;
@@ -49,6 +51,8 @@
49
51
  onComponentInteraction,
50
52
  onPurchaseClicked,
51
53
  onBackClicked,
54
+ onNavigateToPage,
55
+ onClose,
52
56
  containerId = "screen-container",
53
57
  maxContentWidth,
54
58
  variablesPerPackage,
@@ -77,6 +81,8 @@
77
81
  {onCompleteWorkflowNavigate}
78
82
  onVisitCustomerCenterClicked={() => {}}
79
83
  {onBackClicked}
84
+ {onNavigateToPage}
85
+ {onClose}
80
86
  onRestorePurchasesClicked={() => {}}
81
87
  onActionTriggered={(actionId: string) => {
82
88
  onActionTriggered?.(actionId);
@@ -16,6 +16,8 @@ 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
+ onClose?: () => void;
19
21
  containerId?: string;
20
22
  maxContentWidth?: string;
21
23
  variablesPerPackage?: Record<string, VariableDictionary>;
@@ -0,0 +1,53 @@
1
+ <script module lang="ts">
2
+ import { defineMeta } from "@storybook/addon-svelte-csf";
3
+ import type { ComponentProps } from "svelte";
4
+
5
+ import Workflow from "./Workflow.svelte";
6
+ import { uiConfigData } from "../../stories/fixtures";
7
+ import { TWO_PAGE_WORKFLOW } from "./fixtures/two-page-workflow";
8
+
9
+ const { Story } = defineMeta({
10
+ title: "Example/Workflow",
11
+ component: Workflow,
12
+ render: template,
13
+ args: {
14
+ workflow: TWO_PAGE_WORKFLOW,
15
+ uiConfig: uiConfigData,
16
+ onClose: () => alert("onClose — workflow dismissed"),
17
+ onExitBack: () => alert("onExitBack — backed out from root page"),
18
+ onPurchaseClicked: (packageId: string, actionId: string) =>
19
+ alert(`onPurchaseClicked — package: ${packageId}, action: ${actionId}`),
20
+ },
21
+ });
22
+ </script>
23
+
24
+ {#snippet template(props: ComponentProps<typeof Workflow>)}
25
+ <div class="viewport-frame">
26
+ <div class="content-wrapper">
27
+ <Workflow {...props} />
28
+ </div>
29
+ </div>
30
+ {/snippet}
31
+
32
+ <Story name="Two-page navigation" />
33
+
34
+ <style>
35
+ .viewport-frame {
36
+ position: fixed;
37
+ inset: 0;
38
+ display: flex;
39
+ flex-direction: column;
40
+ background-color: var(--rc-purchases-ui-bg-color, Canvas);
41
+ color-scheme: light dark;
42
+ overflow: hidden;
43
+ }
44
+
45
+ .content-wrapper {
46
+ width: 100%;
47
+ display: flex;
48
+ flex: 1 1 auto;
49
+ flex-direction: column;
50
+ min-height: 0;
51
+ overflow: hidden;
52
+ }
53
+ </style>
@@ -0,0 +1,19 @@
1
+ import Workflow from "./Workflow.svelte";
2
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
3
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
+ $$bindings?: Bindings;
5
+ } & Exports;
6
+ (internal: unknown, props: {
7
+ $$events?: Events;
8
+ $$slots?: Slots;
9
+ }): Exports & {
10
+ $set?: any;
11
+ $on?: any;
12
+ };
13
+ z_$$bindings?: Bindings;
14
+ }
15
+ declare const Workflow: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
16
+ [evt: string]: CustomEvent<any>;
17
+ }, {}, {}, string>;
18
+ type Workflow = InstanceType<typeof Workflow>;
19
+ export default Workflow;
@@ -0,0 +1,130 @@
1
+ <script lang="ts">
2
+ import Screen from "./Screen.svelte";
3
+ import type { CompleteWorkflowNavigateArgs } from "../../types/components/button";
4
+ import type { ColorScheme } from "../../types/colors";
5
+ import type { InitialInputSelections } from "../../stores/inputValidation";
6
+ import type { OnComponentInteraction } from "../../types/paywall-component-interaction";
7
+ import type { VariableDictionary } from "../../types/variables";
8
+ import type { WalletButtonRender } from "../../types/wallet";
9
+ import type { UIConfig } from "../../types/ui-config";
10
+ import type { ReservedAttribute } from "../../types/components/input-text";
11
+ import type { WorkflowNavData } from "../../types/workflow-nav";
12
+
13
+ interface Props {
14
+ workflow: WorkflowNavData;
15
+ uiConfig: UIConfig;
16
+ selectedLocale?: string;
17
+ globalVariables?: VariableDictionary;
18
+ variablesPerPackage?: Record<string, VariableDictionary>;
19
+ maxContentWidth?: string;
20
+ initialInputSelections?: InitialInputSelections;
21
+ containerId?: string;
22
+ walletButtonRender?: WalletButtonRender;
23
+ safeAreaFallbackColor?: ColorScheme | null;
24
+ onPurchaseClicked?: (
25
+ packageId: string,
26
+ actionId: string,
27
+ ) => void | Promise<void>;
28
+ onActionTriggered?: (actionId: string) => void;
29
+ onInputChanged?: (
30
+ fieldId: string,
31
+ value: string,
32
+ actionId?: string,
33
+ ) => void;
34
+ onReservedAttributeChanged?: (
35
+ reservedAttribute: ReservedAttribute,
36
+ value: string,
37
+ ) => void;
38
+ onCompleteWorkflowNavigate?: (
39
+ args: CompleteWorkflowNavigateArgs,
40
+ ) => void | Promise<void>;
41
+ onComponentInteraction?: OnComponentInteraction;
42
+ onClose?: () => void;
43
+ onExitBack?: () => void;
44
+ }
45
+
46
+ const {
47
+ workflow,
48
+ uiConfig,
49
+ selectedLocale,
50
+ globalVariables,
51
+ variablesPerPackage,
52
+ maxContentWidth,
53
+ initialInputSelections,
54
+ containerId,
55
+ walletButtonRender,
56
+ safeAreaFallbackColor,
57
+ onPurchaseClicked,
58
+ onActionTriggered,
59
+ onInputChanged,
60
+ onReservedAttributeChanged,
61
+ onCompleteWorkflowNavigate,
62
+ onComponentInteraction,
63
+ onClose,
64
+ onExitBack,
65
+ }: Props = $props();
66
+
67
+ // ── Nav stack ─────────────────────────────────────────────────────────────
68
+ // The stack always has at least the initial page. The current page is the
69
+ // last entry. navigate_to_page pushes, navigate_back pops.
70
+ //
71
+ // We key the reset off workflow.initial_page_id (a stable string) rather
72
+ // than workflow object identity, so hosts that recompute the prop object on
73
+ // unrelated reactive updates don't accidentally snap the user back to the
74
+ // first page mid-flow.
75
+ let navStack = $state<string[]>([]);
76
+ let trackedInitialPageId = $state<string>();
77
+
78
+ const currentPageId = $derived(
79
+ navStack[navStack.length - 1] ?? workflow.initial_page_id,
80
+ );
81
+ const currentScreen = $derived(workflow.pages[currentPageId]);
82
+
83
+ $effect.pre(() => {
84
+ if (trackedInitialPageId !== workflow.initial_page_id) {
85
+ trackedInitialPageId = workflow.initial_page_id;
86
+ navStack = [workflow.initial_page_id];
87
+ }
88
+ });
89
+
90
+ function handleNavigateToPage(pageId: string) {
91
+ if (!workflow.pages[pageId]) {
92
+ console.warn(`[Workflow] navigate_to_page: unknown page_id "${pageId}"`);
93
+ return;
94
+ }
95
+ navStack = [...navStack, pageId];
96
+ }
97
+
98
+ function handleBackClicked() {
99
+ if (navStack.length > 1) {
100
+ navStack = navStack.slice(0, -1);
101
+ } else {
102
+ // Already at root — bubble up to the host.
103
+ onExitBack?.();
104
+ }
105
+ }
106
+ </script>
107
+
108
+ {#key currentPageId}
109
+ <Screen
110
+ paywallComponents={currentScreen}
111
+ {uiConfig}
112
+ {selectedLocale}
113
+ {globalVariables}
114
+ {variablesPerPackage}
115
+ {maxContentWidth}
116
+ {initialInputSelections}
117
+ {containerId}
118
+ {walletButtonRender}
119
+ {safeAreaFallbackColor}
120
+ {onPurchaseClicked}
121
+ {onActionTriggered}
122
+ {onInputChanged}
123
+ {onReservedAttributeChanged}
124
+ {onCompleteWorkflowNavigate}
125
+ {onComponentInteraction}
126
+ onNavigateToPage={handleNavigateToPage}
127
+ onBackClicked={handleBackClicked}
128
+ {onClose}
129
+ />
130
+ {/key}
@@ -0,0 +1,32 @@
1
+ import type { CompleteWorkflowNavigateArgs } from "../../types/components/button";
2
+ import type { ColorScheme } from "../../types/colors";
3
+ import type { InitialInputSelections } from "../../stores/inputValidation";
4
+ import type { OnComponentInteraction } from "../../types/paywall-component-interaction";
5
+ import type { VariableDictionary } from "../../types/variables";
6
+ import type { WalletButtonRender } from "../../types/wallet";
7
+ import type { UIConfig } from "../../types/ui-config";
8
+ import type { ReservedAttribute } from "../../types/components/input-text";
9
+ import type { WorkflowNavData } from "../../types/workflow-nav";
10
+ interface Props {
11
+ workflow: WorkflowNavData;
12
+ uiConfig: UIConfig;
13
+ selectedLocale?: string;
14
+ globalVariables?: VariableDictionary;
15
+ variablesPerPackage?: Record<string, VariableDictionary>;
16
+ maxContentWidth?: string;
17
+ initialInputSelections?: InitialInputSelections;
18
+ containerId?: string;
19
+ walletButtonRender?: WalletButtonRender;
20
+ safeAreaFallbackColor?: ColorScheme | null;
21
+ onPurchaseClicked?: (packageId: string, actionId: string) => void | Promise<void>;
22
+ onActionTriggered?: (actionId: string) => void;
23
+ onInputChanged?: (fieldId: string, value: string, actionId?: string) => void;
24
+ onReservedAttributeChanged?: (reservedAttribute: ReservedAttribute, value: string) => void;
25
+ onCompleteWorkflowNavigate?: (args: CompleteWorkflowNavigateArgs) => void | Promise<void>;
26
+ onComponentInteraction?: OnComponentInteraction;
27
+ onClose?: () => void;
28
+ onExitBack?: () => void;
29
+ }
30
+ declare const Workflow: import("svelte").Component<Props, {}, "">;
31
+ type Workflow = ReturnType<typeof Workflow>;
32
+ export default Workflow;
@@ -0,0 +1,12 @@
1
+ import type { WorkflowNavData } from "../../../types/workflow-nav";
2
+ /**
3
+ * Two-page workflow fixture that exercises navigate_to_page, navigate_back,
4
+ * and close_workflow button actions via the Workflow component.
5
+ *
6
+ * Page layout:
7
+ * "rcpaywall_welcome" — welcome screen with a "Next →" navigate_to_page button
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
+ */
12
+ export declare const TWO_PAGE_WORKFLOW: WorkflowNavData;
@@ -0,0 +1,378 @@
1
+ /**
2
+ * Two-page workflow fixture that exercises navigate_to_page, navigate_back,
3
+ * and close_workflow button actions via the Workflow component.
4
+ *
5
+ * Page layout:
6
+ * "rcpaywall_welcome" — welcome screen with a "Next →" navigate_to_page button
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
+ */
11
+ export const TWO_PAGE_WORKFLOW = {
12
+ initial_page_id: "rcpaywall_welcome",
13
+ pages: {
14
+ rcpaywall_welcome: {
15
+ id: "rcpaywall_welcome",
16
+ default_locale: "en_US",
17
+ name: "Welcome",
18
+ template_name: "stack",
19
+ revision: 1,
20
+ asset_base_url: "https://assets.pawwalls.com",
21
+ config: {},
22
+ localized_strings: {},
23
+ localized_strings_by_tier: {},
24
+ offering_id: null,
25
+ components_localizations: {
26
+ en_US: {
27
+ welcome_title: "Welcome",
28
+ welcome_body: "This is page 1 of 2. Tap the button below to navigate to page 2.",
29
+ next_btn: "Next →",
30
+ },
31
+ },
32
+ components_config: {
33
+ base: {
34
+ background: {
35
+ type: "color",
36
+ value: {
37
+ light: { type: "hex", value: "#f0f4ffff" },
38
+ dark: { type: "hex", value: "#1a1f2eff" },
39
+ },
40
+ },
41
+ stack: {
42
+ type: "stack",
43
+ id: "welcome_root",
44
+ name: "Root",
45
+ size: { width: { type: "fill" }, height: { type: "fill" } },
46
+ dimension: {
47
+ type: "vertical",
48
+ alignment: "center",
49
+ distribution: "start",
50
+ },
51
+ spacing: 0,
52
+ margin: { top: 0, bottom: 0, leading: 0, trailing: 0 },
53
+ padding: { top: 0, bottom: 0, leading: 0, trailing: 0 },
54
+ background: null,
55
+ background_color: null,
56
+ badge: null,
57
+ border: null,
58
+ shadow: null,
59
+ shape: null,
60
+ components: [
61
+ // ── Title ────────────────────────────────────────────────
62
+ {
63
+ type: "text",
64
+ id: "welcome_title",
65
+ name: "Title",
66
+ text_lid: "welcome_title",
67
+ font_size: "heading_xl",
68
+ font_weight: "bold",
69
+ horizontal_alignment: "center",
70
+ size: { width: { type: "fill" }, height: { type: "fit" } },
71
+ margin: { top: 80, bottom: 16, leading: 24, trailing: 24 },
72
+ padding: { top: 0, bottom: 0, leading: 0, trailing: 0 },
73
+ color: {
74
+ light: { type: "hex", value: "#1a1f2eff" },
75
+ dark: { type: "hex", value: "#f0f4ffff" },
76
+ },
77
+ background_color: {
78
+ light: { type: "hex", value: "transparent" },
79
+ dark: { type: "hex", value: "transparent" },
80
+ },
81
+ },
82
+ // ── Body ─────────────────────────────────────────────────
83
+ {
84
+ type: "text",
85
+ id: "welcome_body",
86
+ name: "Body",
87
+ text_lid: "welcome_body",
88
+ font_size: "body_m",
89
+ font_weight: "regular",
90
+ horizontal_alignment: "center",
91
+ size: { width: { type: "fill" }, height: { type: "fit" } },
92
+ margin: { top: 0, bottom: 48, leading: 24, trailing: 24 },
93
+ padding: { top: 0, bottom: 0, leading: 0, trailing: 0 },
94
+ color: {
95
+ light: { type: "hex", value: "#555577ff" },
96
+ dark: { type: "hex", value: "#aaaaccff" },
97
+ },
98
+ background_color: {
99
+ light: { type: "hex", value: "transparent" },
100
+ dark: { type: "hex", value: "transparent" },
101
+ },
102
+ },
103
+ // ── navigate_to_page button ───────────────────────────
104
+ {
105
+ type: "button",
106
+ id: "next_btn",
107
+ name: "Next",
108
+ action: {
109
+ type: "navigate_to_page",
110
+ page_id: "rcpaywall_details",
111
+ },
112
+ stack: {
113
+ type: "stack",
114
+ id: "next_btn_stack",
115
+ name: "Next button stack",
116
+ size: { width: { type: "fill" }, height: { type: "fit" } },
117
+ dimension: {
118
+ type: "horizontal",
119
+ alignment: "center",
120
+ distribution: "center",
121
+ },
122
+ spacing: 0,
123
+ margin: { top: 0, bottom: 0, leading: 24, trailing: 24 },
124
+ padding: { top: 16, bottom: 16, leading: 24, trailing: 24 },
125
+ background: {
126
+ type: "color",
127
+ value: {
128
+ light: { type: "hex", value: "#3366ffff" },
129
+ dark: { type: "hex", value: "#4d7fffff" },
130
+ },
131
+ },
132
+ background_color: null,
133
+ badge: null,
134
+ border: null,
135
+ shadow: null,
136
+ shape: { type: "pill" },
137
+ components: [
138
+ {
139
+ type: "text",
140
+ id: "next_btn_text",
141
+ name: "next_btn_text",
142
+ text_lid: "next_btn",
143
+ font_size: "body_m",
144
+ font_weight: "medium",
145
+ horizontal_alignment: "center",
146
+ size: { width: { type: "fit" }, height: { type: "fit" } },
147
+ margin: { top: 0, bottom: 0, leading: 0, trailing: 0 },
148
+ padding: { top: 0, bottom: 0, leading: 0, trailing: 0 },
149
+ color: {
150
+ light: { type: "hex", value: "#ffffffff" },
151
+ dark: { type: "hex", value: "#ffffffff" },
152
+ },
153
+ background_color: {
154
+ light: { type: "hex", value: "transparent" },
155
+ dark: { type: "hex", value: "transparent" },
156
+ },
157
+ },
158
+ ],
159
+ },
160
+ },
161
+ ],
162
+ },
163
+ },
164
+ },
165
+ },
166
+ rcpaywall_details: {
167
+ id: "rcpaywall_details",
168
+ default_locale: "en_US",
169
+ name: "Details",
170
+ template_name: "stack",
171
+ revision: 1,
172
+ asset_base_url: "https://assets.pawwalls.com",
173
+ config: {},
174
+ localized_strings: {},
175
+ localized_strings_by_tier: {},
176
+ offering_id: null,
177
+ components_localizations: {
178
+ en_US: {
179
+ details_title: "Details",
180
+ details_body: "This is page 2 of 2. Use the buttons below to navigate back or close the workflow entirely.",
181
+ back_btn: "← Back",
182
+ close_btn: "✕ Close",
183
+ },
184
+ },
185
+ components_config: {
186
+ base: {
187
+ background: {
188
+ type: "color",
189
+ value: {
190
+ light: { type: "hex", value: "#fff4f0ff" },
191
+ dark: { type: "hex", value: "#2e1a1aff" },
192
+ },
193
+ },
194
+ stack: {
195
+ type: "stack",
196
+ id: "details_root",
197
+ name: "Root",
198
+ size: { width: { type: "fill" }, height: { type: "fill" } },
199
+ dimension: {
200
+ type: "vertical",
201
+ alignment: "center",
202
+ distribution: "start",
203
+ },
204
+ spacing: 0,
205
+ margin: { top: 0, bottom: 0, leading: 0, trailing: 0 },
206
+ padding: { top: 0, bottom: 0, leading: 0, trailing: 0 },
207
+ background: null,
208
+ background_color: null,
209
+ badge: null,
210
+ border: null,
211
+ shadow: null,
212
+ shape: null,
213
+ components: [
214
+ // ── Title ────────────────────────────────────────────────
215
+ {
216
+ type: "text",
217
+ id: "details_title",
218
+ name: "Title",
219
+ text_lid: "details_title",
220
+ font_size: "heading_xl",
221
+ font_weight: "bold",
222
+ horizontal_alignment: "center",
223
+ size: { width: { type: "fill" }, height: { type: "fit" } },
224
+ margin: { top: 80, bottom: 16, leading: 24, trailing: 24 },
225
+ padding: { top: 0, bottom: 0, leading: 0, trailing: 0 },
226
+ color: {
227
+ light: { type: "hex", value: "#2e1a1aff" },
228
+ dark: { type: "hex", value: "#fff4f0ff" },
229
+ },
230
+ background_color: {
231
+ light: { type: "hex", value: "transparent" },
232
+ dark: { type: "hex", value: "transparent" },
233
+ },
234
+ },
235
+ // ── Body ─────────────────────────────────────────────────
236
+ {
237
+ type: "text",
238
+ id: "details_body",
239
+ name: "Body",
240
+ text_lid: "details_body",
241
+ font_size: "body_m",
242
+ font_weight: "regular",
243
+ horizontal_alignment: "center",
244
+ size: { width: { type: "fill" }, height: { type: "fit" } },
245
+ margin: { top: 0, bottom: 48, leading: 24, trailing: 24 },
246
+ padding: { top: 0, bottom: 0, leading: 0, trailing: 0 },
247
+ color: {
248
+ light: { type: "hex", value: "#775555ff" },
249
+ dark: { type: "hex", value: "#ccaaaaff" },
250
+ },
251
+ background_color: {
252
+ light: { type: "hex", value: "transparent" },
253
+ dark: { type: "hex", value: "transparent" },
254
+ },
255
+ },
256
+ // ── navigate_back button ──────────────────────────────
257
+ {
258
+ type: "button",
259
+ id: "back_btn",
260
+ name: "Back",
261
+ action: { type: "navigate_back" },
262
+ stack: {
263
+ type: "stack",
264
+ id: "back_btn_stack",
265
+ name: "Back button stack",
266
+ size: { width: { type: "fill" }, height: { type: "fit" } },
267
+ dimension: {
268
+ type: "horizontal",
269
+ alignment: "center",
270
+ distribution: "center",
271
+ },
272
+ spacing: 0,
273
+ margin: { top: 0, bottom: 12, leading: 24, trailing: 24 },
274
+ padding: { top: 16, bottom: 16, leading: 24, trailing: 24 },
275
+ background: {
276
+ type: "color",
277
+ value: {
278
+ light: { type: "hex", value: "transparent" },
279
+ dark: { type: "hex", value: "transparent" },
280
+ },
281
+ },
282
+ background_color: null,
283
+ badge: null,
284
+ border: {
285
+ width: 1.5,
286
+ color: {
287
+ light: { type: "hex", value: "#3366ffff" },
288
+ dark: { type: "hex", value: "#4d7fffff" },
289
+ },
290
+ },
291
+ shadow: null,
292
+ shape: { type: "pill" },
293
+ components: [
294
+ {
295
+ type: "text",
296
+ id: "back_btn_text",
297
+ name: "back_btn_text",
298
+ text_lid: "back_btn",
299
+ font_size: "body_m",
300
+ font_weight: "medium",
301
+ horizontal_alignment: "center",
302
+ size: { width: { type: "fit" }, height: { type: "fit" } },
303
+ margin: { top: 0, bottom: 0, leading: 0, trailing: 0 },
304
+ padding: { top: 0, bottom: 0, leading: 0, trailing: 0 },
305
+ color: {
306
+ light: { type: "hex", value: "#3366ffff" },
307
+ dark: { type: "hex", value: "#4d7fffff" },
308
+ },
309
+ background_color: {
310
+ light: { type: "hex", value: "transparent" },
311
+ dark: { type: "hex", value: "transparent" },
312
+ },
313
+ },
314
+ ],
315
+ },
316
+ },
317
+ // ── close button ──────────────────────────────────────
318
+ {
319
+ type: "button",
320
+ id: "close_btn",
321
+ name: "Close",
322
+ action: { type: "close_workflow" },
323
+ stack: {
324
+ type: "stack",
325
+ id: "close_btn_stack",
326
+ name: "Close button stack",
327
+ size: { width: { type: "fill" }, height: { type: "fit" } },
328
+ dimension: {
329
+ type: "horizontal",
330
+ alignment: "center",
331
+ distribution: "center",
332
+ },
333
+ spacing: 0,
334
+ margin: { top: 0, bottom: 40, leading: 24, trailing: 24 },
335
+ padding: { top: 16, bottom: 16, leading: 24, trailing: 24 },
336
+ background: {
337
+ type: "color",
338
+ value: {
339
+ light: { type: "hex", value: "#ff3b30ff" },
340
+ dark: { type: "hex", value: "#ff453aff" },
341
+ },
342
+ },
343
+ background_color: null,
344
+ badge: null,
345
+ border: null,
346
+ shadow: null,
347
+ shape: { type: "pill" },
348
+ components: [
349
+ {
350
+ type: "text",
351
+ id: "close_btn_text",
352
+ name: "close_btn_text",
353
+ text_lid: "close_btn",
354
+ font_size: "body_m",
355
+ font_weight: "medium",
356
+ horizontal_alignment: "center",
357
+ size: { width: { type: "fit" }, height: { type: "fit" } },
358
+ margin: { top: 0, bottom: 0, leading: 0, trailing: 0 },
359
+ padding: { top: 0, bottom: 0, leading: 0, trailing: 0 },
360
+ color: {
361
+ light: { type: "hex", value: "#ffffffff" },
362
+ dark: { type: "hex", value: "#ffffffff" },
363
+ },
364
+ background_color: {
365
+ light: { type: "hex", value: "transparent" },
366
+ dark: { type: "hex", value: "transparent" },
367
+ },
368
+ },
369
+ ],
370
+ },
371
+ },
372
+ ],
373
+ },
374
+ },
375
+ },
376
+ },
377
+ },
378
+ };
package/dist/index.d.ts CHANGED
@@ -13,9 +13,12 @@ export { default as Stack } from "./components/stack/Stack.svelte";
13
13
  export { default as Text } from "./components/text/Text.svelte";
14
14
  export { default as Timeline } from "./components/timeline/Timeline.svelte";
15
15
  export { default as Screen } from "./components/workflows/Screen.svelte";
16
+ export { default as Workflow } from "./components/workflows/Workflow.svelte";
16
17
  export { default as Video } from "./components/video/Video.svelte";
17
18
  export * from "./types";
18
19
  export { type PaywallData } from "./types/paywall";
20
+ export { type WorkflowNavData, workflowDataToNavData, } from "./types/workflow-nav";
21
+ export { type WorkflowData, type WorkflowStep } from "./types/workflow";
19
22
  export { type InitialInputSelections } from "./stores/inputValidation";
20
23
  export { type UIConfig } from "./types/ui-config";
21
24
  export { type WalletButtonRender, type WalletButtonTheme, } from "./types/wallet";
package/dist/index.js CHANGED
@@ -14,9 +14,12 @@ export { default as Stack } from "./components/stack/Stack.svelte";
14
14
  export { default as Text } from "./components/text/Text.svelte";
15
15
  export { default as Timeline } from "./components/timeline/Timeline.svelte";
16
16
  export { default as Screen } from "./components/workflows/Screen.svelte";
17
+ export { default as Workflow } from "./components/workflows/Workflow.svelte";
17
18
  export { default as Video } from "./components/video/Video.svelte";
18
19
  export * from "./types";
19
20
  export {} from "./types/paywall";
21
+ export { workflowDataToNavData, } from "./types/workflow-nav";
22
+ export {} from "./types/workflow";
20
23
  export {} from "./stores/inputValidation";
21
24
  export {} from "./types/ui-config";
22
25
  export {} from "./types/wallet";
@@ -1,6 +1,7 @@
1
1
  import type { ButtonProps } from "./components/button";
2
2
  import type { CarouselProps } from "./components/carousel";
3
3
  import type { CountdownProps } from "./components/countdown";
4
+ import type { FallbackHeaderProps } from "./components/fallback-header";
4
5
  import type { FooterProps } from "./components/footer";
5
6
  import type { IconProps } from "./components/icon";
6
7
  import type { ImageProps } from "./components/image";
@@ -18,4 +19,4 @@ import type { WalletButtonProps } from "./components/wallet-button";
18
19
  import type { SkeletonLoaderProps } from "./components/skeleton-loader-props";
19
20
  import type { ExpressPurchaseButtonProps } from "./components/express-purchase-button-props";
20
21
  import type { HeaderProps } from "./components/header";
21
- export type Component = ButtonProps | CarouselProps | CountdownProps | ExpressPurchaseButtonProps | FooterProps | HeaderProps | IconProps | ImageProps | InputMultipleChoiceProps | InputOptionProps | InputSingleChoiceProps | InputTextProps | PackageProps | PurchaseButtonProps | RedemptionButtonProps | SkeletonLoaderProps | StackProps | TabControlButtonProps | TabControlToggleProps | TabControlProps | TabProps | TabsProps | TextNodeProps | TimelineProps | WalletButtonProps | VideoProps;
22
+ export type Component = ButtonProps | CarouselProps | CountdownProps | ExpressPurchaseButtonProps | FallbackHeaderProps | FooterProps | HeaderProps | IconProps | ImageProps | InputMultipleChoiceProps | InputOptionProps | InputSingleChoiceProps | InputTextProps | PackageProps | PurchaseButtonProps | RedemptionButtonProps | SkeletonLoaderProps | StackProps | TabControlButtonProps | TabControlToggleProps | TabControlProps | TabProps | TabsProps | TextNodeProps | TimelineProps | WalletButtonProps | VideoProps;
@@ -11,6 +11,13 @@ 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
+ interface CloseAction {
19
+ type: "close_workflow";
20
+ }
14
21
  interface NavigateToAction {
15
22
  type: "navigate_to";
16
23
  destination: "customer_center" | "offer_code" | "screen_redirect";
@@ -53,7 +60,7 @@ interface CompleteWorkflowAction {
53
60
  };
54
61
  url_query_params?: CompleteWorkflowUrlQueryParams;
55
62
  }
56
- export type Action = WorkflowAction | RestorePurchasesAction | NavigateBackAction | NavigateToAction | NavigateToSheetAction | NavigateToWebPurchase | NavigateToUrlAction | CompleteWorkflowAction;
63
+ export type Action = WorkflowAction | RestorePurchasesAction | NavigateBackAction | NavigateToPageAction | CloseAction | NavigateToAction | NavigateToSheetAction | NavigateToWebPurchase | NavigateToUrlAction | CompleteWorkflowAction;
57
64
  export interface ButtonProps extends BaseComponent {
58
65
  type: "button";
59
66
  action: Action;
@@ -0,0 +1,9 @@
1
+ import type { BaseComponent } from "../base";
2
+ /**
3
+ * Sentinel component type emitted by the backend when a component has no
4
+ * known renderer on this client. `Node` skips rendering and falls back to
5
+ * the component's `fallback` child (if present) instead.
6
+ */
7
+ export interface FallbackHeaderProps extends BaseComponent {
8
+ type: "fallback_header";
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,5 @@
1
1
  import type { BorderType, ShapeType, ShadowType, SizeType, Spacing, TextAlignments } from "..";
2
+ import type { VerticalAlignment } from "../alignment";
2
3
  import type { BaseComponent } from "../base";
3
4
  import type { ColorGradientScheme } from "../colors";
4
5
  import type { Overrides } from "../overrides";
@@ -17,6 +18,7 @@ export interface InputTextProps extends BaseComponent {
17
18
  size: SizeType;
18
19
  padding: Spacing;
19
20
  margin: Spacing;
21
+ vertical_alignment?: VerticalAlignment | null;
20
22
  border?: BorderType | null;
21
23
  shape?: ShapeType | null;
22
24
  shadow?: ShadowType | null;
@@ -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" | "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" | "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";
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;
@@ -0,0 +1,30 @@
1
+ import type { WorkflowData, WorkflowScreen } from "./workflow";
2
+ /**
3
+ * A self-contained multi-page workflow payload for use with the Workflow
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.
8
+ */
9
+ export interface WorkflowNavData {
10
+ /** ID of the first page to display. Must be a key in `pages`. */
11
+ initial_page_id: string;
12
+ /**
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.
15
+ */
16
+ pages: Record<string, WorkflowScreen>;
17
+ }
18
+ /**
19
+ * Map a full `WorkflowData` SDK response to the `WorkflowNavData` shape
20
+ * consumed by the `Workflow` component.
21
+ *
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).
25
+ * - `initial_page_id` is resolved by looking up the screen_id of the initial
26
+ * step, since `initial_step_id` is a step ID, not a screen ID.
27
+ *
28
+ * Returns `null` if the initial step or its screen cannot be resolved.
29
+ */
30
+ export declare function workflowDataToNavData(data: WorkflowData): WorkflowNavData | null;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Map a full `WorkflowData` SDK response to the `WorkflowNavData` shape
3
+ * consumed by the `Workflow` component.
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).
8
+ * - `initial_page_id` is resolved by looking up the screen_id of the initial
9
+ * step, since `initial_step_id` is a step ID, not a screen ID.
10
+ *
11
+ * Returns `null` if the initial step or its screen cannot be resolved.
12
+ */
13
+ export function workflowDataToNavData(data) {
14
+ const initialStep = data.steps[data.initial_step_id];
15
+ if (!initialStep) {
16
+ console.warn(`[workflowDataToNavData] initial_step_id "${data.initial_step_id}" not found in steps`);
17
+ return null;
18
+ }
19
+ const initialPageId = initialStep.screen_id;
20
+ if (!initialPageId) {
21
+ console.warn(`[workflowDataToNavData] initial step "${data.initial_step_id}" has no screen_id (logic/experiment step?)`);
22
+ return null;
23
+ }
24
+ if (!data.screens[initialPageId]) {
25
+ console.warn(`[workflowDataToNavData] screen_id "${initialPageId}" not found in screens`);
26
+ return null;
27
+ }
28
+ return {
29
+ initial_page_id: initialPageId,
30
+ // Shared by reference intentionally — screens are never mutated after
31
+ // WorkflowData is received from the SDK, so a copy would be wasteful.
32
+ pages: data.screens,
33
+ };
34
+ }
@@ -13,3 +13,32 @@ export interface WorkflowScreen extends PaywallData {
13
13
  revision: number;
14
14
  template_name: string;
15
15
  }
16
+ /**
17
+ * A single step in a workflow, referencing a screen by ID.
18
+ * Aligns with the SDK response shape from khepri.
19
+ */
20
+ export interface WorkflowStep {
21
+ id: string;
22
+ screen_id?: string;
23
+ type: string;
24
+ param_values: Record<string, unknown>;
25
+ trigger_actions: Record<string, unknown>;
26
+ triggers: Record<string, unknown>;
27
+ outputs: Record<string, unknown>;
28
+ metadata: Record<string, unknown> | null;
29
+ }
30
+ /**
31
+ * The full workflow payload returned by the RevenueCat SDK.
32
+ * Aligns with rc-workflows' WorkflowData type — rc-workflows imports
33
+ * WorkflowScreen from this package and can import WorkflowData here too.
34
+ */
35
+ export interface WorkflowData {
36
+ id: string;
37
+ display_name: string;
38
+ initial_step_id: string;
39
+ steps: Record<string, WorkflowStep>;
40
+ screens: Record<string, WorkflowScreen>;
41
+ ui_config: Record<string, unknown>;
42
+ content_max_width: number | null;
43
+ metadata?: Record<string, unknown> | null;
44
+ }
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.5.2",
5
+ "version": "4.7.0",
6
6
  "author": {
7
7
  "name": "RevenueCat, Inc."
8
8
  },