@revenuecat/purchases-ui-js 4.8.21 → 4.8.23

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.
Files changed (42) hide show
  1. package/README.md +6 -0
  2. package/dist/components/carousel/Carousel.svelte +3 -6
  3. package/dist/components/carousel/carousel-utils.d.ts +6 -0
  4. package/dist/components/carousel/carousel-utils.js +8 -0
  5. package/dist/components/input-text/InputText.stories.svelte +92 -0
  6. package/dist/components/input-text/InputText.stories.svelte.d.ts +19 -0
  7. package/dist/components/input-text/InputText.svelte +281 -32
  8. package/dist/components/input-text/InputTextTestWrapper.svelte +14 -4
  9. package/dist/components/input-text/InputTextTestWrapper.svelte.d.ts +6 -0
  10. package/dist/components/input-text/pattern.d.ts +16 -0
  11. package/dist/components/input-text/pattern.js +24 -0
  12. package/dist/components/input-text/validity-rule.d.ts +15 -0
  13. package/dist/components/input-text/validity-rule.js +40 -0
  14. package/dist/components/options/InputMultipleChoice.svelte +5 -2
  15. package/dist/components/options/InputSingleChoice.svelte +5 -2
  16. package/dist/components/paywall/Paywall.stories.svelte +40 -0
  17. package/dist/components/paywall/Paywall.svelte +14 -7
  18. package/dist/components/paywall/Sheet.svelte +19 -6
  19. package/dist/components/paywall/ViewportBackdrop.svelte +13 -1
  20. package/dist/components/paywall/ViewportBackdrop.svelte.d.ts +1 -0
  21. package/dist/components/paywall/fixtures/gradient-sheet-paywall.d.ts +2 -0
  22. package/dist/components/paywall/fixtures/gradient-sheet-paywall.js +68 -0
  23. package/dist/components/paywall/fixtures/hero-media-paywall.d.ts +3 -0
  24. package/dist/components/paywall/fixtures/hero-media-paywall.js +182 -0
  25. package/dist/components/paywall/fixtures/sheet-close-button-paywall.d.ts +2 -0
  26. package/dist/components/paywall/fixtures/sheet-close-button-paywall.js +62 -0
  27. package/dist/components/paywall/paywall-utils.d.ts +2 -0
  28. package/dist/components/paywall/paywall-utils.js +22 -0
  29. package/dist/components/text/TextNode.svelte +2 -22
  30. package/dist/components/text/text-utils.d.ts +8 -0
  31. package/dist/components/text/text-utils.js +26 -0
  32. package/dist/components/workflows/Screen.svelte +5 -1
  33. package/dist/components/workflows/Workflow.svelte +2 -1
  34. package/dist/stores/inputValidation.d.ts +6 -3
  35. package/dist/stores/inputValidation.js +27 -4
  36. package/dist/stores/state.js +2 -2
  37. package/dist/types/components/input-text.d.ts +19 -0
  38. package/dist/types/overrides.d.ts +1 -1
  39. package/dist/types.d.ts +1 -1
  40. package/dist/utils/safe-area-background.js +1 -1
  41. package/dist/utils/style-utils.js +2 -2
  42. package/package.json +1 -1
@@ -1,3 +1,5 @@
1
1
  import type { Size } from "../../types";
2
+ import type { Component } from "../../types/component";
2
3
  import type { CSS } from "../../utils/base-utils";
4
+ export declare function firstContentIsHeroMedia(components: Component[]): boolean;
3
5
  export declare function mapPaywallContentStackStyle(height: Size, pullContentUnderHeader: boolean, headerHeight: number): CSS;
