@revenuecat/purchases-ui-js 3.10.0 → 3.11.1

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.
@@ -1,11 +1,12 @@
1
1
  <script lang="ts">
2
2
  import Stack from "../stack/Stack.svelte";
3
3
  import type { FooterProps } from "../../types/components/footer";
4
+ import { STICKY_OVERLAY_Z_INDEX } from "../../utils/constants";
4
5
 
5
6
  const { stack }: FooterProps = $props();
6
7
  </script>
7
8
 
8
- <div>
9
+ <div style="z-index: {STICKY_OVERLAY_Z_INDEX};">
9
10
  <Stack {...stack} />
10
11
  </div>
11
12
 
@@ -14,7 +15,6 @@
14
15
  position: sticky;
15
16
  bottom: 0;
16
17
  width: 100%;
17
- z-index: 1000;
18
18
 
19
19
  display: flex;
20
20
  flex-direction: row;
@@ -0,0 +1,140 @@
1
+ <script module lang="ts">
2
+ import Header from "./Header.svelte";
3
+ import { componentDecorator } from "../../stories/component-decorator";
4
+ import { localizationDecorator } from "../../stories/localization-decorator";
5
+ import { viewportDecorator } from "../../stories/viewport-decorator";
6
+ import type { HeaderProps } from "../../types/components/header";
7
+ import { defineMeta } from "@storybook/addon-svelte-csf";
8
+
9
+ const defaultLocale = "en_US";
10
+
11
+ const { Story } = defineMeta({
12
+ title: "Components/Header",
13
+ component: Header,
14
+ decorators: [
15
+ componentDecorator(),
16
+ localizationDecorator({
17
+ defaultLocale,
18
+ localizations: {
19
+ [defaultLocale]: {
20
+ "2wCGhwfDvx": "Welcome to the app!",
21
+ },
22
+ },
23
+ }),
24
+ ],
25
+ args: {
26
+ type: "header",
27
+ id: "header",
28
+ name: "Header",
29
+ stack: {
30
+ background: {
31
+ type: "color",
32
+ value: {
33
+ light: {
34
+ type: "hex",
35
+ value: "#BEEEEFFF",
36
+ },
37
+ },
38
+ },
39
+ background_color: null,
40
+ badge: null,
41
+ border: null,
42
+ components: [
43
+ {
44
+ background_color: null,
45
+ color: {
46
+ light: {
47
+ type: "hex",
48
+ value: "#000000",
49
+ },
50
+ },
51
+ font_name: null,
52
+ font_size: 14,
53
+ font_weight: "regular",
54
+ font_weight_int: 400,
55
+ horizontal_alignment: "leading",
56
+ id: "Fhqh8-bFi_",
57
+ margin: {
58
+ bottom: 0,
59
+ leading: 0,
60
+ top: 0,
61
+ trailing: 0,
62
+ },
63
+ name: "",
64
+ padding: {
65
+ bottom: 0,
66
+ leading: 0,
67
+ top: 0,
68
+ trailing: 0,
69
+ },
70
+ size: {
71
+ height: {
72
+ type: "fit",
73
+ value: null,
74
+ },
75
+ width: {
76
+ type: "fit",
77
+ value: null,
78
+ },
79
+ },
80
+ text_lid: "2wCGhwfDvx",
81
+ type: "text",
82
+ },
83
+ ],
84
+ dimension: {
85
+ alignment: "center",
86
+ distribution: "start",
87
+ type: "vertical",
88
+ },
89
+ id: "n_EBzFgjzA",
90
+ margin: {
91
+ bottom: 0,
92
+ leading: 0,
93
+ top: 0,
94
+ trailing: 0,
95
+ },
96
+ name: "Header",
97
+ padding: {
98
+ bottom: 20,
99
+ leading: 0,
100
+ top: 20,
101
+ trailing: 0,
102
+ },
103
+ shadow: {
104
+ color: {
105
+ light: {
106
+ type: "hex",
107
+ value: "#00000033",
108
+ },
109
+ },
110
+ radius: 16,
111
+ x: 0,
112
+ y: 4,
113
+ },
114
+ shape: {
115
+ corners: {
116
+ bottom_leading: 0,
117
+ bottom_trailing: 0,
118
+ top_leading: 0,
119
+ top_trailing: 0,
120
+ },
121
+ type: "rectangle",
122
+ },
123
+ size: {
124
+ height: {
125
+ type: "fit",
126
+ value: null,
127
+ },
128
+ width: {
129
+ type: "fill",
130
+ value: null,
131
+ },
132
+ },
133
+ spacing: 0,
134
+ type: "stack",
135
+ },
136
+ } satisfies HeaderProps,
137
+ });
138
+ </script>
139
+
140
+ <Story name="Header component" />
@@ -0,0 +1,19 @@
1
+ import Header from "./Header.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 Header: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
16
+ [evt: string]: CustomEvent<any>;
17
+ }, {}, {}, string>;
18
+ type Header = InstanceType<typeof Header>;
19
+ export default Header;
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ import Stack from "../stack/Stack.svelte";
3
+ import type { HeaderProps } from "../../types/components/header";
4
+
5
+ const { stack }: HeaderProps = $props();
6
+ </script>
7
+
8
+ <div>
9
+ <Stack {...stack} />
10
+ </div>
11
+
12
+ <style>
13
+ div {
14
+ display: flex;
15
+ flex-direction: row;
16
+ justify-content: center;
17
+ }
18
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { HeaderProps } from "../../types/components/header";
2
+ declare const Header: import("svelte").Component<HeaderProps, {}, "">;
3
+ type Header = ReturnType<typeof Header>;
4
+ export default Header;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import {
3
3
  Footer,
4
+ Header,
4
5
  Image,
5
6
  Package,
6
7
  PurchaseButton,
@@ -38,6 +39,7 @@
38
39
  countdown: Countdown,
39
40
  express_purchase_button: ExpressPurchaseButton,
40
41
  footer: Footer,
42
+ header: Header,
41
43
  icon: Icon,
42
44
  image: Image,
43
45
  input_multiple_choice: InputMultipleChoice,
@@ -84,6 +86,10 @@
84
86
  ];
