@revenuecat/purchases-ui-js 4.14.2 → 4.16.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.
- package/dist/components/button/ButtonNode.svelte +6 -0
- package/dist/components/package/Package.svelte +82 -10
- package/dist/components/paywall/Paywall.stories.svelte +335 -1
- package/dist/components/paywall/Paywall.svelte +95 -4
- package/dist/components/paywall/Paywall.svelte.d.ts +1 -0
- package/dist/components/paywall/Sheet.svelte +31 -1
- package/dist/components/paywall/fixtures/stack-scroll-paywall.d.ts +40 -0
- package/dist/components/paywall/fixtures/stack-scroll-paywall.js +280 -0
- package/dist/components/paywall/fixtures/window-rule-packages-paywall.d.ts +14 -0
- package/dist/components/paywall/fixtures/window-rule-packages-paywall.js +89 -0
- package/dist/components/paywall/fixtures/window-size-paywall.d.ts +2 -0
- package/dist/components/paywall/fixtures/window-size-paywall.js +48 -0
- package/dist/components/stack/Stack.svelte +87 -9
- package/dist/components/stack/Stack.svelte.d.ts +2 -0
- package/dist/components/stack/stack-scroll.d.ts +59 -0
- package/dist/components/stack/stack-scroll.js +162 -0
- package/dist/components/tabs/Tabs.svelte +11 -0
- package/dist/components/workflows/Screen.svelte +3 -0
- package/dist/components/workflows/Screen.svelte.d.ts +1 -0
- package/dist/components/workflows/Workflow.svelte +3 -0
- package/dist/components/workflows/Workflow.svelte.d.ts +1 -0
- package/dist/stores/condition-context.svelte.js +3 -0
- package/dist/stores/default-selection.d.ts +17 -0
- package/dist/stores/default-selection.js +21 -0
- package/dist/stores/rendered-packages.d.ts +10 -0
- package/dist/stores/rendered-packages.js +17 -0
- package/dist/stores/window-size.d.ts +10 -0
- package/dist/stores/window-size.js +10 -0
- package/dist/types/components/button.d.ts +1 -1
- package/dist/types/components/package.d.ts +1 -0
- package/dist/types/components/stack.d.ts +6 -0
- package/dist/types/overrides.d.ts +15 -1
- package/dist/types/overrides.js +3 -0
- package/dist/types/paywall-component-interaction.d.ts +1 -1
- package/dist/utils/package-selection.d.ts +26 -0
- package/dist/utils/package-selection.js +47 -0
- package/dist/utils/style-utils.d.ts +12 -0
- package/dist/utils/style-utils.js +59 -0
- package/package.json +2 -2
|
@@ -7,6 +7,8 @@ interface MiscProps {
|
|
|
7
7
|
style?: CSS;
|
|
8
8
|
class?: string;
|
|
9
9
|
testId?: string;
|
|
10
|
+
/** The paywall's root stack, which the paywall scrolls vertically. */
|
|
11
|
+
rootStack?: boolean;
|
|
10
12
|
}
|
|
11
13
|
type $$ComponentProps = StackProps & MiscProps;
|
|
12
14
|
declare const Stack: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { SizeType } from "../../types";
|
|
2
|
+
import type { Dimension } from "../../types/alignment";
|
|
3
|
+
import type { Component } from "../../types/component";
|
|
4
|
+
import type { StackProps } from "../../types/components/stack";
|
|
5
|
+
import type { CSS } from "../../utils/base-utils";
|
|
6
|
+
import { type ConditionContext } from "../../utils/style-utils";
|
|
7
|
+
export type ScrollAxis = "x" | "y";
|
|
8
|
+
/**
|
|
9
|
+
* The axis a stack scrolls along, like the native SDKs: horizontal stacks
|
|
10
|
+
* scroll sideways, vertical ones down. The root is already the paywall's
|
|
11
|
+
* vertical scroller, and z-layers never scroll.
|
|
12
|
+
*/
|
|
13
|
+
export declare function stackScrollAxis(dimension: Dimension["type"], rootStack: boolean): ScrollAxis | null;
|
|
14
|
+
/**
|
|
15
|
+
* How the paywall's root stack scrolls, the one place that decides it. Like
|
|
16
|
+
* the SDKs it scrolls down unless its overflow is "default", and a horizontal
|
|
17
|
+
* root set to scroll also scrolls sideways.
|
|
18
|
+
*/
|
|
19
|
+
export declare function rootScrollStyle(overflow: StackProps["overflow"], axis: ScrollAxis | null): CSS;
|
|
20
|
+
/**
|
|
21
|
+
* Like the SDKs, a Fit stack that scrolls takes the space its parent leaves
|
|
22
|
+
* it, up to its content, then scrolls. Along the parent's axis it shrinks
|
|
23
|
+
* first, so a parent that grows with its content never caps it. Across a row
|
|
24
|
+
* with a set height it stops at the row's height and keeps the row's
|
|
25
|
+
* alignment. A Fit row's height comes from its other children, where a
|
|
26
|
+
* percentage max-height doesn't resolve, so it stretches to the row instead.
|
|
27
|
+
* With no parent stack (a Fit root) it stops at its container. Inside a parent
|
|
28
|
+
* that scrolls the same way it keeps its full size.
|
|
29
|
+
*/
|
|
30
|
+
export declare function fitScrollSizing(axis: ScrollAxis | null, size: SizeType, parentDimension: Dimension["type"] | undefined, parentScrollAxis: ScrollAxis | null | undefined, parentHasSetHeight?: boolean): CSS;
|
|
31
|
+
/**
|
|
32
|
+
* The axis a Fit stack gives way along because a Fit stack inside it scrolls
|
|
33
|
+
* there. In the SDKs the scroll view makes its Fit parents take the space
|
|
34
|
+
* they're offered, so they size like a scroller, without scrolling.
|
|
35
|
+
*/
|
|
36
|
+
export declare function fitScrollerParentAxis(size: SizeType, components: Component[], context: ConditionContext): ScrollAxis | null;
|
|
37
|
+
/**
|
|
38
|
+
* What stops a sheet's contents stack that scrolls: a Fit stack stops at the
|
|
39
|
+
* screen's height, as the paywall's root does, and a stack with a set or Fill
|
|
40
|
+
* height at that height. A Fit sheet gives Fill contents no height to fill, so
|
|
41
|
+
* the sheet takes the screen's height for them. Either way the stack scrolls
|
|
42
|
+
* itself.
|
|
43
|
+
*/
|
|
44
|
+
export declare function sheetScrollBound(contents: Pick<StackProps, "overflow" | "dimension" | "size">, sheetHeight: SizeType["height"]["type"]): "screen" | "full-screen" | "stack" | null;
|
|
45
|
+
/**
|
|
46
|
+
* Whether a stack's height is set (Fill, Fixed or the paywall's root) rather
|
|
47
|
+
* than coming from its children, so a child's percentage height resolves.
|
|
48
|
+
*/
|
|
49
|
+
export declare function stackHasSetHeight(size: SizeType, rootStack: boolean): boolean;
|
|
50
|
+
type GetHasSetHeight = () => boolean;
|
|
51
|
+
/** Publishes whether this stack's height is set and returns the parent's. */
|
|
52
|
+
export declare function setStackSetHeightContext(context: GetHasSetHeight): GetHasSetHeight | undefined;
|
|
53
|
+
type GetScrollAxis = () => ScrollAxis | null;
|
|
54
|
+
/**
|
|
55
|
+
* Publishes the axis this stack scrolls along and returns the parent's, so a
|
|
56
|
+
* child knows when it sits inside a scroller.
|
|
57
|
+
*/
|
|
58
|
+
export declare function setStackScrollContext(context: GetScrollAxis): GetScrollAxis | undefined;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { resolveOverrideProperties, } from "../../utils/style-utils";
|
|
2
|
+
import { getContext, setContext } from "svelte";
|
|
3
|
+
/**
|
|
4
|
+
* The axis a stack scrolls along, like the native SDKs: horizontal stacks
|
|
5
|
+
* scroll sideways, vertical ones down. The root is already the paywall's
|
|
6
|
+
* vertical scroller, and z-layers never scroll.
|
|
7
|
+
*/
|
|
8
|
+
export function stackScrollAxis(dimension, rootStack) {
|
|
9
|
+
if (dimension === "horizontal")
|
|
10
|
+
return "x";
|
|
11
|
+
if (dimension === "vertical" && !rootStack)
|
|
12
|
+
return "y";
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* How the paywall's root stack scrolls, the one place that decides it. Like
|
|
17
|
+
* the SDKs it scrolls down unless its overflow is "default", and a horizontal
|
|
18
|
+
* root set to scroll also scrolls sideways.
|
|
19
|
+
*/
|
|
20
|
+
export function rootScrollStyle(overflow, axis) {
|
|
21
|
+
if (overflow === "default")
|
|
22
|
+
return { overflow: "clip" };
|
|
23
|
+
return axis === "x"
|
|
24
|
+
? { "overflow-x": "auto", "overflow-y": "auto" }
|
|
25
|
+
: { "overflow-y": "auto" };
|
|
26
|
+
}
|
|
27
|
+
// Far above its siblings' 1, so a scrolling stack gives up space before they
|
|
28
|
+
// do, like a SwiftUI ScrollView next to fixed content.
|
|
29
|
+
const SCROLLER_FLEX_SHRINK = 1_000_000;
|
|
30
|
+
/**
|
|
31
|
+
* Like the SDKs, a Fit stack that scrolls takes the space its parent leaves
|
|
32
|
+
* it, up to its content, then scrolls. Along the parent's axis it shrinks
|
|
33
|
+
* first, so a parent that grows with its content never caps it. Across a row
|
|
34
|
+
* with a set height it stops at the row's height and keeps the row's
|
|
35
|
+
* alignment. A Fit row's height comes from its other children, where a
|
|
36
|
+
* percentage max-height doesn't resolve, so it stretches to the row instead.
|
|
37
|
+
* With no parent stack (a Fit root) it stops at its container. Inside a parent
|
|
38
|
+
* that scrolls the same way it keeps its full size.
|
|
39
|
+
*/
|
|
40
|
+
export function fitScrollSizing(axis, size, parentDimension, parentScrollAxis, parentHasSetHeight = false) {
|
|
41
|
+
if (axis == null)
|
|
42
|
+
return {};
|
|
43
|
+
if (axis === parentScrollAxis)
|
|
44
|
+
return { "flex-shrink": 0 };
|
|
45
|
+
if (axis === "y" && size.height.type === "fit") {
|
|
46
|
+
if (parentDimension === "vertical") {
|
|
47
|
+
return { "flex-shrink": SCROLLER_FLEX_SHRINK, "min-height": 0 };
|
|
48
|
+
}
|
|
49
|
+
if (parentDimension == null || parentHasSetHeight) {
|
|
50
|
+
return { "min-height": 0, "max-height": "100%" };
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
height: "auto",
|
|
54
|
+
"align-self": "stretch",
|
|
55
|
+
"min-height": 0,
|
|
56
|
+
"max-height": "max-content",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (axis === "x" && size.width.type === "fit") {
|
|
60
|
+
return parentDimension === "horizontal"
|
|
61
|
+
? { "flex-shrink": SCROLLER_FLEX_SHRINK, "min-width": 0 }
|
|
62
|
+
: { "min-width": 0, "max-width": "100%" };
|
|
63
|
+
}
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
// The stack a component lays out as: its own, or the one a package or button
|
|
67
|
+
// wraps and takes its size from.
|
|
68
|
+
function layoutStack(component) {
|
|
69
|
+
switch (component.type) {
|
|
70
|
+
case "stack":
|
|
71
|
+
return component;
|
|
72
|
+
case "package":
|
|
73
|
+
case "button":
|
|
74
|
+
case "purchase_button":
|
|
75
|
+
return component.stack;
|
|
76
|
+
default:
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Whether a Fit stack scrolls along this direction somewhere below, reached
|
|
81
|
+
// only through Fit stacks: a stack with a set size bounds what's inside it.
|
|
82
|
+
// A hidden stack doesn't render, so neither it nor its contents count.
|
|
83
|
+
function containsFitScroller(components, direction, context) {
|
|
84
|
+
return components.some((child) => {
|
|
85
|
+
const stack = layoutStack(child);
|
|
86
|
+
if (!stack)
|
|
87
|
+
return false;
|
|
88
|
+
const resolved = {
|
|
89
|
+
...stack,
|
|
90
|
+
...resolveOverrideProperties(context, stack.overrides),
|
|
91
|
+
};
|
|
92
|
+
if (resolved.visible === false)
|
|
93
|
+
return false;
|
|
94
|
+
const size = direction === "vertical" ? resolved.size.height : resolved.size.width;
|
|
95
|
+
if (size.type !== "fit")
|
|
96
|
+
return false;
|
|
97
|
+
if (resolved.overflow === "scroll" &&
|
|
98
|
+
resolved.dimension.type === direction) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return containsFitScroller(resolved.components, direction, context);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The axis a Fit stack gives way along because a Fit stack inside it scrolls
|
|
106
|
+
* there. In the SDKs the scroll view makes its Fit parents take the space
|
|
107
|
+
* they're offered, so they size like a scroller, without scrolling.
|
|
108
|
+
*/
|
|
109
|
+
export function fitScrollerParentAxis(size, components, context) {
|
|
110
|
+
if (size.height.type === "fit" &&
|
|
111
|
+
containsFitScroller(components, "vertical", context)) {
|
|
112
|
+
return "y";
|
|
113
|
+
}
|
|
114
|
+
if (size.width.type === "fit" &&
|
|
115
|
+
containsFitScroller(components, "horizontal", context)) {
|
|
116
|
+
return "x";
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* What stops a sheet's contents stack that scrolls: a Fit stack stops at the
|
|
122
|
+
* screen's height, as the paywall's root does, and a stack with a set or Fill
|
|
123
|
+
* height at that height. A Fit sheet gives Fill contents no height to fill, so
|
|
124
|
+
* the sheet takes the screen's height for them. Either way the stack scrolls
|
|
125
|
+
* itself.
|
|
126
|
+
*/
|
|
127
|
+
export function sheetScrollBound(contents, sheetHeight) {
|
|
128
|
+
if (contents.overflow !== "scroll" ||
|
|
129
|
+
contents.dimension.type !== "vertical") {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
if (contents.size.height.type === "fit")
|
|
133
|
+
return "screen";
|
|
134
|
+
if (contents.size.height.type === "fill" && sheetHeight === "fit") {
|
|
135
|
+
return "full-screen";
|
|
136
|
+
}
|
|
137
|
+
return "stack";
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Whether a stack's height is set (Fill, Fixed or the paywall's root) rather
|
|
141
|
+
* than coming from its children, so a child's percentage height resolves.
|
|
142
|
+
*/
|
|
143
|
+
export function stackHasSetHeight(size, rootStack) {
|
|
144
|
+
return rootStack || size.height.type !== "fit";
|
|
145
|
+
}
|
|
146
|
+
const heightKey = Symbol("stack-set-height");
|
|
147
|
+
/** Publishes whether this stack's height is set and returns the parent's. */
|
|
148
|
+
export function setStackSetHeightContext(context) {
|
|
149
|
+
const parentContext = getContext(heightKey);
|
|
150
|
+
setContext(heightKey, context);
|
|
151
|
+
return parentContext;
|
|
152
|
+
}
|
|
153
|
+
const key = Symbol("stack-scroll");
|
|
154
|
+
/**
|
|
155
|
+
* Publishes the axis this stack scrolls along and returns the parent's, so a
|
|
156
|
+
* child knows when it sits inside a scroller.
|
|
157
|
+
*/
|
|
158
|
+
export function setStackScrollContext(context) {
|
|
159
|
+
const parentContext = getContext(key);
|
|
160
|
+
setContext(key, context);
|
|
161
|
+
return parentContext;
|
|
162
|
+
}
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
setRelativeDiscountScopeContext,
|
|
32
32
|
} from "../../stores/relativeDiscount";
|
|
33
33
|
import { mostExpensivePricePerMonth } from "../../utils/relative-discount";
|
|
34
|
+
import {
|
|
35
|
+
getDefaultSelectionContext,
|
|
36
|
+
openDefaultSelection,
|
|
37
|
+
setDefaultSelectionContext,
|
|
38
|
+
} from "../../stores/default-selection";
|
|
34
39
|
|
|
35
40
|
const props: TabsProps = $props();
|
|
36
41
|
const { name, control, tabs, default_tab_id, state_updates } = props;
|
|
@@ -90,6 +95,11 @@
|
|
|
90
95
|
|
|
91
96
|
const getTabName = (index: number) => tabs[index]?.name;
|
|
92
97
|
|
|
98
|
+
// The first tab renders with its paywall or sheet; each newly chosen tab
|
|
99
|
+
// lets its default package select itself.
|
|
100
|
+
let isDefaultSelectionOpen = getDefaultSelectionContext();
|
|
101
|
+
setDefaultSelectionContext(() => isDefaultSelectionOpen());
|
|
102
|
+
|
|
93
103
|
const selectTab: SelectTab = (tabId, tabIndex, interactionType) => {
|
|
94
104
|
const foundIndex = tabs.findIndex((tab) => tab.id === tabId);
|
|
95
105
|
const nextIndex = foundIndex !== -1 ? foundIndex : tabIndex;
|
|
@@ -118,6 +128,7 @@
|
|
|
118
128
|
});
|
|
119
129
|
}
|
|
120
130
|
|
|
131
|
+
isDefaultSelectionOpen = openDefaultSelection();
|
|
121
132
|
selectedTabIndex.set(nextIndex);
|
|
122
133
|
}
|
|
123
134
|
};
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
onNavigateToUrlClicked?: (url: string) => void;
|
|
62
62
|
onRestorePurchasesClicked?: () => void;
|
|
63
63
|
onVisitCustomerCenterClicked?: () => void;
|
|
64
|
+
onCookieSettingsClicked?: () => void;
|
|
64
65
|
walletButtonRender?: WalletButtonRender;
|
|
65
66
|
prepareWalletPurchase?: WalletPurchasePrepare;
|
|
66
67
|
onWalletPurchaseClicked?: WalletPurchaseStart;
|
|
@@ -99,6 +100,7 @@
|
|
|
99
100
|
onNavigateToUrlClicked,
|
|
100
101
|
onRestorePurchasesClicked,
|
|
101
102
|
onVisitCustomerCenterClicked,
|
|
103
|
+
onCookieSettingsClicked,
|
|
102
104
|
walletButtonRender,
|
|
103
105
|
prepareWalletPurchase,
|
|
104
106
|
onWalletPurchaseClicked,
|
|
@@ -141,6 +143,7 @@
|
|
|
141
143
|
}}
|
|
142
144
|
{onCompleteWorkflowNavigate}
|
|
143
145
|
onVisitCustomerCenterClicked={() => onVisitCustomerCenterClicked?.()}
|
|
146
|
+
onCookieSettingsClicked={() => onCookieSettingsClicked?.()}
|
|
144
147
|
{onBackClicked}
|
|
145
148
|
{onClose}
|
|
146
149
|
onRestorePurchasesClicked={() => onRestorePurchasesClicked?.()}
|
|
@@ -35,6 +35,7 @@ interface Props {
|
|
|
35
35
|
onNavigateToUrlClicked?: (url: string) => void;
|
|
36
36
|
onRestorePurchasesClicked?: () => void;
|
|
37
37
|
onVisitCustomerCenterClicked?: () => void;
|
|
38
|
+
onCookieSettingsClicked?: () => void;
|
|
38
39
|
walletButtonRender?: WalletButtonRender;
|
|
39
40
|
prepareWalletPurchase?: WalletPurchasePrepare;
|
|
40
41
|
onWalletPurchaseClicked?: WalletPurchaseStart;
|
|
@@ -88,6 +88,7 @@
|
|
|
88
88
|
onNavigateToUrlClicked?: (url: string) => void;
|
|
89
89
|
onRestorePurchasesClicked?: () => void;
|
|
90
90
|
onVisitCustomerCenterClicked?: () => void;
|
|
91
|
+
onCookieSettingsClicked?: () => void;
|
|
91
92
|
onComponentInteraction?: OnComponentInteraction;
|
|
92
93
|
onClose?: () => void;
|
|
93
94
|
onExitBack?: () => void;
|
|
@@ -132,6 +133,7 @@
|
|
|
132
133
|
onNavigateToUrlClicked,
|
|
133
134
|
onRestorePurchasesClicked,
|
|
134
135
|
onVisitCustomerCenterClicked,
|
|
136
|
+
onCookieSettingsClicked,
|
|
135
137
|
onComponentInteraction,
|
|
136
138
|
onClose,
|
|
137
139
|
onExitBack,
|
|
@@ -401,6 +403,7 @@
|
|
|
401
403
|
{onNavigateToUrlClicked}
|
|
402
404
|
{onRestorePurchasesClicked}
|
|
403
405
|
{onVisitCustomerCenterClicked}
|
|
406
|
+
{onCookieSettingsClicked}
|
|
404
407
|
{onComponentInteraction}
|
|
405
408
|
onBackClicked={handleBackClicked}
|
|
406
409
|
onClose={() => {
|
|
@@ -42,6 +42,7 @@ interface Props {
|
|
|
42
42
|
onNavigateToUrlClicked?: (url: string) => void;
|
|
43
43
|
onRestorePurchasesClicked?: () => void;
|
|
44
44
|
onVisitCustomerCenterClicked?: () => void;
|
|
45
|
+
onCookieSettingsClicked?: () => void;
|
|
45
46
|
onComponentInteraction?: OnComponentInteraction;
|
|
46
47
|
onClose?: () => void;
|
|
47
48
|
onExitBack?: () => void;
|
|
@@ -5,6 +5,7 @@ import { getPaywallContext } from "./paywall";
|
|
|
5
5
|
import { getSelectedStateContext } from "./selected";
|
|
6
6
|
import { getPaywallStateContext, getStateSignal } from "./state";
|
|
7
7
|
import { getOptionalVariablesContext } from "./variables";
|
|
8
|
+
import { getWindowSizeContext } from "./window-size";
|
|
8
9
|
/**
|
|
9
10
|
* Builds ConditionContext snapshots for resolveOverrideProperties from the
|
|
10
11
|
* ambient Svelte contexts. Call at component init; invoke the returned builder
|
|
@@ -16,6 +17,7 @@ export function useConditionContext(options = {}) {
|
|
|
16
17
|
const packageInfo = fromStore(getOptionalPackageInfoContext());
|
|
17
18
|
const selectedPackageId = fromStore(getPaywallContext().selectedPackageId);
|
|
18
19
|
const variables = fromStore(getOptionalVariablesContext());
|
|
20
|
+
const windowSize = fromStore(getWindowSizeContext());
|
|
19
21
|
const stateStore = options.state ? getPaywallStateContext() : undefined;
|
|
20
22
|
const stateSignal = options.state ? fromStore(getStateSignal()) : null;
|
|
21
23
|
return (extra) => {
|
|
@@ -27,6 +29,7 @@ export function useConditionContext(options = {}) {
|
|
|
27
29
|
packageInfo: packageInfo.current,
|
|
28
30
|
variables: variables.current ?? {},
|
|
29
31
|
selected: selected.current,
|
|
32
|
+
windowSize: windowSize.current,
|
|
30
33
|
...extra,
|
|
31
34
|
};
|
|
32
35
|
if (hover) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a default package rendering now should select itself. It should
|
|
3
|
+
* while the paywall, a newly chosen tab or a sheet first renders, but not when
|
|
4
|
+
* a rule shows its stack again later and the user has picked something else.
|
|
5
|
+
*/
|
|
6
|
+
export type DefaultSelectionOpen = () => boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Open until Svelte has applied the update in progress, which mounts the
|
|
9
|
+
* paywall's, sheet's or new tab's packages and runs their effects. A tab
|
|
10
|
+
* switch reuses the package components, so the window can't close on a mount.
|
|
11
|
+
* Packages that render in a later update, such as a stack a rule shows again,
|
|
12
|
+
* find it closed. Covered by the "package selection" tests in
|
|
13
|
+
* window-size-rules.test.ts and by package-default-selection-tabs.test.ts.
|
|
14
|
+
*/
|
|
15
|
+
export declare function openDefaultSelection(): DefaultSelectionOpen;
|
|
16
|
+
export declare function setDefaultSelectionContext(isOpen: DefaultSelectionOpen): void;
|
|
17
|
+
export declare function getDefaultSelectionContext(): DefaultSelectionOpen;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getContext, setContext, tick } from "svelte";
|
|
2
|
+
const key = Symbol("default-selection");
|
|
3
|
+
/**
|
|
4
|
+
* Open until Svelte has applied the update in progress, which mounts the
|
|
5
|
+
* paywall's, sheet's or new tab's packages and runs their effects. A tab
|
|
6
|
+
* switch reuses the package components, so the window can't close on a mount.
|
|
7
|
+
* Packages that render in a later update, such as a stack a rule shows again,
|
|
8
|
+
* find it closed. Covered by the "package selection" tests in
|
|
9
|
+
* window-size-rules.test.ts and by package-default-selection-tabs.test.ts.
|
|
10
|
+
*/
|
|
11
|
+
export function openDefaultSelection() {
|
|
12
|
+
let open = true;
|
|
13
|
+
tick().then(() => (open = false));
|
|
14
|
+
return () => open;
|
|
15
|
+
}
|
|
16
|
+
export function setDefaultSelectionContext(isOpen) {
|
|
17
|
+
setContext(key, isOpen);
|
|
18
|
+
}
|
|
19
|
+
export function getDefaultSelectionContext() {
|
|
20
|
+
return getContext(key) ?? (() => true);
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SvelteMap } from "svelte/reactivity";
|
|
2
|
+
import type { RenderedPackage } from "../utils/package-selection";
|
|
3
|
+
/** The packages on screen, each registered by the Package component rendering it. */
|
|
4
|
+
export type RenderedPackages = SvelteMap<symbol, RenderedPackage>;
|
|
5
|
+
export declare function setRenderedPackagesContext(packages: RenderedPackages): void;
|
|
6
|
+
export declare function getOptionalRenderedPackagesContext(): RenderedPackages | undefined;
|
|
7
|
+
/** Marks the packages rendering inside a sheet with the sheet's id. */
|
|
8
|
+
export declare function setPackageSheetContext(sheetId: string): void;
|
|
9
|
+
/** The id of the sheet a package renders in, or null outside sheets. */
|
|
10
|
+
export declare function getPackageSheetContext(): string | null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getContext, setContext } from "svelte";
|
|
2
|
+
const key = Symbol("rendered-packages");
|
|
3
|
+
export function setRenderedPackagesContext(packages) {
|
|
4
|
+
setContext(key, packages);
|
|
5
|
+
}
|
|
6
|
+
export function getOptionalRenderedPackagesContext() {
|
|
7
|
+
return getContext(key);
|
|
8
|
+
}
|
|
9
|
+
const sheetKey = Symbol("package-sheet");
|
|
10
|
+
/** Marks the packages rendering inside a sheet with the sheet's id. */
|
|
11
|
+
export function setPackageSheetContext(sheetId) {
|
|
12
|
+
setContext(sheetKey, sheetId);
|
|
13
|
+
}
|
|
14
|
+
/** The id of the sheet a package renders in, or null outside sheets. */
|
|
15
|
+
export function getPackageSheetContext() {
|
|
16
|
+
return getContext(sheetKey) ?? null;
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Readable } from "svelte/store";
|
|
2
|
+
/** The paywall's own box in CSS px, which window size rules evaluate against. */
|
|
3
|
+
export type WindowSize = {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
};
|
|
7
|
+
/** Null until the paywall has been measured. */
|
|
8
|
+
export type WindowSizeStore = Readable<WindowSize | null>;
|
|
9
|
+
export declare function setWindowSizeContext(store: WindowSizeStore): void;
|
|
10
|
+
export declare function getWindowSizeContext(): WindowSizeStore;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { getContext, setContext } from "svelte";
|
|
2
|
+
import { readable } from "svelte/store";
|
|
3
|
+
const key = Symbol("windowSize");
|
|
4
|
+
export function setWindowSizeContext(store) {
|
|
5
|
+
setContext(key, store);
|
|
6
|
+
}
|
|
7
|
+
const unknownWindowSize = readable(null);
|
|
8
|
+
export function getWindowSizeContext() {
|
|
9
|
+
return getContext(key) ?? unknownWindowSize;
|
|
10
|
+
}
|
|
@@ -17,7 +17,7 @@ interface CloseAction {
|
|
|
17
17
|
}
|
|
18
18
|
interface NavigateToAction {
|
|
19
19
|
type: "navigate_to";
|
|
20
|
-
destination: "customer_center" | "offer_code" | "screen_redirect";
|
|
20
|
+
destination: "customer_center" | "offer_code" | "screen_redirect" | "cookie_settings";
|
|
21
21
|
}
|
|
22
22
|
interface NavigateToSheetAction {
|
|
23
23
|
type: "navigate_to";
|
|
@@ -27,6 +27,12 @@ export interface StackProps extends BaseComponent {
|
|
|
27
27
|
shape: ShapeType | null;
|
|
28
28
|
shadow: ShadowType | null;
|
|
29
29
|
badge?: Badge | null;
|
|
30
|
+
/**
|
|
31
|
+
* "scroll" scrolls the stack's content along its axis; "default" turns
|
|
32
|
+
* scrolling off. Unset is the platform default: the root scrolls, other
|
|
33
|
+
* stacks don't.
|
|
34
|
+
*/
|
|
35
|
+
overflow?: "scroll" | "default" | null;
|
|
30
36
|
overrides?: Overrides<StackProps>;
|
|
31
37
|
}
|
|
32
38
|
export {};
|
|
@@ -2,12 +2,16 @@ import type { CustomVariableValue } from "./variables.js";
|
|
|
2
2
|
import type { StateValue } from "./state.js";
|
|
3
3
|
type EqualityOperator = "=" | "!=";
|
|
4
4
|
type ArrayOperator = "in" | "not in";
|
|
5
|
+
export type ComparisonOperator = ">=" | ">" | "<=" | "<" | "=";
|
|
5
6
|
export declare const RuleKey: {
|
|
6
7
|
readonly SelectedPackageCondition: "selected_package_condition";
|
|
7
8
|
readonly IntroOfferCondition: "intro_offer_condition";
|
|
8
9
|
readonly PromoOfferCondition: "promo_offer_condition";
|
|
9
10
|
readonly VariableCondition: "variable_condition";
|
|
10
11
|
readonly StateCondition: "state_condition";
|
|
12
|
+
readonly WindowWidthCondition: "window_width_condition";
|
|
13
|
+
readonly WindowHeightCondition: "window_height_condition";
|
|
14
|
+
readonly WindowAspectRatioCondition: "window_aspect_ratio_condition";
|
|
11
15
|
};
|
|
12
16
|
type IntroOfferCondition = {
|
|
13
17
|
type: "intro_offer";
|
|
@@ -57,7 +61,17 @@ type StateCondition = {
|
|
|
57
61
|
name: string;
|
|
58
62
|
value: StateValue;
|
|
59
63
|
};
|
|
60
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Compares the paywall's own box (CSS px, matching iOS points and Android dp)
|
|
66
|
+
* or its aspect ratio (width ÷ height). Never matches before the paywall is
|
|
67
|
+
* measured.
|
|
68
|
+
*/
|
|
69
|
+
export type WindowSizeCondition = {
|
|
70
|
+
type: typeof RuleKey.WindowWidthCondition | typeof RuleKey.WindowHeightCondition | typeof RuleKey.WindowAspectRatioCondition;
|
|
71
|
+
operator: ComparisonOperator;
|
|
72
|
+
value: number;
|
|
73
|
+
};
|
|
74
|
+
export type OverrideCondition = IntroOfferCondition | MultipleIntroOffersCondition | SelectedCondition | HoverCondition | FocusCondition | ErrorCondition | PromoOfferCondition | IntroductoryOfferCondition | PromotionalOfferCondition | SelectedPackageCondition | VariableCondition | StateCondition | WindowSizeCondition;
|
|
61
75
|
/**
|
|
62
76
|
* The reserved properties of an override that should not be included in the override properties
|
|
63
77
|
*/
|
package/dist/types/overrides.js
CHANGED
|
@@ -4,4 +4,7 @@ export const RuleKey = {
|
|
|
4
4
|
PromoOfferCondition: "promo_offer_condition",
|
|
5
5
|
VariableCondition: "variable_condition",
|
|
6
6
|
StateCondition: "state_condition",
|
|
7
|
+
WindowWidthCondition: "window_width_condition",
|
|
8
|
+
WindowHeightCondition: "window_height_condition",
|
|
9
|
+
WindowAspectRatioCondition: "window_aspect_ratio_condition",
|
|
7
10
|
};
|
|
@@ -16,7 +16,7 @@ interface PackageTransition {
|
|
|
16
16
|
destinationPackageId: string;
|
|
17
17
|
defaultPackageId?: string;
|
|
18
18
|
}
|
|
19
|
-
type ButtonInteractionValue = "workflow" | "navigate_to_url" | "navigate_back" | "close_workflow" | "restore_purchases" | "navigate_to_customer_center" | "screen_redirect" | "navigate_to_privacy_policy" | "navigate_to_terms" | "navigate_to_sheet" | "navigate_to_offer_code" | "navigate_to_web_paywall_link";
|
|
19
|
+
type ButtonInteractionValue = "workflow" | "navigate_to_url" | "navigate_back" | "close_workflow" | "restore_purchases" | "navigate_to_customer_center" | "navigate_to_cookie_settings" | "screen_redirect" | "navigate_to_privacy_policy" | "navigate_to_terms" | "navigate_to_sheet" | "navigate_to_offer_code" | "navigate_to_web_paywall_link";
|
|
20
20
|
export type TabInteractionData = ComponentInteractionBase<"tab"> & IndexedTransition;
|
|
21
21
|
export type SwitchInteractionData = ComponentInteractionBase<"switch", "on" | "off">;
|
|
22
22
|
export type CarouselInteractionData = ComponentInteractionBase<"carousel"> & IndexedTransition;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PackagePlacement } from "./style-utils";
|
|
2
|
+
export type RenderedPackage = {
|
|
3
|
+
componentId: string;
|
|
4
|
+
packageId: string;
|
|
5
|
+
isSelectedByDefault: boolean;
|
|
6
|
+
/** The sheet it renders in, or null outside sheets. */
|
|
7
|
+
sheetId: string | null;
|
|
8
|
+
visible: boolean;
|
|
9
|
+
/** Whether it shows both before and once it's selected, so it can take over the selection. */
|
|
10
|
+
selectable: boolean;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The package to select instead of `current` when a rule hides every copy of
|
|
14
|
+
* it on screen, or `undefined` to keep it. The replacement is a tab's authored
|
|
15
|
+
* default, then one outside the tabs, then the first selectable package in a
|
|
16
|
+
* tab, then outside. The selection stays when no copy is on screen or nothing
|
|
17
|
+
* else can take it.
|
|
18
|
+
*
|
|
19
|
+
* This follows the order in purchases-android "Reconcile package selection
|
|
20
|
+
* when a window size rule hides the selected package" (#4198, not merged yet),
|
|
21
|
+
* without its per-tab remembered pick, which web doesn't have. iOS differs: it
|
|
22
|
+
* only picks within the hidden package's own tab, or among the packages
|
|
23
|
+
* outside tabs (PackageValidator.swift). Either way, a package hidden in an
|
|
24
|
+
* open sheet is replaced from that sheet first, when one there can take it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function reconciledSelection(current: string | undefined, rendered: Iterable<RenderedPackage>, placements: Map<string, PackagePlacement>): string | undefined;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package to select instead of `current` when a rule hides every copy of
|
|
3
|
+
* it on screen, or `undefined` to keep it. The replacement is a tab's authored
|
|
4
|
+
* default, then one outside the tabs, then the first selectable package in a
|
|
5
|
+
* tab, then outside. The selection stays when no copy is on screen or nothing
|
|
6
|
+
* else can take it.
|
|
7
|
+
*
|
|
8
|
+
* This follows the order in purchases-android "Reconcile package selection
|
|
9
|
+
* when a window size rule hides the selected package" (#4198, not merged yet),
|
|
10
|
+
* without its per-tab remembered pick, which web doesn't have. iOS differs: it
|
|
11
|
+
* only picks within the hidden package's own tab, or among the packages
|
|
12
|
+
* outside tabs (PackageValidator.swift). Either way, a package hidden in an
|
|
13
|
+
* open sheet is replaced from that sheet first, when one there can take it.
|
|
14
|
+
*/
|
|
15
|
+
export function reconciledSelection(current, rendered, placements) {
|
|
16
|
+
if (current === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
const packages = [...rendered];
|
|
19
|
+
const copies = packages.filter((pkg) => pkg.packageId === current);
|
|
20
|
+
if (copies.length === 0 || copies.some((pkg) => pkg.visible)) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
// A sheet's packages aren't in the base tree, so they sort last.
|
|
24
|
+
const placement = (pkg) => placements.get(pkg.componentId) ?? {
|
|
25
|
+
index: Number.POSITIVE_INFINITY,
|
|
26
|
+
inTabs: false,
|
|
27
|
+
};
|
|
28
|
+
const candidates = packages
|
|
29
|
+
.filter((pkg) => pkg.selectable)
|
|
30
|
+
.sort((a, b) => placement(a).index - placement(b).index);
|
|
31
|
+
// A sheet's purchase button buys the selection, so when the hidden package
|
|
32
|
+
// is in an open sheet, a package still showing in that sheet comes first.
|
|
33
|
+
const sheetId = copies[0].sheetId;
|
|
34
|
+
if (sheetId !== null && copies.every((pkg) => pkg.sheetId === sheetId)) {
|
|
35
|
+
const inSheet = candidates.filter((pkg) => pkg.sheetId === sheetId);
|
|
36
|
+
const replacement = inSheet.find((pkg) => pkg.isSelectedByDefault) ?? inSheet[0];
|
|
37
|
+
if (replacement)
|
|
38
|
+
return replacement.packageId;
|
|
39
|
+
}
|
|
40
|
+
const inTabs = candidates.filter((pkg) => placement(pkg).inTabs);
|
|
41
|
+
const outsideTabs = candidates.filter((pkg) => !placement(pkg).inTabs);
|
|
42
|
+
const replacement = inTabs.find((pkg) => pkg.isSelectedByDefault) ??
|
|
43
|
+
outsideTabs.find((pkg) => pkg.isSelectedByDefault) ??
|
|
44
|
+
inTabs[0] ??
|
|
45
|
+
outsideTabs[0];
|
|
46
|
+
return replacement?.packageId;
|
|
47
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { Overrides } from "../types/overrides.js";
|
|
|
3
3
|
import type { RootPaywall } from "../types/paywall.js";
|
|
4
4
|
import type { StateValue } from "../types/state.js";
|
|
5
5
|
import type { PackageInfo, VariableDictionary } from "../types/variables.js";
|
|
6
|
+
import type { WindowSize } from "../stores/window-size.js";
|
|
6
7
|
/**
|
|
7
8
|
* Given an instance of PaywallData, returns the id of the first package marked as `is_selected_by_default` if any.
|
|
8
9
|
* If none are marked, returns the first package encountered in traversal order (root stack, then sticky footer).
|
|
@@ -17,6 +18,15 @@ export declare function collectPackageIds(root: Component): string[];
|
|
|
17
18
|
* Duplicates keep the first occurrence. Unselected tabs still count.
|
|
18
19
|
*/
|
|
19
20
|
export declare function collectPackageIdsInDisplayOrder({ stack, sticky_footer, }: RootPaywall): string[];
|
|
21
|
+
export type PackagePlacement = {
|
|
22
|
+
index: number;
|
|
23
|
+
inTabs: boolean;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Each package component's place in display order (root stack, then sticky
|
|
27
|
+
* footer) and whether it sits inside tabs, by component id.
|
|
28
|
+
*/
|
|
29
|
+
export declare function collectPackagePlacements({ stack, sticky_footer, }: RootPaywall): Map<string, PackagePlacement>;
|
|
20
30
|
export type ConditionContext = {
|
|
21
31
|
selectedPackageId: string | undefined;
|
|
22
32
|
packageInfo: PackageInfo | undefined;
|
|
@@ -25,6 +35,8 @@ export type ConditionContext = {
|
|
|
25
35
|
stateValues?: Record<string, StateValue>;
|
|
26
36
|
/** Declared state defaults; optional so existing call sites compile unchanged. */
|
|
27
37
|
stateDefaults?: Record<string, StateValue>;
|
|
38
|
+
/** The paywall's measured box; absent until measured, so window rules don't match. */
|
|
39
|
+
windowSize?: WindowSize | null;
|
|
28
40
|
/** Interaction pseudo-state flags; absent means "not in that state". */
|
|
29
41
|
selected?: boolean;
|
|
30
42
|
hover?: boolean;
|