@revenuecat/purchases-ui-js 3.1.0 → 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;
@@ -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;
@@ -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.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
  }