@revenuecat/purchases-ui-js 3.0.1 → 3.2.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,7 @@
13
13
  import InputMultipleChoice from "../options/InputMultipleChoice.svelte";
14
14
  import InputOption from "../options/InputOption.svelte";
15
15
  import InputSingleChoice from "../options/InputSingleChoice.svelte";
16
+ import RedemptionButton from "../redemption-button/RedemptionButton.svelte";
16
17
  import TextNode from "../text/TextNode.svelte";
17
18
  import type { Component } from "../../types/component";
18
19
  import type { Component as SvelteComponent } from "svelte";
@@ -39,6 +40,7 @@
39
40
  input_single_choice: InputSingleChoice,
40
41
  package: Package,
41
42
  purchase_button: PurchaseButton,
43
+ redemption_button: RedemptionButton,
42
44
  stack: Stack,
43
45
  tab_control_button: TabControlButton,
44
46
  tab_control_toggle: TabControlToggle,
@@ -30,6 +30,13 @@
30
30
  paywallData: PaywallData;
31
31
  selectedLocale?: string;
32
32
  variablesPerPackage?: Record<string, VariableDictionary>;
33
+ /**
34
+ * Global variables that are available independent of package selection.
35
+ * Useful for success screens, confirmation flows, and other contexts where variables
36
+ * aren't tied to a specific package. Package-specific variables from variablesPerPackage
37
+ * will override global variables when both are present.
38
+ */
39
+ globalVariables?: VariableDictionary;
33
40
  infoPerPackage?: Record<string, PackageInfo>;
34
41
  uiConfig: UIConfig;
35
42
  preferredColorMode?: ColorMode;
@@ -56,6 +63,7 @@
56
63
  paywallData,
57
64
  selectedLocale,
58
65
  variablesPerPackage = {},
66
+ globalVariables = {},
59
67
  infoPerPackage = {},
60
68
  preferredColorMode,
61
69
  onPurchaseClicked,
@@ -142,10 +150,13 @@
142
150
  uiConfig,
143
151
  });
144
152
 
145
- const variables: VariablesStore = derived(
146
- selectedPackageId,
147
- (packageId) => variablesPerPackage[packageId || ""],
148
- );
153
+ const variables: VariablesStore = derived(selectedPackageId, (packageId) => {
154
+ const packageVars = variablesPerPackage[packageId || ""];
155
+ return {
156
+ ...globalVariables,
157
+ ...packageVars,
158
+ };
159
+ });
149
160
 
150
161
  setVariablesContext(variables);
151
162
 
