@revenuecat/purchases-ui-js 4.8.2 → 4.8.3

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.
@@ -5,24 +5,26 @@
5
5
  setVariablesContext,
6
6
  } from "../../stores/variables";
7
7
  import type { CountdownProps } from "../../types/components/countdown";
8
- import { derived, writable } from "svelte/store";
8
+ import { derived, toStore } from "svelte/store";
9
9
  import {
10
10
  calculateCountdownValues,
11
11
  hasCountdownEnded,
12
12
  } from "./countdown-utils";
13
+ import { sharedNow } from "./shared-ticker";
13
14
 
14
15
  const props: CountdownProps = $props();
15
16
  const { countdown_stack, end_stack } = props;
16
17
 
17
- // State for countdown values that updates every second
18
- let countdownValues = $state(calculateCountdownValues(props.style.date));
19
- let isEnded = $state(hasCountdownEnded(props.style.date));
18
+ // All countdowns read one shared ticker so they stay in lockstep.
19
+ const countdownValues = $derived(
20
+ calculateCountdownValues(props.style.date, $sharedNow),
21
+ );
22
+ const isEnded = $derived(hasCountdownEnded(props.style.date, $sharedNow));
20
23
 
21
- // Provide countdown variables to child Text components
24
+ // Expose countdown variables to child Text components via context.
22
25
  const fallbackVariables = getVariablesContext();