85
87
  }
86
88
 
89
+ if (nodeData.type === "fallback_header") {
90
+ return undefined;
91
+ }
92
+
87
93
  const { fallback } = nodeData;
88
94
  if (fallback && ComponentTypes[fallback?.type]) {
89
95
  return [
@@ -14,6 +14,8 @@
14
14
  pastaPaywallData,
15
15
  paywallData,
16
16
  paywallWithFooter,
17
+ paywallWithHeader,
18
+ paywallWithTransparentHeaderAndTopImage,
17
19
  posterMakerTemplate,
18
20
  timelineTemplate,
19
21
  uiConfigData,
@@ -323,6 +325,19 @@
323
325
  }}
324
326
  />
325
327
 
328
+ <Story
329
+ name="Header"
330
+ args={{
331
+ paywallData: paywallWithHeader,
332
+ }}
333
+ />
334
+
335
+ <Story
336
+ name="Header with transparent background and top image"
337
+ args={{
338
+ paywallData: paywallWithTransparentHeaderAndTopImage,
339
+ }}
340
+ />
326
341
  <Story
327
342
  name="Timeline"
328
343
  args={{
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import Footer from "../footer/Footer.svelte";
3
+ import Header from "../header/Header.svelte";
3
4
  import { setColorModeContext } from "../../stores/color-mode";
4
5
  import {
5
6
  createInputValidationContext,
@@ -29,6 +30,7 @@
29
30
  } from "../../types/variables";
30
31
  import { mapBackground } from "../../utils/background-utils";
31
32
  import { css } from "../../utils/base-utils";
33
+ import { STICKY_OVERLAY_Z_INDEX } from "../../utils/constants";
32
34
  import { registerFonts } from "../../utils/font-utils";
33
35
  import { findSelectedPackageId } from "../../utils/style-utils";
34
36
  import { onMount } from "svelte";
@@ -244,16 +246,38 @@
244
246
 
245
247
  const paywallClass = $derived(sheet ? "paywall blur" : "paywall");
246
248
 
247
- const { stack, sticky_footer } = base;
249
+ const { stack, sticky_footer, header } = base;
250
+
251
+ const firstComponent = stack.components[0];
252
+ const firstComponentIsFullWidthImage =
253
+ firstComponent?.type === "image" &&
254
+ firstComponent.size.width.type === "fill";
255
+ const pullContentUnderHeader = !!header && firstComponentIsFullWidthImage;
256
+
257
+ let headerHeight = $state(0);
248
258
  </script>
249
259
 
250
260
  <svelte:boundary onerror={onError}>
251
261
  <div class={paywallClass} {style}>
262
+ {#if header}
263
+ <div
264
+ class="header-wrapper"
265
+ style="z-index: {STICKY_OVERLAY_Z_INDEX};"
266
+ bind:clientHeight={headerHeight}
267
+ >
268
+ <Header {...header} />
269
+ </div>
270
+ {/if}
252
271
  <div
253
272
  class="paywall-content"
254
- style={maxContentWidth
255
- ? `max-width: ${maxContentWidth}; margin-inline: auto;`
256
- : ""}
273
+ style={[
274
+ maxContentWidth
275
+ ? `max-width: ${maxContentWidth}; margin-inline: auto;`
276
+ : "",
277
+ pullContentUnderHeader ? `margin-top: -${headerHeight}px;` : "",
278
+ ]
279
+ .filter(Boolean)
280
+ .join(" ")}
257
281
  >
258
282
  <Stack {...stack} class="paywall-content-scroll" />
259
283
  {#if sticky_footer}
@@ -286,6 +310,12 @@
286
310
  }
287
311
  }
288
312
 
313
+ .header-wrapper {
314
+ position: sticky;
315
+ top: 0;
316
+ width: 100%;
317
+ }
318
+
289
319
  .paywall-content {
290
320
  width: 100%;
291
321
  flex: 1;
@@ -107,6 +107,16 @@
107
107
  }}
108
108
  />
109
109
 
110
+ <Story
111
+ name="Autoplay Unmuted"
112
+ args={{
113
+ show_controls: false,
114
+ auto_play: true,
115
+ mute_audio: false,
116
+ loop: true,
117
+ }}
118
+ />
119
+
110
120
  <Story
111
121
  name="With Fallback Image"
112
122
  args={{
@@ -1,10 +1,12 @@
1
1
  <script lang="ts">
2
+ import { untrack } from "svelte";
2
3
  import { getColorModeContext } from "../../stores/color-mode";
3
4
  import { getSelectedStateContext } from "../../stores/selected";
4
5
  import type { VideoProps } from "../../types/components/video";
5
6
  import type { VideoFiles, ImageFiles } from "../../types/media";
6
7
  import {
7
8
  css,
9
+ mapBorderRadius,
8
10
  mapColor,
9
11
  mapColorMode,
10
12
  mapFitMode,
@@ -59,6 +61,7 @@
59
61
  });
60
62
  let videoElement = $state<HTMLVideoElement | null>(null);
61
63
  let hasVideoError = $state(false);
64
+ let showControlsFallback = $state(false);
62
65
 
63
66
  // Load video or fallback image metadata to get dimensions
64
67
  $effect(() => {
@@ -157,87 +160,162 @@
157
160
 
158
161
  const { x, y, radius, color } = shadow;
159
162
  const shadowColor = mapColor(colorMode, color);
160
- return `filter: drop-shadow(${x}px ${y}px ${radius}px ${shadowColor})`;
163
+ return `filter:drop-shadow(${x}px ${y}px ${radius}px ${shadowColor})`;
161
164
  });
162
165
 
166
+ const wrapperStyle = $derived.by(() => {
167
+ return svgStyle ? `${style};${svgStyle}` : style;
168
+ });
169
+
170
+ const clipDivStyle = $derived(
171
+ css({
172
+ width: "100%",
173
+ height: "100%",
174
+ overflow: "hidden",
175
+ "border-radius": mapBorderRadius(mask_shape),
176
+ "clip-path":
177
+ mask_shape?.type === "concave" || mask_shape?.type === "convex"
178
+ ? `url(#${id}-path)`
179
+ : "none",
180
+ }),
181
+ );
182
+
163
183
  const overlay = $derived(
164
184
  color_overlay && mapColorMode(colorMode, color_overlay),
165
185
  );
166
186
 
167
187
  const shouldShowFallback = $derived(hasVideoError || !video);
168
- </script>
169
188
 
170
- <div {style} bind:clientWidth={wrapperWidth}>
171
- <svg
172
- bind:contentRect={svgRect}
173
- width="100%"
174
- height="100%"
175
- {viewBox}
176
- style={svgStyle}
177
- >
178
- <defs>
179
- <clipPath id={`${id}-path`}>
180
- <ClipPath shape={mask_shape} width={svgWidth} height={svgHeight} />
181
- </clipPath>
182
-
183
- <g id={`${id}-border`}>
184
- <ClipPath shape={mask_shape} width={svgWidth} height={svgHeight} />
185
- </g>
189
+ // Programmatic autoplay (single path; HTML autoplay omitted to avoid iOS quirks).
190
+ $effect(() => {
191
+ if (!auto_play) {
192
+ untrack(() => {
193
+ showControlsFallback = false;
194
+ });
195
+ return;
196
+ }
186
197
 
187
- <Overlay id={`${id}-overlay`} {overlay} />
188
- </defs>
198
+ if (shouldShowFallback || !video) {
199
+ untrack(() => {
200
+ showControlsFallback = false;
201
+ });
202
+ return;
203
+ }
189
204
 
190
- {#if border && border.width > 0}
191
- <use href={`#${id}-border`} fill="transparent" />
192
- {/if}
205
+ const el = videoElement;
206
+ if (!el) {
207
+ return;
208
+ }
193
209
 
194
- <g clip-path={`url(#${id}-path)`}>
195
- <foreignObject x="0" y="0" width="100%" height="100%">
196
- {#if shouldShowFallback && fallbackImage}
197
- <img
198
- src={fallbackImage.original}
199
- style="object-fit:{mapFitMode(fit_mode)}"
200
- alt=""
201
- />
202
- {:else if video}
203
- <video
204
- bind:this={videoElement}
205
- src={video.url}
206
- style="object-fit:{mapFitMode(fit_mode)}"
207
- autoplay={auto_play}
208
- {loop}
209
- muted={mute_audio}
210
- controls={show_controls}
211
- playsinline
212
- >
213
- <track kind="captions" />
214
- </video>
215
- {/if}
216
- </foreignObject>
217
-
218
- {#if overlay}
219
- <rect
220
- x="0"
221
- y="0"
222
- height="100%"
223
- width="100%"
224
- fill={`url(#${id}-overlay)`}
210
+ void video.url;
211
+ void mute_audio;
212
+
213
+ untrack(() => {
214
+ showControlsFallback = false;
215
+ });
216
+
217
+ const playPromise = el.play();
218
+ if (playPromise !== undefined) {
219
+ playPromise.catch(() => {
220
+ untrack(() => {
221
+ showControlsFallback = true;
222
+ });
223
+ });
224
+ }
225
+ });
226
+ </script>
227
+
228
+ <div class="video-outer" style={wrapperStyle} bind:clientWidth={wrapperWidth}>
229
+ <div class="video-media">
230
+ <div class="video-clip" style={clipDivStyle}>
231
+ {#if shouldShowFallback && fallbackImage}
232
+ <img
233
+ src={fallbackImage.original}
234
+ style="object-fit:{mapFitMode(fit_mode)}"
235
+ alt=""
225
236
  />
237
+ {:else if video}
238
+ <video
239
+ bind:this={videoElement}
240
+ src={video.url}
241
+ style="object-fit:{mapFitMode(fit_mode)}"
242
+ {loop}
243
+ muted={mute_audio}
244
+ controls={show_controls || showControlsFallback}
245
+ playsinline
246
+ >
247
+ <track kind="captions" />
248
+ </video>
226
249
  {/if}
250
+ </div>
251
+
252
+ <svg
253
+ class="video-deco"
254
+ bind:contentRect={svgRect}
255
+ width="100%"
256
+ height="100%"
257
+ {viewBox}
258
+ >
259
+ <defs>
260
+ <clipPath id={`${id}-path`}>
261
+ <ClipPath shape={mask_shape} width={svgWidth} height={svgHeight} />
262
+ </clipPath>
263
+
264
+ <g id={`${id}-border`}>
265
+ <ClipPath shape={mask_shape} width={svgWidth} height={svgHeight} />
266
+ </g>
267
+
268
+ <Overlay id={`${id}-overlay`} {overlay} />
269
+ </defs>
227
270
 
228
271
  {#if border && border.width > 0}
229
- <use
230
- href={`#${id}-border`}
231
- fill="none"
232
- stroke={mapColor(colorMode, border.color)}
233
- stroke-width={border.width}
234
- />
272
+ <use href={`#${id}-border`} fill="transparent" />
235
273
  {/if}
236
- </g>
237
- </svg>
274
+
275
+ <g clip-path={`url(#${id}-path)`}>
276
+ {#if overlay}
277
+ <rect
278
+ x="0"
279
+ y="0"
280
+ height="100%"
281
+ width="100%"
282
+ fill={`url(#${id}-overlay)`}
283
+ />
284
+ {/if}
285
+
286
+ {#if border && border.width > 0}
287
+ <use
288
+ href={`#${id}-border`}
289
+ fill="none"
290
+ stroke={mapColor(colorMode, border.color)}
291
+ stroke-width={border.width}
292
+ />
293
+ {/if}
294
+ </g>
295
+ </svg>
296
+ </div>
238
297
  </div>
239
298
 
240
299
  <style>
300
+ .video-outer {
301
+ position: relative;
302
+ }
303
+
304
+ .video-media {
305
+ position: relative;
306
+ width: 100%;
307
+ height: 100%;
308
+ min-height: 0;
309
+ }
310
+
311
+ .video-deco {
312
+ position: absolute;
313
+ inset: 0;
314
+ width: 100%;
315
+ height: 100%;
316
+ pointer-events: none;
317
+ }
318
+
241
319
  video,
242
320
  img {
243
321
  width: 100%;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { default as ButtonDeprecated } from "./components/button/Button.svelte";
2
2
  export { default as Countdown } from "./components/countdown/Countdown.svelte";
3
3
  export { default as Footer } from "./components/footer/Footer.svelte";
4
+ export { default as Header } from "./components/header/Header.svelte";
4
5
  export { default as Image } from "./components/image/Image.svelte";
5
6
  export { default as InputMultipleChoice } from "./components/options/InputMultipleChoice.svelte";
6
7
  export { default as InputOption } from "./components/options/InputOption.svelte";
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  export { default as ButtonDeprecated } from "./components/button/Button.svelte";
3
3
  export { default as Countdown } from "./components/countdown/Countdown.svelte";
4
4
  export { default as Footer } from "./components/footer/Footer.svelte";
5
+ export { default as Header } from "./components/header/Header.svelte";
5
6
  export { default as Image } from "./components/image/Image.svelte";
6
7
  export { default as InputMultipleChoice } from "./components/options/InputMultipleChoice.svelte";
7
8
  export { default as InputOption } from "./components/options/InputOption.svelte";
@@ -26,6 +26,8 @@ export declare const localizations: {
26
26
  };
27
27
  export declare const colorModeOverrideTemplate: PaywallData;
28
28
  export declare const paywallWithFooter: PaywallData;
29
+ export declare const paywallWithHeader: PaywallData;
30
+ export declare const paywallWithTransparentHeaderAndTopImage: PaywallData;
29
31
  export declare const errorPaywallData: PaywallData;
30
32
  export declare const fallbackPaywallData: PaywallData;
31
33
  export declare const brandingAppearances: Record<string, BrandingAppearance | null>;