@@ -6,6 +6,13 @@ interface Props {
6
6
  paywallData: PaywallData;
7
7
  selectedLocale?: string;
8
8
  variablesPerPackage?: Record<string, VariableDictionary>;
9
+ /**
10
+ * Global variables that are available independent of package selection.
11
+ * Useful for success screens, confirmation flows, and other contexts where variables
12
+ * aren't tied to a specific package. Package-specific variables from variablesPerPackage
13
+ * will override global variables when both are present.
14
+ */
15
+ globalVariables?: VariableDictionary;
9
16
  infoPerPackage?: Record<string, PackageInfo>;
10
17
  uiConfig: UIConfig;
11
18
  preferredColorMode?: ColorMode;
@@ -0,0 +1,90 @@
1
+ <script module lang="ts">
2
+ import RedemptionButton from "./RedemptionButton.svelte";
3
+ import { componentDecorator } from "../../stories/component-decorator";
4
+ import { localizationDecorator } from "../../stories/localization-decorator";
5
+ import { variablesDecorator } from "../../stories/variables-decorator";
6
+ import type { RedemptionButtonProps } from "../../types/components/redemption-button";
7
+ import type { StackProps } from "../../types/components/stack";
8
+ import type { TextNodeProps } from "../../types/components/text";
9
+ import { DEFAULT_TEXT_COLOR } from "../../utils/constants";
10
+ import { defineMeta } from "@storybook/addon-svelte-csf";
11
+
12
+ const defaultLocale = "en_US";
13
+
14
+ const textProps: TextNodeProps = {
15
+ type: "text",
16
+ id: "redeem-text",
17
+ name: "Redeem text",
18
+ text_lid: "redeem_text",
19
+ font_size: "body_m",
20
+ font_weight: "medium",
21
+ horizontal_alignment: "center",
22
+ size: {
23
+ width: { type: "fit" },
24
+ height: { type: "fit" },
25
+ },
26
+ margin: { top: 0, trailing: 0, bottom: 0, leading: 0 },
27
+ padding: { top: 0, trailing: 0, bottom: 0, leading: 0 },
28
+ color: {
29
+ dark: { type: "hex", value: DEFAULT_TEXT_COLOR },
30
+ light: { type: "hex", value: "#ffffff" },
31
+ },
32
+ background_color: null,
33
+ };
34
+
35
+ const stackProps: StackProps = {
36
+ type: "stack",
37
+ id: "redemption-stack",
38
+ name: "Redemption Stack",
39
+ components: [textProps],
40
+ size: { width: { type: "fill" }, height: { type: "fit" } },
41
+ dimension: {
42
+ type: "horizontal",
43
+ alignment: "center",
44
+ distribution: "center",
45
+ },
46
+ spacing: 8,
47
+ margin: { top: 0, trailing: 0, bottom: 0, leading: 0 },
48
+ padding: { top: 12, trailing: 16, bottom: 12, leading: 16 },
49
+ background_color: null,
50
+ background: {
51
+ type: "color",
52
+ value: {
53
+ dark: { type: "hex", value: "#000000" },
54
+ light: { type: "hex", value: "#000000" },
55
+ },
56
+ },
57
+ border: null,
58
+ shape: { type: "pill" },
59
+ shadow: null,
60
+ badge: null,
61
+ };
62
+
63
+ const { Story } = defineMeta({
64
+ title: "Components/RedemptionButton",
65
+ component: RedemptionButton,
66
+ decorators: [
67
+ componentDecorator(),
68
+ localizationDecorator({
69
+ defaultLocale,
70
+ localizations: {
71
+ [defaultLocale]: {
72
+ redeem_text: "Redeem your trial",
73
+ },
74
+ },
75
+ }),
76
+ variablesDecorator({
77
+ "success.redemption_url": "myapp://redeem?token=abc123xyz",
78
+ }),
79
+ ],
80
+ args: {
81
+ type: "redemption_button",
82
+ id: "redemption-button",
83
+ name: "Redemption Button",
84
+ stack: stackProps,
85
+ transition: null,
86
+ } satisfies RedemptionButtonProps,
87
+ });
88
+ </script>
89
+
90
+ <Story name="Default" />
@@ -0,0 +1,19 @@
1
+ import RedemptionButton from "./RedemptionButton.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 RedemptionButton: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
16
+ [evt: string]: CustomEvent<any>;
17
+ }, {}, {}, string>;
18
+ type RedemptionButton = InstanceType<typeof RedemptionButton>;
19
+ export default RedemptionButton;
@@ -0,0 +1,88 @@
1
+ <script lang="ts">
2
+ import Stack from "../stack/Stack.svelte";
3
+ import { getVariablesContext } from "../../stores/variables";
4
+ import type { RedemptionButtonProps } from "../../types/components/redemption-button";
5
+ import QRCode from "qrcode";
6
+ import { onMount } from "svelte";
7
+
8
+ const props: RedemptionButtonProps = $props();
9
+
10
+ const variables = getVariablesContext();
11
+ const redemptionUrl = $derived($variables?.["success.redemption_url"]);
12
+
13
+ let isMobile = $state(true);
14
+ let qrCodeDataUrl = $state("");
15
+ let qrFailed = $state(false);
16
+
17
+ onMount(() => {
18
+ const mediaQuery = window.matchMedia("(max-width: 767px)");
19
+ isMobile = mediaQuery.matches;
20
+
21
+ const handler = (e: MediaQueryListEvent) => {
22
+ isMobile = e.matches;
23
+ };
24
+
25
+ mediaQuery.addEventListener("change", handler);
26
+
27
+ return () => {
28
+ mediaQuery.removeEventListener("change", handler);
29
+ };
30
+ });
31
+
32
+ $effect(() => {
33
+ if (!isMobile && redemptionUrl) {
34
+ qrFailed = false;
35
+ QRCode.toDataURL(redemptionUrl, {
36
+ width: 512,
37
+ margin: 2,
38
+ errorCorrectionLevel: "M",
39
+ color: {
40
+ dark: "#000000",
41
+ light: "#ffffff",
42
+ },
43
+ })
44
+ .then((url) => {
45
+ qrCodeDataUrl = url;
46
+ })
47
+ .catch((err) => {
48
+ console.warn("Failed to generate QR code:", err);
49
+ qrFailed = true;
50
+ });
51
+ }
52
+ });
53
+ </script>
54
+
55
+ {#if (isMobile || qrFailed) && redemptionUrl}
56
+ <Stack
57
+ {...props.stack}
58
+ onclick={() => {
59
+ window.location.href = redemptionUrl;
60
+ }}
61
+ />
62
+ {:else if qrCodeDataUrl}
63
+ <div class="qr-wrapper">
64
+ <img src={qrCodeDataUrl} alt="Redemption QR Code" class="qr-code" />
65
+ </div>
66
+ {/if}
67
+
68
+ <style>
69
+ .qr-wrapper {
70
+ display: flex;
71
+ justify-content: center;
72
+ align-items: center;
73
+ align-self: center;
74
+ width: fit-content;
75
+ margin: 0 auto;
76
+ background-color: #ffffff;
77
+ border-radius: 8px;
78
+ padding: 16px;
79
+ }
80
+
81
+ .qr-code {
82
+ display: block;
83
+ width: 128px;
84
+ height: 128px;
85
+ image-rendering: -webkit-optimize-contrast;
86
+ image-rendering: crisp-edges;
87
+ }
88
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { RedemptionButtonProps } from "../../types/components/redemption-button";
2
+ declare const RedemptionButton: import("svelte").Component<RedemptionButtonProps, {}, "">;
3
+ type RedemptionButton = ReturnType<typeof RedemptionButton>;
4
+ export default RedemptionButton;
@@ -0,0 +1,31 @@
1
+ <script module lang="ts">
2
+ import { defineMeta } from "@storybook/addon-svelte-csf";
3
+
4
+ import Screen from "./Screen.svelte";
5
+ import { paywallData, uiConfigData } from "../../stories/fixtures";
6
+ import type { WorkflowScreen } from "../../types/workflow";
7
+
8
+ const defaultScreen: WorkflowScreen = {
9
+ ...paywallData,
10
+ asset_base_url: "https://assets.pawwalls.com",
11
+ config: {},
12
+ localized_strings: {},
13
+ localized_strings_by_tier: {},
14
+ name: "Demo Screen",
15
+ offering_id: "demo_offering",
16
+ revision: 1,
17
+ template_name: "stack",
18
+ };
19
+
20
+ const { Story } = defineMeta({
21
+ title: "Components/Screen",
22
+ component: Screen,
23
+ args: {
24
+ paywallComponents: defaultScreen,
25
+ selectedLocale: defaultScreen.default_locale,
26
+ uiConfig: uiConfigData,
27
+ },
28
+ });
29
+ </script>
30
+
31
+ <Story name="Default" />
@@ -0,0 +1,19 @@
1
+ import Screen from "./Screen.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 Screen: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
16
+ [evt: string]: CustomEvent<any>;
17
+ }, {}, {}, string>;
18
+ type Screen = InstanceType<typeof Screen>;
19
+ export default Screen;
@@ -0,0 +1,56 @@
1
+ <script lang="ts">
2
+ import Paywall, {
3
+ type UIConfig,
4
+ } from "../paywall/Paywall.svelte";
5
+ import type { WorkflowScreen } from "../../types/workflow";
6
+ import type { VariableDictionary } from "../../types/variables";
7
+ import { VARIABLES } from "../paywall/fixtures/variables";
8
+ interface Props {
9
+ paywallComponents: WorkflowScreen | null | undefined;
10
+ selectedLocale?: string;
11
+ uiConfig: UIConfig;
12
+ globalVariables?: VariableDictionary;
13
+ onActionTriggered?: (actionId: string) => void;
14
+ onPurchaseClicked?: (
15
+ packageId: string,
16
+ actionId: string,
17
+ ) => void | Promise<void>;
18
+ containerId?: string;
19
+ }
20
+ const {
21
+ paywallComponents,
22
+ selectedLocale = "en_US",
23
+ uiConfig,
24
+ globalVariables = {},
25
+ onActionTriggered,
26
+ onPurchaseClicked,
27
+ containerId = "screen-container",
28
+ }: Props = $props();
29
+ </script>
30
+
31
+ <div id={containerId} class="paywall-container">
32
+ {#if paywallComponents}
33
+ <Paywall
34
+ paywallData={paywallComponents}
35
+ {selectedLocale}
36
+ {uiConfig}
37
+ variablesPerPackage={VARIABLES}
38
+ {globalVariables}
39
+ onNavigateToUrlClicked={() => {}}
40
+ onVisitCustomerCenterClicked={() => {}}
41
+ onBackClicked={() => {}}
42
+ onRestorePurchasesClicked={() => {}}
43
+ onActionTriggered={(actionId: string) => {
44
+ onActionTriggered?.(actionId);
45
+ }}
46
+ {onPurchaseClicked}
47
+ onError={(error) => {
48
+ console.error("Paywall error:", error);
49
+ }}
50
+ />
51
+ {:else}
52
+ <div>
53
+ <p>No paywall data provided</p>
54
+ </div>
55
+ {/if}
56
+ </div>
@@ -0,0 +1,15 @@
1
+ import { type UIConfig } from "../paywall/Paywall.svelte";
2
+ import type { WorkflowScreen } from "../../types/workflow";
3
+ import type { VariableDictionary } from "../../types/variables";
4
+ interface Props {
5
+ paywallComponents: WorkflowScreen | null | undefined;
6
+ selectedLocale?: string;
7
+ uiConfig: UIConfig;
8
+ globalVariables?: VariableDictionary;
9
+ onActionTriggered?: (actionId: string) => void;
10
+ onPurchaseClicked?: (packageId: string, actionId: string) => void | Promise<void>;
11
+ containerId?: string;
12
+ }
13
+ declare const Screen: import("svelte").Component<Props, {}, "">;
14
+ type Screen = ReturnType<typeof Screen>;
15
+ export default Screen;
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export { default as PurchaseButton } from "./components/purchase-button/Purchase
11
11
  export { default as Stack } from "./components/stack/Stack.svelte";
12
12
  export { default as Text } from "./components/text/Text.svelte";
13
13
  export { default as Timeline } from "./components/timeline/Timeline.svelte";
14
+ export { default as Screen } from "./components/workflows/Screen.svelte";
14
15
  export { default as Video } from "./components/video/Video.svelte";
15
16
  export * from "./types";
16
17
  export { type PaywallData } from "./types/paywall";
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ export { default as PurchaseButton } from "./components/purchase-button/Purchase
12
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
+ export { default as Screen } from "./components/workflows/Screen.svelte";
15
16
  export { default as Video } from "./components/video/Video.svelte";
16
17
  export * from "./types";
17
18
  export {} from "./types/paywall";
@@ -7,9 +7,10 @@ import type { ImageProps } from "./components/image";
7
7
  import type { InputMultipleChoiceProps, InputOptionProps, InputSingleChoiceProps } from "./components/options";
8
8
  import type { PackageProps } from "./components/package";
9
9
  import type { PurchaseButtonProps } from "./components/purchase-button";
10
+ import type { RedemptionButtonProps } from "./components/redemption-button";
10
11
  import type { StackProps } from "./components/stack";
11
12
  import type { TabControlButtonProps, TabControlProps, TabControlToggleProps, TabsProps } from "./components/tabs";
12
13
  import type { TextNodeProps } from "./components/text";
13
14
  import type { TimelineProps } from "./components/timeline";
14
15
  import type { VideoProps } from "./components/video";
15
- export type Component = ButtonProps | CarouselProps | CountdownProps | FooterProps | IconProps | ImageProps | InputMultipleChoiceProps | InputOptionProps | InputSingleChoiceProps | PackageProps | PurchaseButtonProps | StackProps | TabControlButtonProps | TabControlToggleProps | TabControlProps | TabsProps | TextNodeProps | TimelineProps | VideoProps;
16
+ export type Component = ButtonProps | CarouselProps | CountdownProps | FooterProps | IconProps | ImageProps | InputMultipleChoiceProps | InputOptionProps | InputSingleChoiceProps | PackageProps | PurchaseButtonProps | RedemptionButtonProps | StackProps | TabControlButtonProps | TabControlToggleProps | TabControlProps | TabsProps | TextNodeProps | TimelineProps | VideoProps;
@@ -0,0 +1,7 @@
1
+ import type { BaseComponent } from "../base";
2
+ import type { StackProps } from "./stack";
3
+ export interface RedemptionButtonProps extends BaseComponent {
4
+ type: "redemption_button";
5
+ stack: StackProps;
6
+ transition?: null;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -7,8 +7,8 @@ export declare enum PackageIdentifier {
7
7
  annual = "$rc_annual",
8
8
  lifetime = "$rc_lifetime"
9
9
  }
10
- type VariableName = "product.price" | "product.price_per_period" | "product.price_per_period_abbreviated" | "product.price_per_day" | "product.price_per_week" | "product.price_per_month" | "product.price_per_year" | "product.period" | "product.period_abbreviated" | "product.periodly" | "product.period_in_days" | "product.period_in_weeks" | "product.period_in_months" | "product.period_in_years" | "product.period_with_unit" | "product.currency_code" | "product.currency_symbol" | "product.offer_price" | "product.offer_price_per_day" | "product.offer_price_per_week" | "product.offer_price_per_month" | "product.offer_price_per_year" | "product.offer_period" | "product.offer_period_abbreviated" | "product.offer_period_in_days" | "product.offer_period_in_weeks" | "product.offer_period_in_months" | "product.offer_period_in_years" | "product.offer_period_with_unit" | "product.offer_end_date" | "product.secondary_offer_price" | "product.secondary_offer_period" | "product.secondary_offer_period_abbreviated" | "product.relative_discount" | "product.store_product_name";
11
- export type VariableDictionary = Record<VariableName, string>;
10
+ type VariableName = "product.price" | "product.price_per_period" | "product.price_per_period_abbreviated" | "product.price_per_day" | "product.price_per_week" | "product.price_per_month" | "product.price_per_year" | "product.period" | "product.period_abbreviated" | "product.periodly" | "product.period_in_days" | "product.period_in_weeks" | "product.period_in_months" | "product.period_in_years" | "product.period_with_unit" | "product.currency_code" | "product.currency_symbol" | "product.offer_price" | "product.offer_price_per_day" | "product.offer_price_per_week" | "product.offer_price_per_month" | "product.offer_price_per_year" | "product.offer_period" | "product.offer_period_abbreviated" | "product.offer_period_in_days" | "product.offer_period_in_weeks" | "product.offer_period_in_months" | "product.offer_period_in_years" | "product.offer_period_with_unit" | "product.offer_end_date" | "product.secondary_offer_price" | "product.secondary_offer_period" | "product.secondary_offer_period_abbreviated" | "product.relative_discount" | "product.store_product_name" | "success.redemption_url" | "success.package_identifier" | "success.product_identifier" | "success.product_title" | "success.product_description" | "success.price" | "success.currency_code" | "success.customer_email" | "success.is_subscription" | "success.is_trial";
11
+ export type VariableDictionary = Partial<Record<VariableName, string>>;
12
12
  export type VariablesDictionary = Record<PackageIdentifier, VariableDictionary>;
13
13
  export interface PackageInfo {
14
14
  hasIntroOffer?: boolean;
@@ -0,0 +1,15 @@
1
+ import type { PaywallData } from "./paywall";
2
+ /**
3
+ * Represents a screen/paywall configuration in a workflow.
4
+ * Extends core PaywallData with workflow-specific metadata.
5
+ */
6
+ export interface WorkflowScreen extends PaywallData {
7
+ asset_base_url: string;
8
+ config: Record<string, unknown>;
9
+ localized_strings: Record<string, Record<string, string>>;
10
+ localized_strings_by_tier: Record<string, Record<string, string>>;
11
+ name: string;
12
+ offering_id: string | null;
13
+ revision: number;
14
+ template_name: string;
15
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/types.d.ts CHANGED
@@ -119,4 +119,4 @@ export declare enum StackDistribution {
119
119
  center = "center",
120
120
  end = "flex-end"
121
121
  }
122
- export {};
122
+ export type { WorkflowScreen } from "./types/workflow";
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": "3.0.1",
5
+ "version": "3.2.0",
6
6
  "author": {
7
7
  "name": "RevenueCat, Inc."
8
8
  },
@@ -83,6 +83,7 @@
83
83
  "@sveltejs/package": "2.5.4",
84
84
  "@sveltejs/vite-plugin-svelte": "6.2.1",
85
85
  "@types/node": "24.9.2",
86
+ "@types/qrcode": "^1.5.6",
86
87
  "@typescript-eslint/parser": "8.46.2",
87
88
  "chromatic": "13.3.2",
88
89
  "eslint": "9.38.0",
@@ -115,5 +116,8 @@
115
116
  "engines": {
116
117
  "node": "^22.18"
117
118
  },
118
- "packageManager": "npm@11.5.2+sha512.aac1241cfc3f41dc38780d64295c6c6b917a41e24288b33519a7b11adfc5a54a5f881c642d7557215b6c70e01e55655ed7ba666300fd0238bc75fb17478afaf3"
119
+ "packageManager": "npm@11.5.2+sha512.aac1241cfc3f41dc38780d64295c6c6b917a41e24288b33519a7b11adfc5a54a5f881c642d7557215b6c70e01e55655ed7ba666300fd0238bc75fb17478afaf3",
120
+ "dependencies": {
121
+ "qrcode": "^1.5.4"
122
+ }
119
123
  }