@@ -1,3 +1,25 @@
1
+ function findFirstContent(components) {
2
+ for (const component of components) {
3
+ if (component.type === "fallback_header") {
4
+ continue;
5
+ }
6
+ if (component.type === "stack") {
7
+ const nestedContent = findFirstContent(component.components);
8
+ if (nestedContent) {
9
+ return nestedContent;
10
+ }
11
+ continue;
12
+ }
13
+ return component;
14
+ }
15
+ }
16
+ export function firstContentIsHeroMedia(components) {
17
+ const firstContent = findFirstContent(components);
18
+ return ((firstContent?.type === "image" ||
19
+ firstContent?.type === "video" ||
20
+ firstContent?.type === "web_view") &&
21
+ firstContent.size.width.type === "fill");
22
+ }
1
23
  export function mapPaywallContentStackStyle(height, pullContentUnderHeader, headerHeight) {
2
24
  return {
3
25
  // Nested fill children need a definite root height; other roots keep
@@ -3,6 +3,7 @@
3
3
  getHtmlFromMarkdown,
4
4
  getTextComponentStyles,
5
5
  getTextWrapperInlineStyles,
6
+ markdownLinkUrlFromClick,
6
7
  } from "./text-utils";
7
8
  import Text from "./Text.svelte";
8
9
  import { getColorModeContext } from "../../stores/color-mode";
@@ -53,28 +54,7 @@
53
54
  const markdownParsed = $derived(getHtmlFromMarkdown(label));
54
55
 
55
56
  const onclick = (event: MouseEvent) => {
56
- if (
57
- event.defaultPrevented ||
58
- event.button !== 0 ||
59
- event.metaKey ||
60
- event.ctrlKey ||
61
- event.shiftKey ||
62
- event.altKey
63
- ) {
64
- return;
65
- }
66
-
67
- const target = event.target;
68
- if (!(target instanceof Element)) {
69
- return;
70
- }
71
-
72
- const anchor = target.closest("a[href]");
73
- if (!(anchor instanceof HTMLAnchorElement)) {
74
- return;
75
- }
76
-
77
- const url = anchor.getAttribute("href") ?? anchor.href;
57
+ const url = markdownLinkUrlFromClick(event);
78
58
  if (!url) {
79
59
  return;
80
60
  }
@@ -4,6 +4,14 @@ import type { TextNodeProps } from "../../types/components/text";
4
4
  import type { AppFontsConfig } from "../../types/ui-config";
5
5
  import { type CSS } from "../../utils/base-utils";
6
6
  export declare const defaultColor: ColorScheme;
7
+ /**
8
+ * Returns the `href` of the markdown link a plain left click landed on, or
9
+ * `null` when the click should be left alone (modifier keys, non-primary
10
+ * button, already handled, not on a link). Hosts route these through
11
+ * `onNavigateToUrl`, since a bare `target="_blank"` anchor is blocked in some
12
+ * of them (embedded webviews, sandboxed frames).
13
+ */
14
+ export declare function markdownLinkUrlFromClick(event: MouseEvent): string | null;
7
15
  export declare function mapTextColor(colorMode: ColorMode, scheme: ColorGradientScheme): CSS;
8
16
  /**
9
17
  * Generates comprehensive styles for text components by combining text, component and size styles
@@ -6,6 +6,32 @@ import { getScopedFontFamily, isFontRCFMManaged } from "../../utils/font-utils";
6
6
  export const defaultColor = {
7
7
  light: { type: "hex", value: DEFAULT_TEXT_COLOR },
8
8
  };
9
+ /**
10
+ * Returns the `href` of the markdown link a plain left click landed on, or
11
+ * `null` when the click should be left alone (modifier keys, non-primary
12
+ * button, already handled, not on a link). Hosts route these through
13
+ * `onNavigateToUrl`, since a bare `target="_blank"` anchor is blocked in some
14
+ * of them (embedded webviews, sandboxed frames).
15
+ */
16
+ export function markdownLinkUrlFromClick(event) {
17
+ if (event.defaultPrevented ||
18
+ event.button !== 0 ||
19
+ event.metaKey ||
20
+ event.ctrlKey ||
21
+ event.shiftKey ||
22
+ event.altKey) {
23
+ return null;
24
+ }
25
+ const target = event.target;
26
+ if (!(target instanceof Element)) {
27
+ return null;
28
+ }
29
+ const anchor = target.closest("a[href]");
30
+ if (!(anchor instanceof HTMLAnchorElement)) {
31
+ return null;
32
+ }
33
+ return anchor.getAttribute("href") || anchor.href || null;
34
+ }
9
35
  export function mapTextColor(colorMode, scheme) {
10
36
  const info = mapColorMode(colorMode, scheme);
11
37
  const color = mapColorInfo(info);
@@ -101,7 +101,11 @@
101
101
  }: Props = $props();
102
102
  </script>
103
103
 
104
- <div id={containerId} class="paywall-container">
104
+ <div
105
+ id={containerId}
106
+ class="paywall-container"
107
+ style="width: 100%; min-width: 0; height: 100%;"
108
+ >
105
109
  {#if paywallComponents}
106
110
  <Paywall
107
111
  paywallData={paywallComponents}
@@ -142,7 +142,8 @@
142
142
  for (const [key, declaration] of Object.entries(
143
143
  screen.state_declarations ?? {},
144
144
  )) {
145
- if (!Object.hasOwn(merged, key)) merged[key] = declaration;
145
+ if (!Object.prototype.hasOwnProperty.call(merged, key))
146
+ merged[key] = declaration;
146
147
  }
147
148
  }
148
149
  return merged;
@@ -4,13 +4,16 @@ import { type Readable } from "svelte/store";
4
4
  * Used to restore selection state when navigating back to a step.
5
5
  */
6
6
  export type InitialInputSelections = Record<string, string[]>;
7
- interface InputValidationContext {
7
+ export interface InputValidationContext {
8
+ /** True only when every registered input is satisfied. */
8
9
  isSatisfied: Readable<boolean>;
9
- updateSatisfaction: (satisfied: boolean) => void;
10
+ /** Registers or updates the satisfaction of a single input, keyed by component id. */
11
+ updateSatisfaction: (id: string, satisfied: boolean) => void;
12
+ /** Stops an unmounted input from gating the screen. */
13
+ removeInput: (id: string) => void;
10
14
  }
11
15
  export declare function createInputValidationContext(initiallySatisfied?: boolean): InputValidationContext;
12
16
  export declare function setInputValidationContext(context: InputValidationContext): void;
13
17
  export declare function getInputValidationContext(): InputValidationContext | undefined;
14
18
  export declare function setInitialInputSelectionsContext(selections: InitialInputSelections): void;
15
19
  export declare function getInitialInputSelectionsContext(): InitialInputSelections;
16
- export {};
@@ -1,15 +1,38 @@
1
1
  import { getContext, setContext } from "svelte";
2
- import { writable } from "svelte/store";
2
+ import { derived, writable } from "svelte/store";
3
3
  const key = Symbol("inputValidation");
4
4
  const initialSelectionsKey = Symbol("initialInputSelections");
5
5
  export function createInputValidationContext(initiallySatisfied = true) {
6
- const isSatisfied = writable(initiallySatisfied);
7
- const updateSatisfaction = (satisfied) => {
8
- isSatisfied.set(satisfied);
6
+ const satisfactionById = writable(new Map());
7
+ // `initiallySatisfied` only matters while no input has registered yet; once
8
+ // any input reports, the map is the single source of truth.
9
+ const isSatisfied = derived(satisfactionById, (byId) => byId.size === 0
10
+ ? initiallySatisfied
11
+ : Array.from(byId.values()).every(Boolean));
12
+ const updateSatisfaction = (id, satisfied) => {
13
+ satisfactionById.update((byId) => {
14
+ if (byId.get(id) === satisfied) {
15
+ return byId;
16
+ }
17
+ const next = new Map(byId);
18
+ next.set(id, satisfied);
19
+ return next;
20
+ });
21
+ };
22
+ const removeInput = (id) => {
23
+ satisfactionById.update((byId) => {
24
+ if (!byId.has(id)) {
25
+ return byId;
26
+ }
27
+ const next = new Map(byId);
28
+ next.delete(id);
29
+ return next;
30
+ });
9
31
  };
10
32
  return {
11
33
  isSatisfied,
12
34
  updateSatisfaction,
35
+ removeInput,
13
36
  };
14
37
  }
15
38
  export function setInputValidationContext(context) {
@@ -34,14 +34,14 @@ export class PaywallStateStore {
34
34
  registerDeclarations(declarations) {
35
35
  let changed = false;
36
36
  for (const [key, declaration] of Object.entries(declarations)) {
37
- if (Object.hasOwn(this.#declaredDefaults, key))
37
+ if (Object.prototype.hasOwnProperty.call(this.#declaredDefaults, key))
38
38
  continue;
39
39
  const normalized = this.#normalizeForType(declaration.default, declaration.type);
40
40
  if (normalized === undefined)
41
41
  continue;
42
42
  this.#declaredDefaults[key] = normalized;
43
43
  this.#declaredTypes[key] = declaration.type;
44
- if (!Object.hasOwn(this.#currentValues, key)) {
44
+ if (!Object.prototype.hasOwnProperty.call(this.#currentValues, key)) {
45
45
  this.#currentValues[key] = normalized;
46
46
  changed = true;
47
47
  }
@@ -6,6 +6,22 @@ import type { Overrides } from "../overrides";
6
6
  export type InputTextCapitalizeType = "none" | "sentences" | "words" | "characters";
7
7
  export type InputTextKeyboardType = "decimal" | "email" | "numeric" | "password" | "tel" | "text" | "url";
8
8
  export type ReservedAttribute = "$email" | "$displayName" | "$phoneNumber";
9
+ export type InputTextValidationFormat = "email" | "uri";
10
+ /**
11
+ * JSON Schema subset mapped to native HTML constraint attributes.
12
+ * Keys keep the JSON Schema camelCase vocabulary.
13
+ */
14
+ export interface InputTextValidation {
15
+ minimum?: number | null;
16
+ maximum?: number | null;
17
+ minLength?: number | null;
18
+ maxLength?: number | null;
19
+ pattern?: string | null;
20
+ format?: InputTextValidationFormat | null;
21
+ }
22
+ export type InputTextValidationRule = "required" | "minimum" | "maximum" | "minLength" | "maxLength" | "pattern" | "format";
23
+ /** Per-rule error copy; `validation_error_lid` is the fallback for missing keys. */
24
+ export type InputTextValidationErrorLids = Partial<Record<InputTextValidationRule, string | null>>;
9
25
  export interface InputTextProps extends BaseComponent {
10
26
  type: "input_text";
11
27
  visible?: boolean | null;
@@ -15,6 +31,9 @@ export interface InputTextProps extends BaseComponent {
15
31
  field_id: string;
16
32
  reserved_attribute?: ReservedAttribute | null;
17
33
  required: boolean;
34
+ validation?: InputTextValidation | null;
35
+ validation_error_lid?: string | null;
36
+ validation_error_lids?: InputTextValidationErrorLids | null;
18
37
  size: SizeType;
19
38
  padding: Spacing;
20
39
  margin: Spacing;
@@ -61,7 +61,7 @@ export type OverrideCondition = IntroOfferCondition | MultipleIntroOffersConditi
61
61
  /**
62
62
  * The reserved properties of an override that should not be included in the override properties
63
63
  */
64
- type ReservedProperty = "id" | "type" | "overrides" | "stack" | "components" | "items" | "pages" | "tabs" | "field_id" | "option_id" | "keyboard_type" | "capitalize";
64
+ type ReservedProperty = "id" | "type" | "overrides" | "stack" | "components" | "items" | "pages" | "tabs" | "field_id" | "option_id" | "keyboard_type" | "capitalize" | "validation" | "validation_error_lid" | "validation_error_lids";
65
65
  type OverrideProperties<C> = Partial<Omit<C, ReservedProperty>>;
66
66
  type Override<C> = {
67
67
  conditions: OverrideCondition[];
package/dist/types.d.ts CHANGED
@@ -128,4 +128,4 @@ export declare enum StackDistribution {
128
128
  export type { WorkflowScreen } from "./types/workflow";
129
129
  export type { ColorScheme } from "./types/colors";
130
130
  export type { ComponentInteractionData, ComponentInteractionType, OnComponentInteraction, } from "./types/paywall-component-interaction";
131
- export type { ReservedAttribute } from "./types/components/input-text";
131
+ export type { InputTextValidation, InputTextValidationErrorLids, InputTextValidationFormat, InputTextValidationRule, ReservedAttribute, } from "./types/components/input-text";
@@ -80,7 +80,7 @@ export function getSafeAreaFallbackColorHeadStyles(paywallData, hostFallbackColo
80
80
  }
81
81
  function colorAtPercent(sortedPoints, percent) {
82
82
  const firstPoint = sortedPoints[0];
83
- const lastPoint = sortedPoints.at(-1);
83
+ const lastPoint = sortedPoints[sortedPoints.length - 1];
84
84
  if (firstPoint == null || lastPoint == null) {
85
85
  return null;
86
86
  }
@@ -206,8 +206,8 @@ const conditionMatches = (condition, context) => {
206
206
  if (condition.type === RuleKey.StateCondition) {
207
207
  const stateValues = context.stateValues ?? {};
208
208
  const stateDefaults = context.stateDefaults ?? {};
209
- const hasCurrentValue = Object.hasOwn(stateValues, condition.name);
210
- const hasDeclaredDefault = Object.hasOwn(stateDefaults, condition.name);
209
+ const hasCurrentValue = Object.prototype.hasOwnProperty.call(stateValues, condition.name);
210
+ const hasDeclaredDefault = Object.prototype.hasOwnProperty.call(stateDefaults, condition.name);
211
211
  // Undeclared key (not in store, not declared): false for BOTH operators.
212
212
  // This diverges from variable_condition (where != matches a missing variable):
213
213
  // state keys are editor-managed, so an undeclared reference is always a bug,
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.8.21",
5
+ "version": "4.8.23",
6
6
  "author": {
7
7
  "name": "RevenueCat, Inc."
8
8
  },