@revenuecat/purchases-ui-js 2.2.0 → 3.0.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.
@@ -0,0 +1,365 @@
1
+ <script module lang="ts">
2
+ import Countdown from "./Countdown.svelte";
3
+ import type { CountdownProps } from "../../types/components/countdown";
4
+ import { componentDecorator } from "../../stories/component-decorator";
5
+ import { mockDateDecorator } from "storybook-mock-date-decorator";
6
+ import { localizations } from "../../stories/fixtures";
7
+ import { localizationDecorator } from "../../stories/localization-decorator";
8
+ import { variablesDecorator } from "../../stories/variables-decorator";
9
+ import { DEFAULT_TEXT_COLOR } from "../../utils/constants";
10
+ import { defineMeta } from "@storybook/addon-svelte-csf";
11
+
12
+ const defaultLocale = Object.keys(localizations)[0];
13
+
14
+ // Freeze date for consistent Chromatic snapshots
15
+ // Using a fixed date: 2024-01-15T12:00:00Z
16
+ const frozenDate = new Date("2024-01-15T12:00:00Z");
17
+
18
+ // Create dates relative to frozen date for consistent stories
19
+ // Target: 1 day, 23 hours, 45 minutes, 56 seconds from frozen date
20
+ // This equals 47 hours, 45 minutes, 56 seconds = 2024-01-17T11:45:56Z
21
+ const futureDate = new Date(frozenDate);
22
+ futureDate.setDate(futureDate.getDate() + 1); // +1 day = 24 hours
23
+ futureDate.setHours(futureDate.getHours() + 23); // +23 hours (total 47 hours from start)
24
+ futureDate.setMinutes(futureDate.getMinutes() + 45); // +45 minutes
25
+ futureDate.setSeconds(futureDate.getSeconds() + 56); // +56 seconds
26
+
27
+ // 3 days from frozen date (for multi-day story)
28
+ const threeDaysFromNow = new Date(frozenDate);
29
+ threeDaysFromNow.setDate(threeDaysFromNow.getDate() + 3);
30
+ threeDaysFromNow.setHours(threeDaysFromNow.getHours() + 5); // Add 5 hours to show hours too
31
+
32
+ // Create a date in the past for ended countdown
33
+ const pastDate = new Date(frozenDate);
34
+ pastDate.setHours(pastDate.getHours() - 1);
35
+
36
+ const { Story } = defineMeta({
37
+ title: "Components/Countdown",
38
+ component: Countdown,
39
+ decorators: [
40
+ mockDateDecorator,
41
+ componentDecorator(),
42
+ localizationDecorator({
43
+ defaultLocale,
44
+ localizations,
45
+ }),
46
+ variablesDecorator(undefined),
47
+ ],
48
+ parameters: {
49
+ date: frozenDate,
50
+ },
51
+ args: {
52
+ type: "countdown",
53
+ id: "countdown",
54
+ name: "Countdown",
55
+ style: {
56
+ type: "date",
57
+ date: futureDate.toISOString(),
58
+ },
59
+ countdown_stack: {
60
+ type: "stack",
61
+ id: "countdown-stack",
62
+ name: "Countdown Stack",
63
+ components: [
64
+ {
65
+ type: "text",
66
+ id: "countdown-text",
67
+ name: "Countdown Text",
68
+ text_lid: "countdown",
69
+ color: {
70
+ light: { type: "hex", value: DEFAULT_TEXT_COLOR },
71
+ },
72
+ font_size: "heading_xl",
73
+ font_weight: "bold",
74
+ horizontal_alignment: "center",
75
+ size: {
76
+ width: { type: "fit" },
77
+ height: { type: "fit" },
78
+ },
79
+ margin: { top: 0, trailing: 0, bottom: 0, leading: 0 },
80
+ padding: { top: 0, trailing: 0, bottom: 0, leading: 0 },
81
+ background_color: null,
82
+ },
83
+ ],
84
+ size: {
85
+ width: { type: "fit" },
86
+ height: { type: "fit" },
87
+ },
88
+ dimension: {
89
+ type: "vertical",
90
+ alignment: "center",
91
+ distribution: "center",
92
+ },
93
+ spacing: 8,
94
+ margin: { top: 0, trailing: 0, bottom: 0, leading: 0 },
95
+ padding: { top: 16, trailing: 16, bottom: 16, leading: 16 },
96
+ background_color: {
97
+ light: { type: "hex", value: "#F0F0F0" },
98
+ },
99
+ background: null,
100
+ border: null,
101
+ shape: {
102
+ type: "rectangle",
103
+ corners: {
104
+ top_leading: 8,
105
+ top_trailing: 8,
106
+ bottom_leading: 8,
107
+ bottom_trailing: 8,
108
+ },
109
+ },
110
+ shadow: null,
111
+ badge: null,
112
+ },
113
+ end_stack: {
114
+ type: "stack",
115
+ id: "end-stack",
116
+ name: "End Stack",
117
+ components: [
118
+ {
119
+ type: "text",
120
+ id: "end-text",
121
+ name: "End Text",
122
+ text_lid: "ended",
123
+ color: {
124
+ light: { type: "hex", value: "#FF0000" },
125
+ },
126
+ font_size: "heading_xl",
127
+ font_weight: "bold",
128
+ horizontal_alignment: "center",
129
+ size: {
130
+ width: { type: "fit" },
131
+ height: { type: "fit" },
132
+ },
133
+ margin: { top: 0, trailing: 0, bottom: 0, leading: 0 },
134
+ padding: { top: 0, trailing: 0, bottom: 0, leading: 0 },
135
+ background_color: null,
136
+ },
137
+ ],
138
+ size: {
139
+ width: { type: "fit" },
140
+ height: { type: "fit" },
141
+ },
142
+ dimension: {
143
+ type: "vertical",
144
+ alignment: "center",
145
+ distribution: "center",
146
+ },
147
+ spacing: 8,
148
+ margin: { top: 0, trailing: 0, bottom: 0, leading: 0 },
149
+ padding: { top: 16, trailing: 16, bottom: 16, leading: 16 },
150
+ background_color: {
151
+ light: { type: "hex", value: "#FFE0E0" },
152
+ },
153
+ background: null,
154
+ border: null,
155
+ shape: {
156
+ type: "rectangle",
157
+ corners: {
158
+ top_leading: 8,
159
+ top_trailing: 8,
160
+ bottom_leading: 8,
161
+ bottom_trailing: 8,
162
+ },
163
+ },
164
+ shadow: null,
165
+ badge: null,
166
+ },
167
+ } satisfies CountdownProps,
168
+ });
169
+ </script>
170
+
171
+ <Story
172
+ name="Active Countdown"
173
+ args={{
174
+ style: {
175
+ type: "date",
176
+ date: futureDate.toISOString(),
177
+ },
178
+ }}
179
+ />
180
+
181
+ <Story
182
+ name="Ended Countdown"
183
+ args={{
184
+ style: {
185
+ type: "date",
186
+ date: pastDate.toISOString(),
187
+ },
188
+ }}
189
+ />
190
+
191
+ <Story
192
+ name="Countdown with Variables"
193
+ args={{
194
+ style: {
195
+ type: "date",
196
+ date: futureDate.toISOString(),
197
+ },
198
+ countdown_stack: {
199
+ type: "stack",
200
+ id: "countdown-stack-vars",
201
+ name: "Countdown Stack with Variables",
202
+ components: [
203
+ {
204
+ type: "text",
205
+ id: "days",
206
+ name: "Days",
207
+ text_lid: "days",
208
+ color: {
209
+ light: { type: "hex", value: DEFAULT_TEXT_COLOR },
210
+ },
211
+ font_size: "heading_xxl",
212
+ font_weight: "bold",
213
+ horizontal_alignment: "center",
214
+ size: {
215
+ width: { type: "fit" },
216
+ height: { type: "fit" },
217
+ },
218
+ margin: { top: 0, trailing: 0, bottom: 0, leading: 0 },
219
+ padding: { top: 0, trailing: 0, bottom: 0, leading: 0 },
220
+ background_color: null,
221
+ },
222
+ {
223
+ type: "text",
224
+ id: "time",
225
+ name: "Time",
226
+ text_lid: "time",
227
+ color: {
228
+ light: { type: "hex", value: "#666666" },
229
+ },
230
+ font_size: "heading_l",
231
+ font_weight: "regular",
232
+ horizontal_alignment: "center",
233
+ size: {
234
+ width: { type: "fit" },
235
+ height: { type: "fit" },
236
+ },
237
+ margin: { top: 8, trailing: 0, bottom: 0, leading: 0 },
238
+ padding: { top: 0, trailing: 0, bottom: 0, leading: 0 },
239
+ background_color: null,
240
+ },
241
+ ],
242
+ size: {
243
+ width: { type: "fit" },
244
+ height: { type: "fit" },
245
+ },
246
+ dimension: {
247
+ type: "vertical",
248
+ alignment: "center",
249
+ distribution: "center",
250
+ },
251
+ spacing: 0,
252
+ margin: { top: 0, trailing: 0, bottom: 0, leading: 0 },
253
+ padding: { top: 32, trailing: 32, bottom: 32, leading: 32 },
254
+ background_color: {
255
+ light: { type: "hex", value: "#E8F5E9" },
256
+ },
257
+ background: null,
258
+ border: {
259
+ width: 2,
260
+ color: {
261
+ light: { type: "hex", value: "#4CAF50" },
262
+ },
263
+ },
264
+ shape: {
265
+ type: "rectangle",
266
+ corners: {
267
+ top_leading: 16,
268
+ top_trailing: 16,
269
+ bottom_leading: 16,
270
+ bottom_trailing: 16,
271
+ },
272
+ },
273
+ shadow: null,
274
+ badge: null,
275
+ },
276
+ }}
277
+ />
278
+
279
+ <Story
280
+ name="Countdown with Multiple Days"
281
+ args={{
282
+ style: {
283
+ type: "date",
284
+ date: threeDaysFromNow.toISOString(),
285
+ },
286
+ countdown_stack: {
287
+ type: "stack",
288
+ id: "countdown-stack-multi-day",
289
+ name: "Countdown Stack with Multiple Days",
290
+ components: [
291
+ {
292
+ type: "text",
293
+ id: "days",
294
+ name: "Days",
295
+ text_lid: "days",
296
+ color: {
297
+ light: { type: "hex", value: DEFAULT_TEXT_COLOR },
298
+ },
299
+ font_size: "heading_xxl",
300
+ font_weight: "bold",
301
+ horizontal_alignment: "center",
302
+ size: {
303
+ width: { type: "fit" },
304
+ height: { type: "fit" },
305
+ },
306
+ margin: { top: 0, trailing: 0, bottom: 0, leading: 0 },
307
+ padding: { top: 0, trailing: 0, bottom: 0, leading: 0 },
308
+ background_color: null,
309
+ },
310
+ {
311
+ type: "text",
312
+ id: "time",
313
+ name: "Time",
314
+ text_lid: "time",
315
+ color: {
316
+ light: { type: "hex", value: "#666666" },
317
+ },
318
+ font_size: "heading_l",
319
+ font_weight: "regular",
320
+ horizontal_alignment: "center",
321
+ size: {
322
+ width: { type: "fit" },
323
+ height: { type: "fit" },
324
+ },
325
+ margin: { top: 8, trailing: 0, bottom: 0, leading: 0 },
326
+ padding: { top: 0, trailing: 0, bottom: 0, leading: 0 },
327
+ background_color: null,
328
+ },
329
+ ],
330
+ size: {
331
+ width: { type: "fit" },
332
+ height: { type: "fit" },
333
+ },
334
+ dimension: {
335
+ type: "vertical",
336
+ alignment: "center",
337
+ distribution: "center",
338
+ },
339
+ spacing: 0,
340
+ margin: { top: 0, trailing: 0, bottom: 0, leading: 0 },
341
+ padding: { top: 32, trailing: 32, bottom: 32, leading: 32 },
342
+ background_color: {
343
+ light: { type: "hex", value: "#E3F2FD" },
344
+ },
345
+ background: null,
346
+ border: {
347
+ width: 2,
348
+ color: {
349
+ light: { type: "hex", value: "#2196F3" },
350
+ },
351
+ },
352
+ shape: {
353
+ type: "rectangle",
354
+ corners: {
355
+ top_leading: 16,
356
+ top_trailing: 16,
357
+ bottom_leading: 16,
358
+ bottom_trailing: 16,
359
+ },
360
+ },
361
+ shadow: null,
362
+ badge: null,
363
+ },
364
+ }}
365
+ />
@@ -0,0 +1,19 @@
1
+ import Countdown from "./Countdown.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 Countdown: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
16
+ [evt: string]: CustomEvent<any>;
17
+ }, {}, {}, string>;
18
+ type Countdown = InstanceType<typeof Countdown>;
19
+ export default Countdown;
@@ -0,0 +1,55 @@
1
+ <script lang="ts">
2
+ import Stack from "../stack/Stack.svelte";
3
+ import {
4
+ getVariablesContext,
5
+ setVariablesContext,
6
+ } from "../../stores/variables";
7
+ import type { CountdownProps } from "../../types/components/countdown";
8
+ import { derived, writable } from "svelte/store";
9
+ import {
10
+ calculateCountdownValues,
11
+ hasCountdownEnded,
12
+ } from "./countdown-utils";
13
+
14
+ const props: CountdownProps = $props();
15
+ const { countdown_stack, end_stack } = props;
16
+
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));
20
+
21
+ // Provide countdown variables to child Text components
22
+ const fallbackVariables = getVariablesContext();
23
+ const countdownValuesStore = writable(countdownValues);
24
+ const variables = derived(
25
+ [fallbackVariables, countdownValuesStore],
26
+ ([fallback, countdown]) => ({
27
+ ...fallback,
28
+ ...countdown,
29
+ }),
30
+ );
31
+ setVariablesContext(variables);
32
+
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
+ // Determine which stack to show
45
+ const activeStack = $derived.by(() => {
46
+ // If countdown has ended and end_stack exists, show end_stack
47
+ if (isEnded && end_stack) {
48
+ return end_stack;
49
+ }
50
+ // Otherwise show countdown_stack
51
+ return countdown_stack;
52
+ });
53
+ </script>
54
+
55
+ <Stack {...activeStack} />
@@ -0,0 +1,4 @@
1
+ import type { CountdownProps } from "../../types/components/countdown";
2
+ declare const Countdown: import("svelte").Component<CountdownProps, {}, "">;
3
+ type Countdown = ReturnType<typeof Countdown>;
4
+ export default Countdown;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Calculates countdown values from a target date
3
+ * Returns 8 countdown variables for use in Text components:
4
+ * - count_days_with_zero / count_days_without_zero
5
+ * - count_hours_with_zero / count_hours_without_zero
6
+ * - count_minutes_with_zero / count_minutes_without_zero
7
+ * - count_seconds_with_zero / count_seconds_without_zero
8
+ */
9
+ export declare function calculateCountdownValues(targetDate: string): Record<string, string>;
10
+ /**
11
+ * Checks if a countdown has ended (target date is in the past)
12
+ */
13
+ export declare function hasCountdownEnded(targetDate: string): boolean;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Formats a number with leading zero if it's less than 10
3
+ */
4
+ function formatWithZero(value) {
5
+ return value < 10 ? `0${value}` : `${value}`;
6
+ }
7
+ /**
8
+ * Formats a number without leading zero
9
+ */
10
+ function formatWithoutZero(value) {
11
+ return `${value}`;
12
+ }
13
+ /**
14
+ * Calculates countdown values from a target date
15
+ * Returns 8 countdown variables for use in Text components:
16
+ * - count_days_with_zero / count_days_without_zero
17
+ * - count_hours_with_zero / count_hours_without_zero
18
+ * - count_minutes_with_zero / count_minutes_without_zero
19
+ * - count_seconds_with_zero / count_seconds_without_zero
20
+ */
21
+ export function calculateCountdownValues(targetDate) {
22
+ const now = new Date();
23
+ const target = new Date(targetDate);
24
+ const diffInMs = target.getTime() - now.getTime();
25
+ // If past target date, return all zeros
26
+ if (diffInMs <= 0) {
27
+ return {
28
+ count_days_with_zero: "00",
29
+ count_days_without_zero: "0",
30
+ count_hours_with_zero: "00",
31
+ count_hours_without_zero: "0",
32
+ count_minutes_with_zero: "00",
33
+ count_minutes_without_zero: "0",
34
+ count_seconds_with_zero: "00",
35
+ count_seconds_without_zero: "0",
36
+ };
37
+ }
38
+ // Calculate time units
39
+ const totalSeconds = Math.floor(diffInMs / 1000);
40
+ const totalMinutes = Math.floor(totalSeconds / 60);
41
+ const totalHours = Math.floor(totalMinutes / 60);
42
+ const totalDays = Math.floor(totalHours / 24);
43
+ // Calculate remaining partial units (hours 0-23, minutes 0-59, seconds 0-59)
44
+ const days = totalDays;
45
+ const hours = totalHours % 24;
46
+ const minutes = totalMinutes % 60;
47
+ const seconds = totalSeconds % 60;
48
+ return {
49
+ count_days_with_zero: formatWithZero(days),
50
+ count_days_without_zero: formatWithoutZero(days),
51
+ count_hours_with_zero: formatWithZero(hours),
52
+ count_hours_without_zero: formatWithoutZero(hours),
53
+ count_minutes_with_zero: formatWithZero(minutes),
54
+ count_minutes_without_zero: formatWithoutZero(minutes),
55
+ count_seconds_with_zero: formatWithZero(seconds),
56
+ count_seconds_without_zero: formatWithoutZero(seconds),
57
+ };
58
+ }
59
+ /**
60
+ * Checks if a countdown has ended (target date is in the past)
61
+ */
62
+ export function hasCountdownEnded(targetDate) {
63
+ const now = new Date();
64
+ const target = new Date(targetDate);
65
+ return target.getTime() <= now.getTime();
66
+ }
@@ -0,0 +1,2 @@
1
+ import type { PaywallData } from "../../../types/paywall";
2
+ export declare const COUNTDOWN_PAYWALL: PaywallData;