@revenuecat/purchases-ui-js 4.3.0 → 4.5.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/input-text/InputText.svelte +16 -2
- package/dist/components/paywall/Paywall.svelte +20 -1
- package/dist/components/paywall/Paywall.svelte.d.ts +9 -0
- package/dist/components/workflows/Screen.svelte +11 -0
- package/dist/components/workflows/Screen.svelte.d.ts +4 -0
- package/dist/stores/paywall.d.ts +2 -0
- package/dist/types/colors.d.ts +1 -1
- package/dist/types/components/input-text.d.ts +2 -0
- package/dist/types/paywall.d.ts +2 -0
- package/dist/types.d.ts +2 -0
- package/dist/utils/document-background.d.ts +7 -1
- package/dist/utils/document-background.js +18 -36
- package/dist/utils/safe-area-background.d.ts +21 -0
- package/dist/utils/safe-area-background.js +211 -0
- package/dist/utils/safe-area.d.ts +1 -0
- package/dist/utils/safe-area.js +4 -0
- package/package.json +5 -1
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
keyboard_type,
|
|
42
42
|
capitalize,
|
|
43
43
|
field_id,
|
|
44
|
+
reserved_attribute,
|
|
44
45
|
required,
|
|
45
46
|
size,
|
|
46
47
|
padding,
|
|
@@ -74,7 +75,15 @@
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
const value = input.value.trim();
|
|
77
|
-
|
|
78
|
+
if (reserved_attribute) {
|
|
79
|
+
if (!onReservedAttributeChanged) {
|
|
80
|
+
console.error("onReservedAttributeChanged is not set");
|
|
81
|
+
} else {
|
|
82
|
+
onReservedAttributeChanged(reserved_attribute, value);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
onInputChanged?.(field_id, value);
|
|
86
|
+
}
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
const oninput: FormEventHandler<HTMLInputElement> = (event) => {
|
|
@@ -90,7 +99,12 @@
|
|
|
90
99
|
|
|
91
100
|
const getColorMode = getColorModeContext();
|
|
92
101
|
const colorMode = $derived(getColorMode());
|
|
93
|
-
const {
|
|
102
|
+
const {
|
|
103
|
+
uiConfig,
|
|
104
|
+
onInputChanged,
|
|
105
|
+
onReservedAttributeChanged,
|
|
106
|
+
selectedPackageId,
|
|
107
|
+
} = getPaywallContext();
|
|
94
108
|
const packageInfo = getOptionalPackageInfoContext();
|
|
95
109
|
|
|
96
110
|
const wrapperStyle = $derived(
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
type VariablesStore,
|
|
16
16
|
} from "../../stores/variables";
|
|
17
17
|
import type { ColorMode } from "../../types";
|
|
18
|
+
import type { ColorScheme } from "../../types/colors";
|
|
18
19
|
import type {
|
|
19
20
|
Action,
|
|
20
21
|
CompleteWorkflowNavigateArgs,
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
setPackageInfoContext,
|
|
49
50
|
} from "../../stores/packageInfo";
|
|
50
51
|
import type { WalletButtonRender } from "../../types/wallet";
|
|
52
|
+
import type { ReservedAttribute } from "../../types/components/input-text";
|
|
51
53
|
|
|
52
54
|
/**
|
|
53
55
|
* Props are captured once at mount and are not reactive to subsequent changes.
|
|
@@ -89,6 +91,10 @@
|
|
|
89
91
|
value: string,
|
|
90
92
|
actionId?: string,
|
|
91
93
|
) => void;
|
|
94
|
+
onReservedAttributeChanged?: (
|
|
95
|
+
reservedAttribute: ReservedAttribute,
|
|
96
|
+
value: string,
|
|
97
|
+
) => void;
|
|
92
98
|
maxContentWidth?: string;
|
|
93
99
|
initialInputSelections?: InitialInputSelections;
|
|
94
100
|
/**
|
|
@@ -105,6 +111,12 @@
|
|
|
105
111
|
* ```
|
|
106
112
|
*/
|
|
107
113
|
customVariables?: CustomVariables;
|
|
114
|
+
/**
|
|
115
|
+
* Optional baseline safe-area colour applied when the paywall background
|
|
116
|
+
* can't derive one and `background.safe_area_fallback_color` is unset.
|
|
117
|
+
* Hosts (e.g. workflow runtimes) pass their workflow-level fallback here.
|
|
118
|
+
*/
|
|
119
|
+
safeAreaFallbackColor?: ColorScheme | null;
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
const {
|
|
@@ -126,10 +138,12 @@
|
|
|
126
138
|
uiConfig,
|
|
127
139
|
walletButtonRender,
|
|
128
140
|
onInputChanged,
|
|
141
|
+
onReservedAttributeChanged,
|
|
129
142
|
hideBackButtons = false,
|
|
130
143
|
maxContentWidth,
|
|
131
144
|
initialInputSelections = {},
|
|
132
145
|
customVariables = {},
|
|
146
|
+
safeAreaFallbackColor,
|
|
133
147
|
}: Props = $props();
|
|
134
148
|
|
|
135
149
|
const getColorMode = setColorModeContext(() => preferredColorMode);
|
|
@@ -140,7 +154,11 @@
|
|
|
140
154
|
|
|
141
155
|
const instanceId: symbol = Symbol();
|
|
142
156
|
$effect(() =>
|
|
143
|
-
applyDocumentBackground(instanceId, viewportBackdropModel,
|
|
157
|
+
applyDocumentBackground(instanceId, viewportBackdropModel, {
|
|
158
|
+
paywallData,
|
|
159
|
+
colorMode: getColorMode(),
|
|
160
|
+
hostFallbackColor: safeAreaFallbackColor,
|
|
161
|
+
}),
|
|
144
162
|
);
|
|
145
163
|
|
|
146
164
|
const { default_locale, components_config, components_localizations } =
|
|
@@ -285,6 +303,7 @@
|
|
|
285
303
|
onNavigateToUrl: onNavigateToUrlClicked,
|
|
286
304
|
onButtonAction,
|
|
287
305
|
onInputChanged,
|
|
306
|
+
onReservedAttributeChanged,
|
|
288
307
|
walletButtonRender,
|
|
289
308
|
uiConfig,
|
|
290
309
|
hideBackButtons,
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { type InitialInputSelections } from "../../stores/inputValidation";
|
|
2
2
|
import type { ColorMode } from "../../types";
|
|
3
|
+
import type { ColorScheme } from "../../types/colors";
|
|
3
4
|
import type { CompleteWorkflowNavigateArgs } from "../../types/components/button";
|
|
4
5
|
import type { OnComponentInteraction } from "../../types/paywall-component-interaction";
|
|
5
6
|
import type { PaywallData } from "../../types/paywall";
|
|
6
7
|
import type { UIConfig } from "../../types/ui-config";
|
|
7
8
|
import { type CustomVariables, type PackageInfo, type VariableDictionary } from "../../types/variables";
|
|
8
9
|
import type { WalletButtonRender } from "../../types/wallet";
|
|
10
|
+
import type { ReservedAttribute } from "../../types/components/input-text";
|
|
9
11
|
/**
|
|
10
12
|
* Props are captured once at mount and are not reactive to subsequent changes.
|
|
11
13
|
* The paywall should be remounted to reflect new prop values.
|
|
@@ -40,6 +42,7 @@ interface Props {
|
|
|
40
42
|
hideBackButtons?: boolean;
|
|
41
43
|
walletButtonRender?: WalletButtonRender;
|
|
42
44
|
onInputChanged?: (fieldId: string, value: string, actionId?: string) => void;
|
|
45
|
+
onReservedAttributeChanged?: (reservedAttribute: ReservedAttribute, value: string) => void;
|
|
43
46
|
maxContentWidth?: string;
|
|
44
47
|
initialInputSelections?: InitialInputSelections;
|
|
45
48
|
/**
|
|
@@ -56,6 +59,12 @@ interface Props {
|
|
|
56
59
|
* ```
|
|
57
60
|
*/
|
|
58
61
|
customVariables?: CustomVariables;
|
|
62
|
+
/**
|
|
63
|
+
* Optional baseline safe-area colour applied when the paywall background
|
|
64
|
+
* can't derive one and `background.safe_area_fallback_color` is unset.
|
|
65
|
+
* Hosts (e.g. workflow runtimes) pass their workflow-level fallback here.
|
|
66
|
+
*/
|
|
67
|
+
safeAreaFallbackColor?: ColorScheme | null;
|
|
59
68
|
}
|
|
60
69
|
declare const Paywall: import("svelte").Component<Props, {}, "">;
|
|
61
70
|
type Paywall = ReturnType<typeof Paywall>;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Paywall from "../paywall/Paywall.svelte";
|
|
3
3
|
import type { CompleteWorkflowNavigateArgs } from "../../types/components/button";
|
|
4
|
+
import type { ColorScheme } from "../../types/colors";
|
|
4
5
|
import type { InitialInputSelections } from "../../stores/inputValidation";
|
|
5
6
|
import type { OnComponentInteraction } from "../../types/paywall-component-interaction";
|
|
6
7
|
import type { WorkflowScreen } from "../../types/workflow";
|
|
7
8
|
import type { VariableDictionary } from "../../types/variables";
|
|
8
9
|
import type { WalletButtonRender } from "../../types/wallet";
|
|
9
10
|
import type { UIConfig } from "../../types/ui-config";
|
|
11
|
+
import type { ReservedAttribute } from "../../types/components/input-text";
|
|
10
12
|
interface Props {
|
|
11
13
|
paywallComponents: WorkflowScreen | null | undefined;
|
|
12
14
|
selectedLocale?: string;
|
|
@@ -28,10 +30,15 @@
|
|
|
28
30
|
value: string,
|
|
29
31
|
actionId?: string,
|
|
30
32
|
) => void;
|
|
33
|
+
onReservedAttributeChanged?: (
|
|
34
|
+
reservedAttribute: ReservedAttribute,
|
|
35
|
+
value: string,
|
|
36
|
+
) => void;
|
|
31
37
|
onCompleteWorkflowNavigate?: (
|
|
32
38
|
args: CompleteWorkflowNavigateArgs,
|
|
33
39
|
) => void | Promise<void>;
|
|
34
40
|
walletButtonRender?: WalletButtonRender;
|
|
41
|
+
safeAreaFallbackColor?: ColorScheme | null;
|
|
35
42
|
}
|
|
36
43
|
const {
|
|
37
44
|
paywallComponents,
|
|
@@ -47,8 +54,10 @@
|
|
|
47
54
|
variablesPerPackage,
|
|
48
55
|
initialInputSelections = {},
|
|
49
56
|
onInputChanged,
|
|
57
|
+
onReservedAttributeChanged,
|
|
50
58
|
onCompleteWorkflowNavigate,
|
|
51
59
|
walletButtonRender,
|
|
60
|
+
safeAreaFallbackColor,
|
|
52
61
|
}: Props = $props();
|
|
53
62
|
</script>
|
|
54
63
|
|
|
@@ -75,7 +84,9 @@
|
|
|
75
84
|
{onComponentInteraction}
|
|
76
85
|
{onPurchaseClicked}
|
|
77
86
|
{onInputChanged}
|
|
87
|
+
{onReservedAttributeChanged}
|
|
78
88
|
{walletButtonRender}
|
|
89
|
+
{safeAreaFallbackColor}
|
|
79
90
|
onError={(error) => {
|
|
80
91
|
console.error("Paywall error:", error);
|
|
81
92
|
}}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { CompleteWorkflowNavigateArgs } from "../../types/components/button";
|
|
2
|
+
import type { ColorScheme } from "../../types/colors";
|
|
2
3
|
import type { InitialInputSelections } from "../../stores/inputValidation";
|
|
3
4
|
import type { OnComponentInteraction } from "../../types/paywall-component-interaction";
|
|
4
5
|
import type { WorkflowScreen } from "../../types/workflow";
|
|
5
6
|
import type { VariableDictionary } from "../../types/variables";
|
|
6
7
|
import type { WalletButtonRender } from "../../types/wallet";
|
|
7
8
|
import type { UIConfig } from "../../types/ui-config";
|
|
9
|
+
import type { ReservedAttribute } from "../../types/components/input-text";
|
|
8
10
|
interface Props {
|
|
9
11
|
paywallComponents: WorkflowScreen | null | undefined;
|
|
10
12
|
selectedLocale?: string;
|
|
@@ -19,8 +21,10 @@ interface Props {
|
|
|
19
21
|
variablesPerPackage?: Record<string, VariableDictionary>;
|
|
20
22
|
initialInputSelections?: InitialInputSelections;
|
|
21
23
|
onInputChanged?: (fieldId: string, value: string, actionId?: string) => void;
|
|
24
|
+
onReservedAttributeChanged?: (reservedAttribute: ReservedAttribute, value: string) => void;
|
|
22
25
|
onCompleteWorkflowNavigate?: (args: CompleteWorkflowNavigateArgs) => void | Promise<void>;
|
|
23
26
|
walletButtonRender?: WalletButtonRender;
|
|
27
|
+
safeAreaFallbackColor?: ColorScheme | null;
|
|
24
28
|
}
|
|
25
29
|
declare const Screen: import("svelte").Component<Props, {}, "">;
|
|
26
30
|
type Screen = ReturnType<typeof Screen>;
|
package/dist/stores/paywall.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Action } from "../types/components/button";
|
|
2
|
+
import type { ReservedAttribute } from "../types/components/input-text";
|
|
2
3
|
import type { ComponentInteractionData } from "../types/paywall-component-interaction";
|
|
3
4
|
import type { UIConfig } from "../types/ui-config";
|
|
4
5
|
import type { PackageInfo, VariableDictionary } from "../types/variables";
|
|
@@ -21,6 +22,7 @@ type PaywallContext = Readonly<{
|
|
|
21
22
|
emitComponentInteraction: (data: ComponentInteractionData) => void;
|
|
22
23
|
onNavigateToUrl?: (url: string) => void;
|
|
23
24
|
onInputChanged?: (fieldId: string, value: string, actionId?: string) => void;
|
|
25
|
+
onReservedAttributeChanged?: (reservedAttribute: ReservedAttribute, value: string) => void;
|
|
24
26
|
walletButtonRender?: WalletButtonRender;
|
|
25
27
|
onWalletButtonReady?: (walletButtonAvailable?: boolean) => void;
|
|
26
28
|
onButtonAction: (action: Action, actionId?: string) => void;
|
package/dist/types/colors.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { ColorGradientScheme } from "../colors";
|
|
|
4
4
|
import type { Overrides } from "../overrides";
|
|
5
5
|
export type InputTextCapitalizeType = "none" | "sentences" | "words" | "characters";
|
|
6
6
|
export type InputTextKeyboardType = "decimal" | "email" | "numeric" | "tel" | "text" | "url";
|
|
7
|
+
export type ReservedAttribute = "$email" | "$displayName" | "$phoneNumber";
|
|
7
8
|
export interface InputTextProps extends BaseComponent {
|
|
8
9
|
type: "input_text";
|
|
9
10
|
visible?: boolean | null;
|
|
@@ -11,6 +12,7 @@ export interface InputTextProps extends BaseComponent {
|
|
|
11
12
|
keyboard_type: InputTextKeyboardType;
|
|
12
13
|
capitalize: InputTextCapitalizeType;
|
|
13
14
|
field_id: string;
|
|
15
|
+
reserved_attribute?: ReservedAttribute | null;
|
|
14
16
|
required: boolean;
|
|
15
17
|
size: SizeType;
|
|
16
18
|
padding: Spacing;
|
package/dist/types/paywall.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Background } from "./background";
|
|
2
|
+
import type { ColorScheme } from "./colors";
|
|
2
3
|
import type { FooterProps } from "./components/footer";
|
|
3
4
|
import type { HeaderProps } from "./components/header";
|
|
4
5
|
import type { StackProps } from "./components/stack";
|
|
@@ -8,6 +9,7 @@ export interface RootPaywall {
|
|
|
8
9
|
stack: StackProps;
|
|
9
10
|
sticky_footer?: FooterProps | null;
|
|
10
11
|
header?: HeaderProps | null;
|
|
12
|
+
safe_area_fallback_color?: ColorScheme | null;
|
|
11
13
|
}
|
|
12
14
|
export interface ComponentConfig {
|
|
13
15
|
colors?: Record<string, string>;
|
package/dist/types.d.ts
CHANGED
|
@@ -120,4 +120,6 @@ export declare enum StackDistribution {
|
|
|
120
120
|
end = "flex-end"
|
|
121
121
|
}
|
|
122
122
|
export type { WorkflowScreen } from "./types/workflow";
|
|
123
|
+
export type { ColorScheme } from "./types/colors";
|
|
123
124
|
export type { ComponentInteractionData, ComponentInteractionType, OnComponentInteraction, } from "./types/paywall-component-interaction";
|
|
125
|
+
export type { ReservedAttribute } from "./types/components/input-text";
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import type { PaywallData } from "../types/paywall";
|
|
2
2
|
import type { PaywallRootBackgroundModel } from "./background-utils";
|
|
3
|
-
|
|
3
|
+
import type { ColorMode } from "../types";
|
|
4
|
+
import type { ColorScheme } from "../types/colors";
|
|
5
|
+
export declare function applyDocumentBackground(instanceId: symbol, model: PaywallRootBackgroundModel, options: {
|
|
6
|
+
paywallData: PaywallData | null | undefined;
|
|
7
|
+
colorMode: ColorMode;
|
|
8
|
+
hostFallbackColor?: ColorScheme | null;
|
|
9
|
+
}): () => void;
|
|
@@ -1,40 +1,14 @@
|
|
|
1
|
+
import { getBackgroundSafeAreaColors, resolveSafeAreaFallbackCss, SAFE_AREA_FALLBACK_COLOR_CSS_VAR, } from "./safe-area-background";
|
|
1
2
|
// Last writer wins, but cleanup only fires for the last writer — otherwise an
|
|
2
|
-
// unmounting paywall would clear a variable set by another paywall that
|
|
3
|
-
// it during a transition.
|
|
3
|
+
// unmounting paywall would clear a variable set by another paywall that
|
|
4
|
+
// overlapped it during a transition.
|
|
4
5
|
let lastBgWriter = null;
|
|
5
|
-
|
|
6
|
-
// safe-area painting for the opposite strip. A vertical gradient's edge stop
|
|
7
|
-
// can stand in as a solid fallback for that strip; any other angle would smear
|
|
8
|
-
// a horizontal colour range across the strip that no single colour represents.
|
|
9
|
-
function gradientSafeAreaFallbackColour(gradient, hasHeader, hasFooter) {
|
|
10
|
-
if (!hasHeader && !hasFooter)
|
|
11
|
-
return null;
|
|
12
|
-
if (hasHeader && hasFooter)
|
|
13
|
-
return null;
|
|
14
|
-
const angleMatch = gradient.match(/linear-gradient\((\d+)deg/);
|
|
15
|
-
if (!angleMatch)
|
|
16
|
-
return null;
|
|
17
|
-
if (parseInt(angleMatch[1], 10) % 180 !== 0)
|
|
18
|
-
return null;
|
|
19
|
-
const stops = gradient.match(/#[0-9a-fA-F]{6,8}/g);
|
|
20
|
-
if (!stops || stops.length < 2)
|
|
21
|
-
return null;
|
|
22
|
-
const candidate = hasHeader ? stops[stops.length - 1] : stops[0];
|
|
23
|
-
if (candidate.length === 9 && candidate.slice(-2).toLowerCase() !== "ff") {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
return candidate;
|
|
27
|
-
}
|
|
28
|
-
// Published as a CSS variable so consumer surfaces can opt in to safe-area
|
|
29
|
-
// painting (`background: var(--rc-purchases-ui-bg-color, Canvas)` on any
|
|
30
|
-
// element that extends into the safe-area canvas — typically html, body, or
|
|
31
|
-
// a position:fixed root). Writing to documentElement.backgroundColor directly
|
|
32
|
-
// would bleed the paywall colour onto host page chrome that shouldn't take
|
|
33
|
-
// it on, so we expose the value rather than apply it.
|
|
34
|
-
export function applyDocumentBackground(instanceId, model, paywallData) {
|
|
6
|
+
export function applyDocumentBackground(instanceId, model, options) {
|
|
35
7
|
if (typeof document === "undefined")
|
|
36
8
|
return () => { };
|
|
9
|
+
const { paywallData, colorMode, hostFallbackColor } = options;
|
|
37
10
|
const root = document.documentElement;
|
|
11
|
+
const base = paywallData?.components_config?.base;
|
|
38
12
|
let value = null;
|
|
39
13
|
if (model.kind === "style" && model.style.background) {
|
|
40
14
|
const bg = model.style.background;
|
|
@@ -42,18 +16,26 @@ export function applyDocumentBackground(instanceId, model, paywallData) {
|
|
|
42
16
|
value = bg;
|
|
43
17
|
}
|
|
44
18
|
else {
|
|
45
|
-
const
|
|
46
|
-
|
|
19
|
+
const edges = getBackgroundSafeAreaColors(base?.background, colorMode, { width: window.innerWidth, height: window.innerHeight }, {
|
|
20
|
+
stickyComponents: {
|
|
21
|
+
hasHeader: base?.header != null,
|
|
22
|
+
hasFooter: base?.sticky_footer != null,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
value = edges.top ?? edges.bottom;
|
|
47
26
|
}
|
|
48
27
|
}
|
|
28
|
+
if (value === null) {
|
|
29
|
+
value = resolveSafeAreaFallbackCss(base?.safe_area_fallback_color, hostFallbackColor, colorMode);
|
|
30
|
+
}
|
|
49
31
|
if (value !== null) {
|
|
50
32
|
lastBgWriter = instanceId;
|
|
51
|
-
root.style.setProperty(
|
|
33
|
+
root.style.setProperty(SAFE_AREA_FALLBACK_COLOR_CSS_VAR, value);
|
|
52
34
|
}
|
|
53
35
|
return () => {
|
|
54
36
|
if (lastBgWriter === instanceId) {
|
|
55
37
|
lastBgWriter = null;
|
|
56
|
-
root.style.removeProperty(
|
|
38
|
+
root.style.removeProperty(SAFE_AREA_FALLBACK_COLOR_CSS_VAR);
|
|
57
39
|
}
|
|
58
40
|
};
|
|
59
41
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ColorMode } from "../types";
|
|
2
|
+
import type { Background } from "../types/background";
|
|
3
|
+
import type { ColorScheme } from "../types/colors";
|
|
4
|
+
import type { PaywallData } from "../types/paywall";
|
|
5
|
+
type Viewport = {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
};
|
|
9
|
+
type SafeAreaEdge = "top" | "bottom";
|
|
10
|
+
export declare const SAFE_AREA_FALLBACK_COLOR_CSS_VAR = "--rc-purchases-ui-bg-color";
|
|
11
|
+
export declare function resolveSafeAreaFallbackCss(paywallOverride: ColorScheme | null | undefined, hostFallback: ColorScheme | null | undefined, colorMode: ColorMode): string | null;
|
|
12
|
+
export declare function getSafeAreaFallbackColorHeadStyles(paywallData: PaywallData | null | undefined, hostFallbackColor?: ColorScheme | null): string;
|
|
13
|
+
export declare function getBackgroundSafeAreaColors(background: Background | null | undefined, colorMode: ColorMode, viewport: Viewport, options?: {
|
|
14
|
+
stickyComponents?: {
|
|
15
|
+
hasHeader?: boolean;
|
|
16
|
+
hasFooter?: boolean;
|
|
17
|
+
};
|
|
18
|
+
paywallFallbackColor?: ColorScheme | null;
|
|
19
|
+
hostFallbackColor?: ColorScheme | null;
|
|
20
|
+
}): Record<SafeAreaEdge, string | null>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { mapColorInfo, mapColorMode } from "./base-utils";
|
|
2
|
+
export const SAFE_AREA_FALLBACK_COLOR_CSS_VAR = "--rc-purchases-ui-bg-color";
|
|
3
|
+
// Light-only schemes must not bleed into the dark media query — a dark rule
|
|
4
|
+
// reading from `light` would override the light value.
|
|
5
|
+
function getSafeAreaFallbackColorInfoForMode(paywallOverride, hostFallback, colorMode) {
|
|
6
|
+
const colorAtMode = (scheme) => colorMode === "dark" ? scheme?.dark : scheme?.light;
|
|
7
|
+
return colorAtMode(paywallOverride) ?? colorAtMode(hostFallback) ?? null;
|
|
8
|
+
}
|
|
9
|
+
const HEX_COLOR_REGEX = /^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/;
|
|
10
|
+
function isHexColor(value) {
|
|
11
|
+
return HEX_COLOR_REGEX.test(value);
|
|
12
|
+
}
|
|
13
|
+
function isOpaqueHexColor(value) {
|
|
14
|
+
const match = HEX_COLOR_REGEX.exec(value);
|
|
15
|
+
return match != null && (match[1] == null || match[1].toLowerCase() === "ff");
|
|
16
|
+
}
|
|
17
|
+
function normalizeHexColor(color) {
|
|
18
|
+
return color.length === 7 ? `${color.toLowerCase()}ff` : color.toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
function getCssHexColor(value) {
|
|
21
|
+
return value?.type === "hex" && isHexColor(value.value) ? value.value : null;
|
|
22
|
+
}
|
|
23
|
+
function isAxisAlignedVerticalGradient(degrees) {
|
|
24
|
+
return Math.abs(((degrees % 180) + 180) % 180) < 1e-10;
|
|
25
|
+
}
|
|
26
|
+
// Alias-aware counterpart for the SDK runtime; mapColorInfo resolves aliases
|
|
27
|
+
// that the hex-only path can't.
|
|
28
|
+
export function resolveSafeAreaFallbackCss(paywallOverride, hostFallback, colorMode) {
|
|
29
|
+
const info = getSafeAreaFallbackColorInfoForMode(paywallOverride, hostFallback, colorMode);
|
|
30
|
+
return info ? mapColorInfo(info) : null;
|
|
31
|
+
}
|
|
32
|
+
// SSR has no real viewport (no off-axis/radial sampling). Hex and alias
|
|
33
|
+
// backgrounds short-circuit immediately; axis-aligned vertical gradients go
|
|
34
|
+
// through the sticky-component disambiguation since edges may differ. Alias
|
|
35
|
+
// values are emitted as var() references — valid CSS that resolves once the
|
|
36
|
+
// SDK stylesheet is present. Invalid hex is skipped rather than emitted.
|
|
37
|
+
function getSsrSafeAreaCssForMode(paywallData, hostFallback, colorMode) {
|
|
38
|
+
const base = paywallData?.components_config?.base;
|
|
39
|
+
const background = base?.background;
|
|
40
|
+
if (background?.type === "color") {
|
|
41
|
+
const color = mapColorMode(colorMode, background.value);
|
|
42
|
+
if (color.type === "hex") {
|
|
43
|
+
const hex = getCssHexColor(color);
|
|
44
|
+
if (hex)
|
|
45
|
+
return hex;
|
|
46
|
+
}
|
|
47
|
+
else if (color.type === "alias") {
|
|
48
|
+
return mapColorInfo(color);
|
|
49
|
+
}
|
|
50
|
+
else if (color.type === "linear" &&
|
|
51
|
+
isAxisAlignedVerticalGradient(color.degrees)) {
|
|
52
|
+
const edges = getBackgroundSafeAreaColors(background, colorMode, { width: 1, height: 1 }, {
|
|
53
|
+
stickyComponents: {
|
|
54
|
+
hasHeader: base?.header != null,
|
|
55
|
+
hasFooter: base?.sticky_footer != null,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const sampled = edges.top ?? edges.bottom;
|
|
59
|
+
if (sampled && isHexColor(sampled))
|
|
60
|
+
return sampled;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const fallbackInfo = getSafeAreaFallbackColorInfoForMode(base?.safe_area_fallback_color, hostFallback, colorMode);
|
|
64
|
+
if (fallbackInfo == null)
|
|
65
|
+
return null;
|
|
66
|
+
return fallbackInfo.type === "alias"
|
|
67
|
+
? mapColorInfo(fallbackInfo)
|
|
68
|
+
: getCssHexColor(fallbackInfo);
|
|
69
|
+
}
|
|
70
|
+
export function getSafeAreaFallbackColorHeadStyles(paywallData, hostFallbackColor) {
|
|
71
|
+
return ["light", "dark"]
|
|
72
|
+
.map((mode) => {
|
|
73
|
+
const value = getSsrSafeAreaCssForMode(paywallData, hostFallbackColor, mode);
|
|
74
|
+
return value
|
|
75
|
+
? `@media (prefers-color-scheme: ${mode}) { html, body { ${SAFE_AREA_FALLBACK_COLOR_CSS_VAR}: ${value}; } }`
|
|
76
|
+
: null;
|
|
77
|
+
})
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.join("\n");
|
|
80
|
+
}
|
|
81
|
+
function colorAtPercent(sortedPoints, percent) {
|
|
82
|
+
const firstPoint = sortedPoints[0];
|
|
83
|
+
const lastPoint = sortedPoints.at(-1);
|
|
84
|
+
if (firstPoint == null || lastPoint == null) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (percent <= firstPoint.percent) {
|
|
88
|
+
return normalizeHexColor(firstPoint.color);
|
|
89
|
+
}
|
|
90
|
+
if (percent >= lastPoint.percent) {
|
|
91
|
+
return normalizeHexColor(lastPoint.color);
|
|
92
|
+
}
|
|
93
|
+
const nextPointIndex = sortedPoints.findIndex((point) => point.percent >= percent);
|
|
94
|
+
const previousPoint = sortedPoints[nextPointIndex - 1];
|
|
95
|
+
const nextPoint = sortedPoints[nextPointIndex];
|
|
96
|
+
if (previousPoint == null || nextPoint == null) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
if (previousPoint.color.toLowerCase() === nextPoint.color.toLowerCase()) {
|
|
100
|
+
return normalizeHexColor(previousPoint.color);
|
|
101
|
+
}
|
|
102
|
+
if (previousPoint.percent === nextPoint.percent) {
|
|
103
|
+
return normalizeHexColor(nextPoint.color);
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function getSortedGradientPoints(points) {
|
|
108
|
+
return points
|
|
109
|
+
.filter((point) => Number.isFinite(point.percent) && isHexColor(point.color))
|
|
110
|
+
.slice()
|
|
111
|
+
.sort((a, b) => a.percent - b.percent);
|
|
112
|
+
}
|
|
113
|
+
function solidColorForRange(sortedPoints, [rangeStart, rangeEnd]) {
|
|
114
|
+
const expectedColor = colorAtPercent(sortedPoints, rangeStart);
|
|
115
|
+
if (expectedColor == null || !isOpaqueHexColor(expectedColor)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const breakpoints = [
|
|
119
|
+
rangeStart,
|
|
120
|
+
...sortedPoints
|
|
121
|
+
.map((point) => point.percent)
|
|
122
|
+
.filter((percent) => percent > rangeStart && percent < rangeEnd),
|
|
123
|
+
rangeEnd,
|
|
124
|
+
];
|
|
125
|
+
const isSolid = breakpoints.every((breakpoint, index) => {
|
|
126
|
+
if (colorAtPercent(sortedPoints, breakpoint) !== expectedColor) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const nextBreakpoint = breakpoints[index + 1];
|
|
130
|
+
if (nextBreakpoint == null || nextBreakpoint === breakpoint) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
return (colorAtPercent(sortedPoints, (breakpoint + nextBreakpoint) / 2) ===
|
|
134
|
+
expectedColor);
|
|
135
|
+
});
|
|
136
|
+
return isSolid ? expectedColor : null;
|
|
137
|
+
}
|
|
138
|
+
function snapToZero(value) {
|
|
139
|
+
return Math.abs(value) < 1e-10 ? 0 : value;
|
|
140
|
+
}
|
|
141
|
+
function linearGradientPositionPercent(degrees, x, y, viewport) {
|
|
142
|
+
const radians = (degrees * Math.PI) / 180;
|
|
143
|
+
// Math.sin(Math.PI) is ~1.22e-16, not 0. Snap so a 180° gradient's edge
|
|
144
|
+
// sits exactly on a stop instead of just barely missing it.
|
|
145
|
+
const dx = snapToZero(Math.sin(radians));
|
|
146
|
+
const dy = snapToZero(-Math.cos(radians));
|
|
147
|
+
const gradientLength = Math.abs(dx) * viewport.width + Math.abs(dy) * viewport.height;
|
|
148
|
+
const offset = (x - viewport.width / 2) * dx + (y - viewport.height / 2) * dy;
|
|
149
|
+
return ((offset + gradientLength / 2) / gradientLength) * 100;
|
|
150
|
+
}
|
|
151
|
+
function linearEdgeRangePercent(degrees, edge, viewport) {
|
|
152
|
+
const y = edge === "top" ? 0 : viewport.height;
|
|
153
|
+
const start = linearGradientPositionPercent(degrees, 0, y, viewport);
|
|
154
|
+
const end = linearGradientPositionPercent(degrees, viewport.width, y, viewport);
|
|
155
|
+
return [Math.min(start, end), Math.max(start, end)];
|
|
156
|
+
}
|
|
157
|
+
function radialGradientPositionPercent(x, y, viewport) {
|
|
158
|
+
const radius = Math.hypot(viewport.width / 2, viewport.height / 2);
|
|
159
|
+
const distance = Math.hypot(x - viewport.width / 2, y - viewport.height / 2);
|
|
160
|
+
return (distance / radius) * 100;
|
|
161
|
+
}
|
|
162
|
+
function radialEdgeRangePercent(edge, viewport) {
|
|
163
|
+
const y = edge === "top" ? 0 : viewport.height;
|
|
164
|
+
const center = radialGradientPositionPercent(viewport.width / 2, y, viewport);
|
|
165
|
+
const corner = radialGradientPositionPercent(0, y, viewport);
|
|
166
|
+
return [Math.min(center, corner), Math.max(center, corner)];
|
|
167
|
+
}
|
|
168
|
+
function getSolidEdgeColor(color, edge, viewport, sortedPoints) {
|
|
169
|
+
switch (color.type) {
|
|
170
|
+
case "hex":
|
|
171
|
+
case "alias":
|
|
172
|
+
return mapColorInfo(color);
|
|
173
|
+
case "linear":
|
|
174
|
+
return solidColorForRange(sortedPoints ?? [], linearEdgeRangePercent(color.degrees, edge, viewport));
|
|
175
|
+
case "radial":
|
|
176
|
+
return solidColorForRange(sortedPoints ?? [], radialEdgeRangePercent(edge, viewport));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// `position: sticky` promotes a header/footer to its own compositor layer
|
|
180
|
+
// regardless of fill, so even transparent sticky elements break the opposite
|
|
181
|
+
// safe-area strip. Pass `stickyComponents` when painting into a single CSS
|
|
182
|
+
// variable (SDK runtime + SSR head) so the covered edge is nulled and the
|
|
183
|
+
// caller can fall through to the override/host chain; editor previews leave
|
|
184
|
+
// it absent.
|
|
185
|
+
export function getBackgroundSafeAreaColors(background, colorMode, viewport, options) {
|
|
186
|
+
let top = null;
|
|
187
|
+
let bottom = null;
|
|
188
|
+
if (background?.type === "color") {
|
|
189
|
+
const color = mapColorMode(colorMode, background.value);
|
|
190
|
+
const sortedPoints = color.type === "linear" || color.type === "radial"
|
|
191
|
+
? getSortedGradientPoints(color.points)
|
|
192
|
+
: null;
|
|
193
|
+
top = getSolidEdgeColor(color, "top", viewport, sortedPoints);
|
|
194
|
+
bottom = getSolidEdgeColor(color, "bottom", viewport, sortedPoints);
|
|
195
|
+
}
|
|
196
|
+
if (options?.stickyComponents) {
|
|
197
|
+
const hasHeader = options.stickyComponents.hasHeader === true;
|
|
198
|
+
const hasFooter = options.stickyComponents.hasFooter === true;
|
|
199
|
+
// A single CSS variable can only paint one strip; emit only when exactly
|
|
200
|
+
// one edge is exposed (the opposite edge is occluded by a sticky element).
|
|
201
|
+
top = hasFooter && !hasHeader ? top : null;
|
|
202
|
+
bottom = hasHeader && !hasFooter ? bottom : null;
|
|
203
|
+
}
|
|
204
|
+
if (options?.paywallFallbackColor != null ||
|
|
205
|
+
options?.hostFallbackColor != null) {
|
|
206
|
+
const fallback = resolveSafeAreaFallbackCss(options.paywallFallbackColor, options.hostFallbackColor, colorMode);
|
|
207
|
+
top = top ?? fallback;
|
|
208
|
+
bottom = bottom ?? fallback;
|
|
209
|
+
}
|
|
210
|
+
return { top, bottom };
|
|
211
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getBackgroundSafeAreaColors, getSafeAreaFallbackColorHeadStyles, } from "./safe-area-background";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Public surface for `@revenuecat/purchases-ui-js/safe-area`. The CSS var
|
|
2
|
+
// name and helpers used internally by `applyDocumentBackground` are
|
|
3
|
+
// deliberately not re-exported.
|
|
4
|
+
export { getBackgroundSafeAreaColors, getSafeAreaFallbackColorHeadStyles, } from "./safe-area-background";
|
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.
|
|
5
|
+
"version": "4.5.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "RevenueCat, Inc."
|
|
8
8
|
},
|
|
@@ -65,6 +65,10 @@
|
|
|
65
65
|
},
|
|
66
66
|
"./web-components": {
|
|
67
67
|
"default": "./dist/web-components/index.js"
|
|
68
|
+
},
|
|
69
|
+
"./safe-area": {
|
|
70
|
+
"types": "./dist/utils/safe-area.d.ts",
|
|
71
|
+
"default": "./dist/utils/safe-area.js"
|
|
68
72
|
}
|
|
69
73
|
},
|
|
70
74
|
"engines": {
|