23
- const countdownValuesStore = writable(countdownValues);
24
26
  const variables = derived(
25
- [fallbackVariables, countdownValuesStore],
27
+ [fallbackVariables, toStore(() => countdownValues)],
26
28
  ([fallback, countdown]) => ({
27
29
  ...fallback,
28
30
  ...countdown,
@@ -30,17 +32,6 @@
30
32
  );
31
33
  setVariablesContext(variables);
32
34
 
33
- // Setup interval to update countdown every second
34
- $effect(() => {
35
- const interval = setInterval(() => {
36
- countdownValues = calculateCountdownValues(props.style.date);
37
- isEnded = hasCountdownEnded(props.style.date);
38
- countdownValuesStore.set(countdownValues);
39
- }, 1000);
40
-
41
- return () => clearInterval(interval);
42
- });
43
-
44
35
  // Determine which stack to show
45
36
  const activeStack = $derived.by(() => {
46
37
  // If countdown has ended and end_stack exists, show end_stack
@@ -6,8 +6,8 @@
6
6
  * - count_minutes_with_zero / count_minutes_without_zero
7
7
  * - count_seconds_with_zero / count_seconds_without_zero
8
8
  */
9
- export declare function calculateCountdownValues(targetDate: string): Record<string, string>;
9
+ export declare function calculateCountdownValues(targetDate: string, nowMs?: number): Record<string, string>;
10
10
  /**
11
11
  * Checks if a countdown has ended (target date is in the past)
12
12
  */
13
- export declare function hasCountdownEnded(targetDate: string): boolean;
13
+ export declare function hasCountdownEnded(targetDate: string, nowMs?: number): boolean;
@@ -18,10 +18,9 @@ function formatWithoutZero(value) {
18
18
  * - count_minutes_with_zero / count_minutes_without_zero
19
19
  * - count_seconds_with_zero / count_seconds_without_zero
20
20
  */
21
- export function calculateCountdownValues(targetDate) {
22
- const now = new Date();
21
+ export function calculateCountdownValues(targetDate, nowMs = Date.now()) {
23
22
  const target = new Date(targetDate);
24
- const diffInMs = target.getTime() - now.getTime();
23
+ const diffInMs = target.getTime() - nowMs;
25
24
  // If past target date, return all zeros
26
25
  if (diffInMs <= 0) {
27
26
  return {
@@ -59,8 +58,7 @@ export function calculateCountdownValues(targetDate) {
59
58
  /**
60
59
  * Checks if a countdown has ended (target date is in the past)
61
60
  */
62
- export function hasCountdownEnded(targetDate) {
63
- const now = new Date();
61
+ export function hasCountdownEnded(targetDate, nowMs = Date.now()) {
64
62
  const target = new Date(targetDate);
65
- return target.getTime() <= now.getTime();
63
+ return target.getTime() <= nowMs;
66
64
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Wall-clock now (epoch ms) shared by every countdown so they tick in lockstep.
3
+ * The interval runs only while at least one countdown is subscribed: Svelte's
4
+ * `readable` starts on the first subscriber and stops on the last.
5
+ */
6
+ export declare const sharedNow: import("svelte/store").Readable<number>;
@@ -0,0 +1,20 @@
1
+ import { readable } from "svelte/store";
2
+ /**
3
+ * Wall-clock now (epoch ms) shared by every countdown so they tick in lockstep.
4
+ * The interval runs only while at least one countdown is subscribed: Svelte's
5
+ * `readable` starts on the first subscriber and stops on the last.
6
+ */
7
+ export const sharedNow = readable(Date.now(), (set) => {
8
+ set(Date.now());
9
+ // Align the first tick to the whole second, then tick every second.
10
+ let interval;
11
+ const boundaryTimeout = setTimeout(() => {
12
+ set(Date.now());
13
+ interval = setInterval(() => set(Date.now()), 1000);
14
+ }, 1000 - (Date.now() % 1000));
15
+ return () => {
16
+ clearTimeout(boundaryTimeout);
17
+ if (interval !== undefined)
18
+ clearInterval(interval);
19
+ };
20
+ });
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { getColorModeContext } from "../../stores/color-mode";
3
+ import { getInputValidationContext } from "../../stores/inputValidation";
3
4
  import { getLocalizationContext } from "../../stores/localization";
4
5
  import { getOptionalPackageInfoContext } from "../../stores/packageInfo";
5
6
  import { getPaywallContext } from "../../stores/paywall";
@@ -35,6 +36,7 @@
35
36
 
36
37
  let focused = $state(false);
37
38
  let error = $state(false);
39
+ let valid = $state(!props.required);
38
40
 
39
41
  const {
40
42
  placeholder_lid,
@@ -69,12 +71,7 @@
69
71
  };
70
72
  });
71
73
 
72
- function handleValueChange(input: HTMLInputElement) {
73
- error = !input.validity.valid;
74
- if (error) {
75
- return;
76
- }
77
-
74
+ function commitValue(input: HTMLInputElement) {
78
75
  const value = input.value.trim();
79
76
  if (reserved_attribute) {
80
77
  if (!onReservedAttributeChanged) {
@@ -87,17 +84,35 @@
87
84
  }
88
85
  }
89
86
 
87
+ function handleValueChange(input: HTMLInputElement) {
88
+ error = !input.validity.valid;
89
+ if (error) {
90
+ return;
91
+ }
92
+ commitValue(input);
93
+ }
94
+
90
95
  const oninput: FormEventHandler<HTMLInputElement> = (event) => {
96
+ const input = event.currentTarget;
97
+ valid = input.validity.valid;
98
+ // Keep the propagated value in lockstep with `valid` so Continue can
99
+ // never unlock before the workflow has received the current value.
100
+ if (valid) {
101
+ commitValue(input);
102
+ }
91
103
  if (error) {
92
- handleValueChange(event.currentTarget);
104
+ handleValueChange(input);
93
105
  }
94
106
  };
95
107
 
96
108
  const onblur: FormEventHandler<HTMLInputElement> = (event) => {
97
109
  focused = false;
110
+ valid = event.currentTarget.validity.valid;
98
111
  handleValueChange(event.currentTarget);
99
112
  };
100
113
 
114
+ const validationContext = getInputValidationContext();
115
+
101
116
  const getColorMode = getColorModeContext();
102
117
  const colorMode = $derived(getColorMode());
103
118
  const {
@@ -201,6 +216,14 @@
201
216
  props.visible,
202
217
  ),
203
218
  );
219
+
220
+ if (validationContext) {
221
+ $effect(() => {
222
+ // A hidden field can't be filled in, so it shouldn't be able to block
223
+ // Continue for the rest of the screen.
224
+ validationContext.updateSatisfaction(!isVisible || valid);
225
+ });
226
+ }
204
227
  </script>
205
228
 
206
229
  {#if isVisible}
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ import { setColorModeContext } from "../../stores/color-mode";
3
+ import {
4
+ createInputValidationContext,
5
+ setInputValidationContext,
6
+ } from "../../stores/inputValidation";
7
+ import { setLocalizationContext } from "../../stores/localization";
8
+ import { setPackageInfoContext } from "../../stores/packageInfo";
9
+ import { setPaywallContext } from "../../stores/paywall";
10
+ import { setVariablesContext } from "../../stores/variables";
11
+ import { readable, writable } from "svelte/store";
12
+ import InputText from "./InputText.svelte";
13
+ import type { InputTextProps } from "../../types/components/input-text";
14
+
15
+ interface Props extends InputTextProps {
16
+ onInputChanged?: (fieldId: string, value: string) => void;
17
+ onSatisfactionChange?: (satisfied: boolean) => void;
18
+ }
19
+
20
+ const { onInputChanged, onSatisfactionChange, ...inputTextProps }: Props =
21
+ $props();
22
+
23
+ const validationContext = createInputValidationContext();
24
+ setInputValidationContext(validationContext);
25
+ if (onSatisfactionChange) {
26
+ validationContext.isSatisfied.subscribe(onSatisfactionChange);
27
+ }
28
+
29
+ setColorModeContext();
30
+ setLocalizationContext(() => ({
31
+ defaultLocale: "en_US",
32
+ localizations: {
33
+ en_US: {},
34
+ },
35
+ }));
36
+
37
+ setPaywallContext({
38
+ selectedPackageId: writable(undefined),
39
+ variablesPerPackage: readable(undefined),
40
+ baseVariables: readable(undefined),
41
+ infoPerPackage: readable(undefined),
42
+ onPurchase: () => {},
43
+ emitComponentInteraction: () => {},
44
+ onButtonAction: () => {},
45
+ onInputChanged,
46
+ uiConfig: { app: { fonts: {} } } as never,
47
+ hideBackButtons: false,
48
+ });
49
+ setPackageInfoContext(readable(undefined));
50
+ setVariablesContext(readable({}));
51
+ </script>
52
+
53
+ <InputText {...inputTextProps} />
@@ -0,0 +1,8 @@
1
+ import type { InputTextProps } from "../../types/components/input-text";
2
+ interface Props extends InputTextProps {
3
+ onInputChanged?: (fieldId: string, value: string) => void;
4
+ onSatisfactionChange?: (satisfied: boolean) => void;
5
+ }
6
+ declare const InputTextTestWrapper: import("svelte").Component<Props, {}, "">;
7
+ type InputTextTestWrapper = ReturnType<typeof InputTextTestWrapper>;
8
+ export default InputTextTestWrapper;
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.2",
5
+ "version": "4.8.3",
6
6
  "author": {
7
7
  "name": "RevenueCat, Inc."
8
8
  },