@revenuecat/purchases-ui-js 3.1.0 → 3.3.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.
@@ -15,7 +15,7 @@
15
15
  };
16
16
  });
17
17
 
18
- const { onButtonAction } = getPaywallContext();
18
+ const { onButtonAction, hideBackButtons } = getPaywallContext();
19
19
 
20
20
  const onclick = () => {
21
21
  const actionId = props.triggers?.on_press;
@@ -27,8 +27,9 @@
27
27
  case "workflow":
28
28
  return true;
29
29
  case "restore_purchases":
30
- case "navigate_back":
31
30
  return false;
31
+ case "navigate_back":
32
+ return !hideBackButtons;
32
33
  case "navigate_to":
33
34
  return action.destination !== "web_paywall_link";
34
35
  default:
@@ -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;
@@ -40,6 +47,7 @@
40
47
  onNavigateToUrlClicked?: (url: string) => void;
41
48
  onActionTriggered?: (actionId: string) => void;
42
49
  onError?: (error: unknown) => void;
50
+ hideBackButtons?: boolean;
43
51
  walletButtonRender?: (
44
52
  node: HTMLElement,
45
53
  params: {
@@ -56,6 +64,7 @@
56
64
  paywallData,
57
65
  selectedLocale,
58
66
  variablesPerPackage = {},
67
+ globalVariables = {},
59
68
  infoPerPackage = {},
60
69
  preferredColorMode,
61
70
  onPurchaseClicked,
@@ -67,6 +76,7 @@
67
76
  onError,
68
77
  uiConfig,
69
78
  walletButtonRender,
79
+ hideBackButtons = false,
70
80
  }: Props = $props();
71
81
 
72
82
  const getColorMode = setColorModeContext(() => preferredColorMode);
@@ -140,12 +150,16 @@
140
150
  onButtonAction,
141
151
  walletButtonRender,
142
152
  uiConfig,
153
+ hideBackButtons,
143
154
  });
144
155
 
145
- const variables: VariablesStore = derived(
146
- selectedPackageId,
147
- (packageId) => variablesPerPackage[packageId || ""],
148
- );
156
+ const variables: VariablesStore = derived(selectedPackageId, (packageId) => {
157
+ const packageVars = variablesPerPackage[packageId || ""];
158
+ return {
159
+ ...globalVariables,
160
+ ...packageVars,
161
+ };
162
+ });
149
163
 
150
164
  setVariablesContext(variables);
151
165
 
@@ -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;
@@ -16,6 +23,7 @@ interface Props {
16
23
  onNavigateToUrlClicked?: (url: string) => void;
17
24
  onActionTriggered?: (actionId: string) => void;
18
25
  onError?: (error: unknown) => void;
26
+ hideBackButtons?: boolean;
19
27
  walletButtonRender?: (node: HTMLElement, params: {
20
28
  selectedPackageId: string;
21
29
  onReady?: () => void;
@@ -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;
@@ -3,11 +3,13 @@
3
3
  type UIConfig,
4
4
  } from "../paywall/Paywall.svelte";
5
5
  import type { WorkflowScreen } from "../../types/workflow";
6
+ import type { VariableDictionary } from "../../types/variables";
6
7
  import { VARIABLES } from "../paywall/fixtures/variables";
7
8
  interface Props {
8
9
  paywallComponents: WorkflowScreen | null | undefined;
9
10
  selectedLocale?: string;
10
11
  uiConfig: UIConfig;
12
+ globalVariables?: VariableDictionary;
11
13
  onActionTriggered?: (actionId: string) => void;
12
14
  onPurchaseClicked?: (
13
15
  packageId: string,
@@ -19,6 +21,7 @@
19
21
  paywallComponents,
20
22
  selectedLocale = "en_US",
21
23
  uiConfig,
24
+ globalVariables = {},
22
25
  onActionTriggered,
23
26
  onPurchaseClicked,
24
27
  containerId = "screen-container",
@@ -32,6 +35,7 @@
32
35
  {selectedLocale}
33
36
  {uiConfig}
34
37
  variablesPerPackage={VARIABLES}
38
+ {globalVariables}
35
39
  onNavigateToUrlClicked={() => {}}
36
40
  onVisitCustomerCenterClicked={() => {}}
37
41
  onBackClicked={() => {}}
@@ -1,9 +1,11 @@
1
1
  import { type UIConfig } from "../paywall/Paywall.svelte";
2
2
  import type { WorkflowScreen } from "../../types/workflow";
3
+ import type { VariableDictionary } from "../../types/variables";
3
4
  interface Props {
4
5
  paywallComponents: WorkflowScreen | null | undefined;
5
6
  selectedLocale?: string;
6
7
  uiConfig: UIConfig;
8
+ globalVariables?: VariableDictionary;
7
9
  onActionTriggered?: (actionId: string) => void;
8
10
  onPurchaseClicked?: (packageId: string, actionId: string) => void | Promise<void>;
9
11
  containerId?: string;
@@ -16,6 +16,7 @@ type PaywallContext = Readonly<{
16
16
  };
17
17
  onButtonAction: (action: Action, actionId?: string) => void;
18
18
  uiConfig: UIConfig;
19
+ hideBackButtons: boolean;
19
20
  }>;
20
21
  export declare function setPaywallContext(context: PaywallContext): void;
21
22
  export declare function getPaywallContext(): PaywallContext;
@@ -19,6 +19,7 @@ export function paywallDecorator(walletButtonRender) {
19
19
  infoPerPackage: readable(undefined),
20
20
  onPurchase: () => window.alert("Purchase clicked"),
21
21
  onButtonAction: (action, actionId) => window.alert(`Button clicked: ${JSON.stringify({ action, actionId }, undefined, 2)}`),
22
+ hideBackButtons: false,
22
23
  walletButtonRender,
23
24
  uiConfig: emptyUiConfig,
24
25
  });
@@ -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;
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.1.0",
5
+ "version": "3.3.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
  }