@revenuecat/purchases-ui-js 4.8.6 → 4.8.7

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,19 +1,16 @@
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";
5
4
 
6
5
  const { stack }: FooterProps = $props();
7
6
  </script>
8
7
 
9
- <div style="z-index: {STICKY_OVERLAY_Z_INDEX};">
8
+ <div>
10
9
  <Stack {...stack} />
11
10
  </div>
12
11
 
13
12
  <style>
14
13
  div {
15
- position: sticky;
16
- bottom: 0;
17
14
  width: 100%;
18
15
 
19
16
  display: flex;
@@ -39,6 +39,11 @@
39
39
  DUELINGUE_PAYWALL,
40
40
  } from "./fixtures/express-purchase-button-paywall";
41
41
  import { CustomVariableValue } from "../../types/variables";
42
+ import {
43
+ paywallWithTransparentFooter,
44
+ paywallWithCenteredBodyAndFooter,
45
+ paywallWithHeaderAndFooter,
46
+ } from "./fixtures/transparent-footer-paywall";
42
47
 
43
48
  const { Story } = defineMeta({
44
49
  title: "Example/Paywall",
@@ -480,6 +485,28 @@
480
485
  }}
481
486
  />
482
487
 
488
+ <Story
489
+ name="Sticky Footer - transparent (overlaps content)"
490
+ args={{
491
+ paywallData: paywallWithTransparentFooter,
492
+ }}
493
+ />
494
+
495
+ <Story
496
+ name="Sticky Footer - centered body above footer"
497
+ decorators={[viewportDecorator(500, 500, 0)]}
498
+ args={{
499
+ paywallData: paywallWithCenteredBodyAndFooter,
500
+ }}
501
+ />
502
+
503
+ <Story
504
+ name="Sticky Footer - with header"
505
+ args={{
506
+ paywallData: paywallWithHeaderAndFooter,
507
+ }}
508
+ />
509
+
483
510
  <Story
484
511
  name="Header"
