@revenuecat/purchases-ui-js 4.11.0 → 4.12.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.
@@ -117,6 +117,10 @@
117
117
  walletButtonRender?: WalletButtonRender;
118
118
  prepareWalletPurchase?: WalletPurchasePrepare;
119
119
  onWalletPurchaseClicked?: WalletPurchaseStart;
120
+ appUserId?: string;
121
+ isSandbox?: boolean;
122
+ rcSource?: string;
123
+ onCustomWebCheckout?: (url: string) => void;
120
124
  onInputChanged?: (
121
125
  fieldId: string,
122
126
  value: string,
@@ -181,6 +185,10 @@
181
185
  walletButtonRender,
182
186
  prepareWalletPurchase,
183
187
  onWalletPurchaseClicked,
188
+ appUserId,
189
+ isSandbox,
190
+ rcSource,
191
+ onCustomWebCheckout,
184
192
  onInputChanged,
185
193
  onSensitiveInputChanged,
186
194
  onReservedAttributeChanged,
@@ -398,6 +406,10 @@
398
406
  walletButtonRender,
399
407
  prepareWalletPurchase,
400
408
  onWalletPurchaseClicked,
409
+ appUserId,
410
+ isSandbox,
411
+ rcSource,
412
+ onCustomWebCheckout,
401
413
  uiConfig,
402
414
  hideBackButtons,
403
415
  });
@@ -468,18 +480,26 @@
468
480
  <Stack
469
481
  {...stack}
470
482
  class="paywall-content-scroll"
471
- padding={sticky_footer
472
- ? {
473
- ...stack.padding,
474
- bottom: (stack.padding?.bottom ?? 0) + footerHeight,
475
- }
476
- : stack.padding}
477
483
  style={mapPaywallContentStackStyle(
478
484
  stack.size.height,
479
485
  pullContentUnderHeader,
480
486
  headerHeight,
481
487
  )}
482
- />
488
+ >
489
+ {#if sticky_footer}
490
+ <!-- Adds a real flex item rather than bottom padding to clear
491
+ a footer because iOS Safari ignores the padding if the content
492
+ still fits on screen leaving content stuck behind the footer. -->
493
+ <div
494
+ class="footer-scroll-spacer"
495
+ aria-hidden="true"
496
+ style="height: {Math.max(
497
+ 0,
498
+ footerHeight - (stack.spacing ?? 0),
499
+ )}px;"
500
+ ></div>
501
+ {/if}
502
+ </Stack>
483
503
  </div>
484
504
  {#if sticky_footer}
485
505
  <!-- Overlaps the content so translucent footers show it; sticky rather than
@@ -536,6 +556,11 @@
536
556
  flex-shrink: 0;
537
557
  }
538
558
 