485
512
  args={{
@@ -352,6 +352,13 @@
352
352
  const pullContentUnderHeader = !!header && firstComponentIsFullWidthImage;
353
353
 
354
354
  let headerHeight = $state(0);
355
+ let footerHeight = $state(0);
356
+
357
+ const contentColumnStyle = $derived(
358
+ maxContentWidth
359
+ ? `max-width: ${maxContentWidth}; margin-inline: auto;`
360
+ : "",
361
+ );
355
362
  </script>
356
363
 
357
364
  <svelte:boundary onerror={onError}>
@@ -366,19 +373,16 @@
366
373
  <Header {...header} />
367
374
  </div>
368
375
  {/if}
369
- <div
370
- class="paywall-content"
371
- style={[
372
- maxContentWidth
373
- ? `max-width: ${maxContentWidth}; margin-inline: auto;`
374
- : "",
375
- ]
376
- .filter(Boolean)
377
- .join(" ")}
378
- >
376
+ <div class="paywall-content" style={contentColumnStyle}>
379
377
  <Stack
380
378
  {...stack}
381
379
  class="paywall-content-scroll"
380
+ padding={sticky_footer
381
+ ? {
382
+ ...stack.padding,
383
+ bottom: (stack.padding?.bottom ?? 0) + footerHeight,
384
+ }
385
+ : stack.padding}
382
386
  style={{
383
387
  height: "auto",
384
388
  ...(pullContentUnderHeader
@@ -386,10 +390,19 @@
386
390
  : {}),
387
391
  }}
388
392
  />
389
- {#if sticky_footer}
390
- <Footer {...sticky_footer} />
391
- {/if}
392
393
  </div>
394
+ {#if sticky_footer}
395
+ <!-- Overlaps the content so translucent footers show it; sticky rather than
396
+ absolute so it stays visible when an ancestor scrollport is shorter than
397
+ the paywall. -->
398
+ <div
399
+ class="footer-wrapper"
400
+ style="z-index: {STICKY_OVERLAY_Z_INDEX}; margin-top: -{footerHeight}px; {contentColumnStyle}"
401
+ bind:clientHeight={footerHeight}
402
+ >
403
+ <Footer {...sticky_footer} />
404
+ </div>
405
+ {/if}
393
406
  </div>
394
407
 
395
408
  {#if sheet}
@@ -422,6 +435,13 @@
422
435
  width: 100%;
423
436
  }
424
437
 
438
+ .footer-wrapper {
439
+ position: sticky;
440
+ bottom: 0;
441
+ width: 100%;
442
+ flex-shrink: 0;
443
+ }
444
+
425
445
  .paywall-content {
426
446
  z-index: 2;
427
447
  width: 100%;
@@ -0,0 +1,7 @@
1
+ import type { PaywallData } from "../../../types/paywall";
2
+ /** Translucent sticky footer over long scrollable content. */
3
+ export declare const paywallWithTransparentFooter: PaywallData;
4
+ /** Small body that must center in the space above the footer, not the whole screen. */
5
+ export declare const paywallWithCenteredBodyAndFooter: PaywallData;
6
+ /** Header and sticky footer together: the footer overlay must not affect the header. */
7
+ export declare const paywallWithHeaderAndFooter: PaywallData;
@@ -0,0 +1,59 @@
1
+ import { paywallWithFooter, paywallWithHeader } from "../../../stories/fixtures";
2
+ function footerStack(paywall) {
3
+ const footer = paywall.components_config.base.sticky_footer;
4
+ if (!footer) {
5
+ throw new Error("Fixture has no sticky footer");
6
+ }
7
+ return footer.stack;
8
+ }
9
+ function lastTextComponent(components) {
10
+ return components.findLast((component) => component.type === "text");
11
+ }
12
+ /** Duplicates the last text component so the content is tall enough to scroll. */
13
+ function appendScrollableContent(paywall, count) {
14
+ const components = paywall.components_config.base.stack.components;
15
+ const lastText = lastTextComponent(components);
16
+ if (!lastText) {
17
+ return;
18
+ }
19
+ for (let index = 0; index < count; index++) {
20
+ const copy = structuredClone(lastText);
21
+ copy.id = `scroll-filler-${index}`;
22
+ components.push(copy);
23
+ }
24
+ }
25
+ function setFooterBackground(paywall, hex) {
26
+ footerStack(paywall).background_color = {
27
+ light: { type: "hex", value: hex },
28
+ };
29
+ }
30
+ /** Translucent sticky footer over long scrollable content. */
31
+ export const paywallWithTransparentFooter = (() => {
32
+ const paywall = structuredClone(paywallWithFooter);
33
+ appendScrollableContent(paywall, 25);
34
+ setFooterBackground(paywall, "#057C5BAA");
35
+ return paywall;
36
+ })();
37
+ /** Small body that must center in the space above the footer, not the whole screen. */
38
+ export const paywallWithCenteredBodyAndFooter = (() => {
39
+ const paywall = structuredClone(paywallWithFooter);
40
+ const stack = paywall.components_config.base.stack;
41
+ const lastText = lastTextComponent(stack.components);
42
+ stack.components = lastText ? [lastText] : [];
43
+ stack.dimension.distribution = "center";
44
+ setFooterBackground(paywall, "#057C5BAA");
45
+ return paywall;
46
+ })();
47
+ /** Header and sticky footer together: the footer overlay must not affect the header. */
48
+ export const paywallWithHeaderAndFooter = (() => {
49
+ const paywall = structuredClone(paywallWithFooter);
50
+ appendScrollableContent(paywall, 15);
51
+ const headerDonor = structuredClone(paywallWithHeader);
52
+ paywall.components_config.base.header =
53
+ headerDonor.components_config.base.header;
54
+ paywall.components_localizations.en_US = {
55
+ ...paywall.components_localizations.en_US,
56
+ ...headerDonor.components_localizations.en_US,
57
+ };
58
+ return paywall;
59
+ })();
@@ -56,17 +56,23 @@
56
56
  let fitWidth = $state<number | undefined>(undefined);
57
57
  let fitHeight = $state<number | undefined>(undefined);
58
58
 
59
+ const fitAxisFallback = (axis: typeof props.size.width): number =>
60
+ axis.type === "fit" && typeof axis.default === "number"
61
+ ? axis.default
62
+ : FIT_FALLBACK_SIZE_PX;
63
+
59
64
  const style = $derived.by(() => {
60
65
  const { width, height } = props.size;
61
66
  const widthFit = width.type === "fit";
62
67
  const heightFit = height.type === "fit";
63
68
  // Pin a `fit` axis with an explicit size. `flex-shrink: 0` keeps flex parents
64
69
  // from collapsing the box; avoid mirroring the size in `min-*` or it beats `max-*`.
70
+ // Prefer author-declared `default` when present; else the hard 300px fallback.
65
71
  const fitWidthPx = widthFit
66
- ? clampFitSize(fitWidth ?? FIT_FALLBACK_SIZE_PX)
72
+ ? clampFitSize(fitWidth ?? fitAxisFallback(width))
67
73
  : null;
68
74
  const fitHeightPx = heightFit
69
- ? clampFitSize(fitHeight ?? FIT_FALLBACK_SIZE_PX)
75
+ ? clampFitSize(fitHeight ?? fitAxisFallback(height))
70
76
  : null;
71
77
  return css({
72
78
  width: fitWidthPx != null ? px(fitWidthPx) : mapSize(width),
package/dist/types.d.ts CHANGED
@@ -48,11 +48,17 @@ type RelativeSize = {
48
48
  type: "relative";
49
49
  value: number;
50
50
  };
51
- type FitFillSize = {
52
- type: "fit" | "fill";
51
+ type FitSize = {
52
+ type: "fit";
53
53
  value?: null;
54
+ /** Placeholder px until the content reports its measured size. */
55
+ default?: number;
54
56
  };
55
- export type Size = FixedSize | RelativeSize | FitFillSize;
57
+ type FillSize = {
58
+ type: "fill";
59
+ value?: null;
60
+ };
61
+ export type Size = FixedSize | RelativeSize | FitSize | FillSize;
56
62
  export type SizeType = {
57
63
  width: Size;
58
64
  height: Size;
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.6",
5
+ "version": "4.8.7",
6
6
  "author": {
7
7
  "name": "RevenueCat, Inc."
8
8
  },