559
+ .footer-scroll-spacer {
560
+ width: 100%;
561
+ flex-shrink: 0;
562
+ }
563
+
539
564
  .paywall-content {
540
565
  z-index: 2;
541
566
  width: 100%;
@@ -47,6 +47,10 @@ interface Props {
47
47
  walletButtonRender?: WalletButtonRender;
48
48
  prepareWalletPurchase?: WalletPurchasePrepare;
49
49
  onWalletPurchaseClicked?: WalletPurchaseStart;
50
+ appUserId?: string;
51
+ isSandbox?: boolean;
52
+ rcSource?: string;
53
+ onCustomWebCheckout?: (url: string) => void;
50
54
  onInputChanged?: (fieldId: string, value: string, actionId?: string) => void;
51
55
  /**
52
56
  * Called for sensitive inputs (keyboard_type: "password") instead of
@@ -6,6 +6,7 @@
6
6
  import type { PurchaseButtonProps } from "../../types/components/purchase-button";
7
7
  import type { PurchaseButtonInteractionData } from "../../types/paywall-component-interaction";
8
8
  import { get } from "svelte/store";
9
+ import { buildCustomWebCheckoutURL } from "./custom-web-checkout-url";
9
10
 
10
11
  const props: PurchaseButtonProps = $props();
11
12
  const { stack } = props;
@@ -13,41 +14,42 @@
13
14
  const {
14
15
  emitComponentInteraction,
15
16
  onPurchase,
17
+ onCustomWebCheckout,
16
18
  selectedPackageId,
17
19
  infoPerPackage,
20
+ appUserId,
21
+ isSandbox,
22
+ rcSource,
18
23
  } = getPaywallContext();
19
24
  const { getLocalizedString } = getLocalizationContext();
20
25
 
21
26
  // Set when embedded in a package; standalone buttons fall back to the store.
22
27
  const enclosingPackageId = getOptionalPackageIdContext();
23
28
 
24
- const appendQueryParam = (url: string, key: string, value: string) => {
25
- const hashIndex = url.indexOf("#");
26
- const hash = hashIndex === -1 ? "" : url.slice(hashIndex);
27
- const urlWithoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex);
28
- const separator = urlWithoutHash.includes("?") ? "&" : "?";
29
-
30
- return `${urlWithoutHash}${separator}${encodeURIComponent(key)}=${encodeURIComponent(value)}${hash}`;
29
+ const getCustomWebCheckoutURL = (packageId: string) => {
30
+ if (props.method?.type !== "custom_web_checkout") {
31
+ return undefined;
32
+ }
33
+ const url = getLocalizedString(props.method.custom_url.url_lid);
34
+ return url
35
+ ? buildCustomWebCheckoutURL(url, props.method.custom_url, {
36
+ packageId,
37
+ appUserId,
38
+ isSandbox,
39
+ rcSource,
40
+ })
41
+ : undefined;
31
42
  };
32
43
 
33
44
  const getPurchaseButtonInteractionData = (
34
45
  packageId: string,
46
+ customWebCheckoutURL: string | undefined,
35
47
  ): PurchaseButtonInteractionData => {
36
- let componentURL: string | undefined;
48
+ let componentURL = customWebCheckoutURL;
37
49
  const resolvedMethodType =
38
50
  props.method?.type === "unknown" ? undefined : props.method?.type;
39
51
  const webCheckoutURL = $infoPerPackage?.[packageId]?.webCheckoutURL;
40
52
 
41
- if (props.method?.type === "custom_web_checkout") {
42
- const url = getLocalizedString(props.method.custom_url.url_lid);
43
- if (url) {
44
- const packageParam = props.method.custom_url.package_param;
45
- componentURL = packageParam
46
- ? appendQueryParam(url, packageParam, packageId)
47
- : url;
48
- }
49
- }
50
-
51
53
  if (componentURL === undefined) {
52
54
  const interactionValue = resolvedMethodType ?? props.action;
53
55
  if (
@@ -75,7 +77,15 @@
75
77
  if (packageId !== undefined) {
76
78
  // Sync before purchasing so onPurchase reads this package, not a stale one.
77
79
  $selectedPackageId = packageId;
78
- emitComponentInteraction(getPurchaseButtonInteractionData(packageId));
80
+ const customWebCheckoutURL = getCustomWebCheckoutURL(packageId);
81
+ emitComponentInteraction(
82
+ getPurchaseButtonInteractionData(packageId, customWebCheckoutURL),
83
+ );
84
+ // Hosts without the callback keep the purchase flow.
85
+ if (customWebCheckoutURL !== undefined && onCustomWebCheckout) {
86
+ onCustomWebCheckout(customWebCheckoutURL);
87
+ return;
88
+ }
79
89
  }
80
90
 
81
91
  onPurchase(props.triggers?.on_purchase_press);
@@ -0,0 +1,17 @@
1
+ import type { PurchaseButtonMethod } from "../../types/components/purchase-button";
2
+ type CustomURLConfig = Extract<PurchaseButtonMethod, {
3
+ type: "custom_web_checkout";
4
+ }>["custom_url"];
5
+ type CustomWebCheckoutContext = {
6
+ packageId: string;
7
+ appUserId?: string;
8
+ isSandbox?: boolean;
9
+ rcSource?: string;
10
+ };
11
+ /**
12
+ * Mirrors the native SDKs: source, then app user ID, env and package when
13
+ * configured. The source is the host's own, so it survives a same-tab hop.
14
+ * Same-named params are replaced; an unparseable URL is returned unchanged.
15
+ */
16
+ export declare const buildCustomWebCheckoutURL: (url: string, config: CustomURLConfig, { packageId, appUserId, isSandbox, rcSource }: CustomWebCheckoutContext) => string;
17
+ export {};
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Mirrors the native SDKs: source, then app user ID, env and package when
3
+ * configured. The source is the host's own, so it survives a same-tab hop.
4
+ * Same-named params are replaced; an unparseable URL is returned unchanged.
5
+ */
6
+ export const buildCustomWebCheckoutURL = (url, config, { packageId, appUserId, isSandbox, rcSource }) => {
7
+ let parsed;
8
+ try {
9
+ parsed = new URL(url);
10
+ }
11
+ catch {
12
+ return url;
13
+ }
14
+ const { searchParams } = parsed;
15
+ if (rcSource !== undefined) {
16
+ searchParams.set("rc_source", rcSource);
17
+ }
18
+ if (config.app_user_id_param && appUserId !== undefined) {
19
+ searchParams.set(config.app_user_id_param, appUserId);
20
+ }
21
+ if (config.env_param && isSandbox !== undefined) {
22
+ searchParams.set(config.env_param, isSandbox ? "sandbox" : "production");
23
+ }
24
+ if (config.package_param) {
25
+ searchParams.set(config.package_param, packageId);
26
+ }
27
+ return parsed.toString();
28
+ };
@@ -64,6 +64,10 @@
64
64
  walletButtonRender?: WalletButtonRender;
65
65
  prepareWalletPurchase?: WalletPurchasePrepare;
66
66
  onWalletPurchaseClicked?: WalletPurchaseStart;
67
+ appUserId?: string;
68
+ isSandbox?: boolean;
69
+ rcSource?: string;
70
+ onCustomWebCheckout?: (url: string) => void;
67
71
  safeAreaFallbackColor?: ColorScheme | null;
68
72
  hideBackButtons?: boolean;
69
73
  offering?: PaywallOffering | null;
@@ -98,6 +102,10 @@
98
102
  walletButtonRender,
99
103
  prepareWalletPurchase,
100
104
  onWalletPurchaseClicked,
105
+ appUserId,
106
+ isSandbox,
107
+ rcSource,
108
+ onCustomWebCheckout,
101
109
  safeAreaFallbackColor,
102
110
  hideBackButtons,
103
111
  offering,
@@ -147,6 +155,10 @@
147
155
  {walletButtonRender}
148
156
  {prepareWalletPurchase}
149
157
  {onWalletPurchaseClicked}
158
+ {appUserId}
159
+ {isSandbox}
160
+ {rcSource}
161
+ {onCustomWebCheckout}
150
162
  {safeAreaFallbackColor}
151
163
  {hideBackButtons}
152
164
  {offering}
@@ -38,6 +38,10 @@ interface Props {
38
38
  walletButtonRender?: WalletButtonRender;
39
39
  prepareWalletPurchase?: WalletPurchasePrepare;
40
40
  onWalletPurchaseClicked?: WalletPurchaseStart;
41
+ appUserId?: string;
42
+ isSandbox?: boolean;
43
+ rcSource?: string;
44
+ onCustomWebCheckout?: (url: string) => void;
41
45
  safeAreaFallbackColor?: ColorScheme | null;
42
46
  hideBackButtons?: boolean;
43
47
  offering?: PaywallOffering | null;
@@ -57,6 +57,10 @@
57
57
  walletButtonRender?: WalletButtonRender;
58
58
  prepareWalletPurchase?: WalletPurchasePrepare;
59
59
  onWalletPurchaseClicked?: WalletPurchaseStart;
60
+ appUserId?: string;
61
+ isSandbox?: boolean;
62
+ rcSource?: string;
63
+ onCustomWebCheckout?: (url: string) => void;
60
64
  safeAreaFallbackColor?: ColorScheme | null;
61
65
  hideBackButtons?: boolean;
62
66
  onPurchaseClicked?: (
@@ -113,6 +117,10 @@
113
117
  walletButtonRender,
114
118
  prepareWalletPurchase,
115
119
  onWalletPurchaseClicked,
120
+ appUserId,
121
+ isSandbox,
122
+ rcSource,
123
+ onCustomWebCheckout,
116
124
  safeAreaFallbackColor,
117
125
  hideBackButtons,
118
126
  onPurchaseClicked,
@@ -378,6 +386,10 @@
378
386
  {walletButtonRender}
379
387
  {prepareWalletPurchase}
380
388
  {onWalletPurchaseClicked}
389
+ {appUserId}
390
+ {isSandbox}
391
+ {rcSource}
392
+ {onCustomWebCheckout}
381
393
  {safeAreaFallbackColor}
382
394
  {hideBackButtons}
383
395
  {onPurchaseClicked}
@@ -23,6 +23,10 @@ interface Props {
23
23
  walletButtonRender?: WalletButtonRender;
24
24
  prepareWalletPurchase?: WalletPurchasePrepare;
25
25
  onWalletPurchaseClicked?: WalletPurchaseStart;
26
+ appUserId?: string;
27
+ isSandbox?: boolean;
28
+ rcSource?: string;
29
+ onCustomWebCheckout?: (url: string) => void;
26
30
  safeAreaFallbackColor?: ColorScheme | null;
27
31
  hideBackButtons?: boolean;
28
32
  onPurchaseClicked?: (packageId: string, actionId: string) => void | Promise<void>;
@@ -32,6 +32,8 @@ type PaywallContext = Readonly<{
32
32
  typedCustom?: BridgeCustomValues;
33
33
  bridgeInputs?: Writable<BridgeInputs>;
34
34
  onPurchase: (actionId?: string) => void;
35
+ /** Opens a custom checkout URL in place of purchasing. */
36
+ onCustomWebCheckout?: (url: string) => void;
35
37
  emitComponentInteraction: (data: ComponentInteractionData) => void;
36
38
  onNavigateToUrl?: (url: string) => void;
37
39
  onInputChanged?: (fieldId: string, value: string, actionId?: string) => void;
@@ -47,6 +49,10 @@ type PaywallContext = Readonly<{
47
49
  prepareWalletPurchase?: WalletPurchasePrepare;
48
50
  onWalletPurchaseClicked?: WalletPurchaseStart;
49
51
  onButtonAction: (action: Action, actionId?: string) => void;
52
+ /** Forwarded on custom checkout URLs. */
53
+ appUserId?: string;
54
+ isSandbox?: boolean;
55
+ rcSource?: string;
50
56
  uiConfig: UIConfig;
51
57
  hideBackButtons: boolean;
52
58
  }>;
@@ -16,7 +16,9 @@ export type PurchaseButtonMethod = {
16
16
  type: "custom_web_checkout";
17
17
  custom_url: {
18
18
  url_lid: string;
19
- package_param?: string;
19
+ package_param?: string | null;
20
+ app_user_id_param?: string | null;
21
+ env_param?: string | null;
20
22
  };
21
23
  } & PurchaseButtonWebMethod) | {
22
24
  type: "unknown";
@@ -41,9 +41,9 @@ export declare const filterMatchingOverrides: <T extends Component>(context: Con
41
41
  * evaluate from the context flags, so mixed pseudo + rule/state overrides get
42
42
  * the same all-conditions-AND-ed semantics as iOS's buildPartial.
43
43
  *
44
- * Only an override containing a state_condition may reveal a base-hidden
45
- * component (set `visible: true`); overrides without one can only hide.
46
- * Components derive `isVisible` from the merged result
44
+ * An override may both hide (`visible: false`) and reveal (`visible: true`) a
45
+ * component, regardless of which condition types it carries, matching iOS's
46
+ * buildPartial. Components derive `isVisible` from the merged result
47
47
  * (`actualProps.visible !== false`).
48
48
  */
49
49
  export declare const resolveOverrideProperties: <T extends Component>(context: ConditionContext, overrides?: Overrides<T>) => Partial<T>;
@@ -131,19 +131,17 @@ export const filterMatchingOverrides = (context, overrides) => {
131
131
  * evaluate from the context flags, so mixed pseudo + rule/state overrides get
132
132
  * the same all-conditions-AND-ed semantics as iOS's buildPartial.
133
133
  *
134
- * Only an override containing a state_condition may reveal a base-hidden
135
- * component (set `visible: true`); overrides without one can only hide.
136
- * Components derive `isVisible` from the merged result
134
+ * An override may both hide (`visible: false`) and reveal (`visible: true`) a
135
+ * component, regardless of which condition types it carries, matching iOS's
136
+ * buildPartial. Components derive `isVisible` from the merged result
137
137
  * (`actualProps.visible !== false`).
138
138
  */
139
139
  export const resolveOverrideProperties = (context, overrides) => {
140
140
  let merged = {};
141
141
  for (const override of filterMatchingOverrides(context, overrides)) {
142
142
  const properties = { ...override.properties };
143
- const canReveal = override.conditions.some((c) => c.type === RuleKey.StateCondition);
144
143
  // visible: null carries no intent and must not clobber an earlier false.
145
- if (properties.visible === null ||
146
- (properties.visible === true && !canReveal)) {
144
+ if (properties.visible === null) {
147
145
  delete properties.visible;
148
146
  }
149
147
  merged = { ...merged, ...properties };
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.11.0",
5
+ "version": "4.12.0",
6
6
  "author": {
7
7
  "name": "RevenueCat, Inc."
8
8
  },