@pushwoosh/websdk-common 0.0.0 → 6.17.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/package.json CHANGED
@@ -1,23 +1,48 @@
1
1
  {
2
2
  "name": "@pushwoosh/websdk-common",
3
- "version": "0.0.0",
4
- "description": "Framework-agnostic code shared by the Pushwoosh WebSDK and the Control Panel",
3
+ "version": "6.17.0",
4
+ "description": "Code shared by the Pushwoosh WebSDK and the Control Panel, one entry point per module (web-popups: web popup model and renderer; subscription-widget: push subscription widget model and renderer)",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
7
7
  "types": "index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./index.d.ts",
11
+ "default": "./index.js"
12
+ },
13
+ "./web-popups": {
14
+ "types": "./web-popups/index.d.ts",
15
+ "default": "./web-popups/index.js"
16
+ },
17
+ "./subscription-widget": {
18
+ "types": "./subscription-widget/index.d.ts",
19
+ "default": "./subscription-widget/index.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
8
23
  "sideEffects": false,
9
24
  "publishConfig": {
10
25
  "access": "public",
11
26
  "directory": "dist"
12
27
  },
28
+ "devDependencies": {
29
+ "@types/react": "^18.3.31",
30
+ "react": "^18.3.1",
31
+ "typescript": "^6.0.3",
32
+ "vitest": "^4.1.10"
33
+ },
34
+ "peerDependencies": {
35
+ "react": ">=18.0.0"
36
+ },
13
37
  "engines": {
14
38
  "node": ">= 24"
15
39
  },
16
40
  "scripts": {
17
41
  "build": "pushwoosh-engine lib:build",
18
42
  "check": "pnpm run check:lint && pnpm run check:types",
19
- "check:lint": "echo \"Run check:lint\" && NODE_ENV=production eslint ./src",
20
- "check:lint-fix": "echo \"Run check:lint\" && NODE_ENV=production eslint ./src --fix",
21
- "check:types": "echo \"Run check:types\" && pushwoosh-engine lib:check-types"
43
+ "check:lint": "echo \"Run check:lint\" && NODE_ENV=production eslint ./src ./tests",
44
+ "check:lint-fix": "echo \"Run check:lint\" && NODE_ENV=production eslint ./src ./tests --fix",
45
+ "check:types": "echo \"Run check:types\" && pushwoosh-engine lib:check-types && tsc --noEmit -p tsconfig.json",
46
+ "test": "vitest run"
22
47
  }
23
48
  }
@@ -0,0 +1,23 @@
1
+ import { type SubscriptionWidgetConfig, type SubscriptionWidgetPromptVariant, type SubscriptionWidgetReaderProps } from './types';
2
+ import { type WebPopupResolvedColors } from '../web-popups/types';
3
+ /** The prompt box without placement, for CP forms that show it beside their fields. */
4
+ export declare function SubscriptionPromptBox({ config, variant, colors, busy, onAccept, onDecline }: {
5
+ config: SubscriptionWidgetConfig;
6
+ variant: SubscriptionWidgetPromptVariant;
7
+ colors: WebPopupResolvedColors;
8
+ busy?: boolean;
9
+ onAccept?: () => void;
10
+ onDecline?: () => void;
11
+ }): import("react").JSX.Element;
12
+ /** The launcher bell without placement. */
13
+ export declare function SubscriptionLauncherButton({ config, blocked, colors, onClick }: {
14
+ config: SubscriptionWidgetConfig;
15
+ blocked: boolean;
16
+ colors: WebPopupResolvedColors;
17
+ onClick?: () => void;
18
+ }): import("react").JSX.Element;
19
+ /**
20
+ * The launcher plus, while `open`, the prompt; 'live' pins them to the viewport, 'preview' to the
21
+ * nearest positioned parent with no enter animation. The host owns `open` and every side effect.
22
+ */
23
+ export declare function SubscriptionWidgetReader({ config, permission, open, mode, colorScheme, busy, onAccept, onDecline, onLauncherClick, }: SubscriptionWidgetReaderProps): import("react").JSX.Element | null;
@@ -0,0 +1,307 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { SUBSCRIPTION_WIDGET_KEYFRAMES } from './constants';
3
+ import { subscriptionWidgetLauncherVisible, subscriptionWidgetPromptVariant } from './helpers';
4
+ import { useColorScheme } from '../web-popups/colorScheme';
5
+ import { webPopupButtonPillStyle } from '../web-popups/constants';
6
+ import { WEB_POPUP_GLYPH_PATHS } from '../web-popups/glyphs';
7
+ import { resolveWebPopupColors, webPopupDarkEnabled } from '../web-popups/palette';
8
+ const CARD_WIDTH = 360;
9
+ const MODAL_WIDTH = 420;
10
+ const EDGE_GAP = 16;
11
+ const BOX_SHADOW = '0 12px 32px rgba(16, 24, 40, 0.18)';
12
+ const BELL_SLASH_PATH = 'M4 4l16 16';
13
+ function cornerStyle(corner, offset) {
14
+ const [v, h] = corner.split('-');
15
+ return {
16
+ [v]: offset,
17
+ [h]: offset
18
+ };
19
+ }
20
+ function enterAnimation(fromTop, animate) {
21
+ return animate ? `${fromTop ? 'pw-subw-drop' : 'pw-subw-rise'} 0.3s ease-out backwards` : undefined;
22
+ }
23
+ function promptFrameStyle(config, position, animate) {
24
+ const {
25
+ layout,
26
+ position: at
27
+ } = config.prompt;
28
+ const base = {
29
+ position,
30
+ zIndex: config.zIndex,
31
+ boxSizing: 'border-box',
32
+ pointerEvents: 'auto'
33
+ };
34
+ if (layout === 'bar') {
35
+ return {
36
+ ...base,
37
+ left: 0,
38
+ right: 0,
39
+ [at]: 0,
40
+ animation: enterAnimation(at === 'top', animate)
41
+ };
42
+ }
43
+ if (layout === 'modal') {
44
+ return {
45
+ ...base,
46
+ top: '50%',
47
+ left: '50%',
48
+ width: `min(${MODAL_WIDTH}px, calc(100% - ${EDGE_GAP * 2}px))`,
49
+ transform: 'translate(-50%, -50%)',
50
+ animation: animate ? 'pw-subw-fade 0.25s ease-out backwards' : undefined
51
+ };
52
+ }
53
+ const width = `min(${CARD_WIDTH}px, calc(100% - ${EDGE_GAP * 2}px))`;
54
+ const animation = enterAnimation(at.startsWith('top'), animate);
55
+ if (at === 'top' || at === 'bottom') {
56
+ return {
57
+ ...base,
58
+ [at]: EDGE_GAP,
59
+ left: '50%',
60
+ width,
61
+ transform: 'translateX(-50%)',
62
+ animation
63
+ };
64
+ }
65
+ return {
66
+ ...base,
67
+ ...cornerStyle(at, EDGE_GAP),
68
+ width,
69
+ animation
70
+ };
71
+ }
72
+ function Button({
73
+ label,
74
+ primary,
75
+ colors,
76
+ disabled,
77
+ onClick
78
+ }) {
79
+ return _jsx("button", {
80
+ type: "button",
81
+ disabled: disabled,
82
+ onClick: onClick,
83
+ style: {
84
+ ...webPopupButtonPillStyle(primary ? 'primary' : 'secondary', colors),
85
+ display: 'inline-block',
86
+ width: 'auto',
87
+ padding: '8px 18px',
88
+ whiteSpace: 'nowrap',
89
+ opacity: disabled ? 0.6 : 1,
90
+ cursor: disabled ? 'default' : 'pointer'
91
+ },
92
+ children: label
93
+ });
94
+ }
95
+ /** The prompt box without placement, for CP forms that show it beside their fields. */
96
+ export function SubscriptionPromptBox({
97
+ config,
98
+ variant,
99
+ colors,
100
+ busy,
101
+ onAccept,
102
+ onDecline
103
+ }) {
104
+ const {
105
+ prompt,
106
+ texts
107
+ } = config;
108
+ const bar = prompt.layout === 'bar';
109
+ const blocked = variant === 'blocked';
110
+ return _jsxs("div", {
111
+ role: "dialog",
112
+ "aria-label": blocked ? texts.blockedTitle : texts.title,
113
+ style: {
114
+ display: 'flex',
115
+ flexDirection: bar ? 'row' : 'column',
116
+ alignItems: bar ? 'center' : 'stretch',
117
+ flexWrap: bar ? 'wrap' : 'nowrap',
118
+ gap: 16,
119
+ padding: bar ? '12px 20px' : 20,
120
+ boxSizing: 'border-box',
121
+ width: '100%',
122
+ borderRadius: bar ? 0 : prompt.borderRadius,
123
+ boxShadow: BOX_SHADOW,
124
+ backgroundColor: colors.background,
125
+ color: colors.text,
126
+ fontFamily: config.fontFamily,
127
+ colorScheme: colors.scheme
128
+ },
129
+ children: [_jsxs("div", {
130
+ style: {
131
+ display: 'flex',
132
+ alignItems: 'flex-start',
133
+ gap: 12,
134
+ flex: bar ? '1 1 240px' : undefined
135
+ },
136
+ children: [prompt.iconUrl && _jsx("img", {
137
+ src: prompt.iconUrl,
138
+ alt: "",
139
+ width: 48,
140
+ height: 48,
141
+ style: {
142
+ flex: 'none',
143
+ borderRadius: 8,
144
+ objectFit: 'cover'
145
+ }
146
+ }), _jsxs("div", {
147
+ style: {
148
+ display: 'flex',
149
+ flexDirection: 'column',
150
+ gap: 4,
151
+ minWidth: 0
152
+ },
153
+ children: [_jsx("div", {
154
+ style: {
155
+ fontSize: 16,
156
+ fontWeight: 600,
157
+ lineHeight: 1.4
158
+ },
159
+ children: blocked ? texts.blockedTitle : texts.title
160
+ }), _jsx("div", {
161
+ style: {
162
+ fontSize: 14,
163
+ lineHeight: 1.5,
164
+ color: colors.textSecondary
165
+ },
166
+ children: blocked ? texts.blockedMessage : texts.message
167
+ })]
168
+ })]
169
+ }), _jsx("div", {
170
+ style: {
171
+ display: 'flex',
172
+ justifyContent: 'flex-end',
173
+ gap: 8,
174
+ flex: 'none'
175
+ },
176
+ children: blocked ? _jsx(Button, {
177
+ label: texts.closeText,
178
+ primary: true,
179
+ colors: colors,
180
+ onClick: onDecline
181
+ }) : _jsxs(_Fragment, {
182
+ children: [_jsx(Button, {
183
+ label: texts.declineText,
184
+ primary: false,
185
+ colors: colors,
186
+ onClick: onDecline
187
+ }), _jsx(Button, {
188
+ label: texts.acceptText,
189
+ primary: true,
190
+ colors: colors,
191
+ disabled: busy,
192
+ onClick: onAccept
193
+ })]
194
+ })
195
+ })]
196
+ });
197
+ }
198
+ /** The launcher bell without placement. */
199
+ export function SubscriptionLauncherButton({
200
+ config,
201
+ blocked,
202
+ colors,
203
+ onClick
204
+ }) {
205
+ const {
206
+ size
207
+ } = config.launcher;
208
+ const label = blocked ? config.texts.launcherBlockedLabel : config.texts.launcherLabel;
209
+ const iconSize = Math.round(size / 2);
210
+ return _jsx("button", {
211
+ type: "button",
212
+ "aria-label": label,
213
+ title: label,
214
+ onClick: onClick,
215
+ style: {
216
+ display: 'flex',
217
+ alignItems: 'center',
218
+ justifyContent: 'center',
219
+ width: size,
220
+ height: size,
221
+ padding: 0,
222
+ border: 'none',
223
+ borderRadius: '50%',
224
+ backgroundColor: colors.accent,
225
+ color: colors.onAccent,
226
+ boxShadow: BOX_SHADOW,
227
+ cursor: 'pointer',
228
+ opacity: blocked ? 0.7 : 1
229
+ },
230
+ children: _jsxs("svg", {
231
+ width: iconSize,
232
+ height: iconSize,
233
+ viewBox: "0 0 24 24",
234
+ fill: "none",
235
+ stroke: "currentColor",
236
+ strokeWidth: 2,
237
+ strokeLinecap: "round",
238
+ strokeLinejoin: "round",
239
+ "aria-hidden": "true",
240
+ children: [_jsx("path", {
241
+ d: WEB_POPUP_GLYPH_PATHS.bell
242
+ }), blocked && _jsx("path", {
243
+ d: BELL_SLASH_PATH
244
+ })]
245
+ })
246
+ });
247
+ }
248
+ /**
249
+ * The launcher plus, while `open`, the prompt; 'live' pins them to the viewport, 'preview' to the
250
+ * nearest positioned parent with no enter animation. The host owns `open` and every side effect.
251
+ */
252
+ export function SubscriptionWidgetReader({
253
+ config,
254
+ permission,
255
+ open,
256
+ mode = 'live',
257
+ colorScheme = 'auto',
258
+ busy,
259
+ onAccept,
260
+ onDecline,
261
+ onLauncherClick
262
+ }) {
263
+ const scheme = useColorScheme(colorScheme, webPopupDarkEnabled(config));
264
+ const colors = resolveWebPopupColors(config, scheme);
265
+ const live = mode === 'live';
266
+ const position = live ? 'fixed' : 'absolute';
267
+ const launcher = subscriptionWidgetLauncherVisible(config, permission);
268
+ if (!config.enable || !open && !launcher) return null;
269
+ return _jsxs(_Fragment, {
270
+ children: [live && _jsx("style", {
271
+ children: SUBSCRIPTION_WIDGET_KEYFRAMES
272
+ }), launcher && _jsx("div", {
273
+ style: {
274
+ position,
275
+ zIndex: config.zIndex,
276
+ pointerEvents: 'auto',
277
+ ...cornerStyle(config.launcher.position, config.launcher.offset)
278
+ },
279
+ children: _jsx(SubscriptionLauncherButton, {
280
+ config: config,
281
+ blocked: permission === 'denied',
282
+ colors: colors,
283
+ onClick: onLauncherClick
284
+ })
285
+ }), open && config.prompt.overlay && _jsx("div", {
286
+ style: {
287
+ position,
288
+ inset: 0,
289
+ zIndex: config.zIndex,
290
+ backgroundColor: 'rgba(16, 24, 40, 0.5)',
291
+ animation: live ? 'pw-subw-fade 0.2s ease-out backwards' : undefined,
292
+ pointerEvents: 'auto'
293
+ },
294
+ onClick: onDecline
295
+ }), open && _jsx("div", {
296
+ style: promptFrameStyle(config, position, live),
297
+ children: _jsx(SubscriptionPromptBox, {
298
+ config: config,
299
+ variant: subscriptionWidgetPromptVariant(permission),
300
+ colors: colors,
301
+ busy: busy,
302
+ onAccept: onAccept,
303
+ onDecline: onDecline
304
+ })
305
+ })]
306
+ });
307
+ }
@@ -0,0 +1,19 @@
1
+ import { type SubscriptionWidgetConfig, type SubscriptionWidgetCorner, type SubscriptionWidgetLayout, type SubscriptionWidgetPosition, type SubscriptionWidgetTrigger } from './types';
2
+ /** Positions each layout accepts; the first one is the layout's default. */
3
+ export declare const SUBSCRIPTION_WIDGET_POSITIONS: Record<SubscriptionWidgetLayout, SubscriptionWidgetPosition[]>;
4
+ export declare const SUBSCRIPTION_WIDGET_LAYOUTS: SubscriptionWidgetLayout[];
5
+ export declare const SUBSCRIPTION_WIDGET_CORNERS: SubscriptionWidgetCorner[];
6
+ export declare const SUBSCRIPTION_WIDGET_TRIGGERS: SubscriptionWidgetTrigger[];
7
+ /** Inclusive bounds every numeric field is clamped to. */
8
+ export declare const SUBSCRIPTION_WIDGET_LIMITS: {
9
+ readonly delay: readonly [0, 3600];
10
+ readonly cappingCount: readonly [0, 100];
11
+ readonly cappingInterval: readonly [0, 8760];
12
+ readonly borderRadius: readonly [0, 32];
13
+ readonly offset: readonly [0, 100];
14
+ readonly size: readonly [32, 80];
15
+ readonly zIndex: readonly [0, 2147483647];
16
+ };
17
+ export declare const SUBSCRIPTION_WIDGET_TEXT_MAX_LENGTH = 300;
18
+ export declare const SUBSCRIPTION_WIDGET_DEFAULT_CONFIG: SubscriptionWidgetConfig;
19
+ export declare const SUBSCRIPTION_WIDGET_KEYFRAMES = "\n@keyframes pw-subw-fade { from { opacity: 0; } }\n@keyframes pw-subw-drop { from { opacity: 0; translate: 0 -16px; } }\n@keyframes pw-subw-rise { from { opacity: 0; translate: 0 16px; } }\n";
@@ -0,0 +1,61 @@
1
+ import { WEB_POPUP_DEFAULT_COLORS, WEB_POPUP_FONT_STACK } from '../web-popups/constants';
2
+ /** Positions each layout accepts; the first one is the layout's default. */
3
+ export const SUBSCRIPTION_WIDGET_POSITIONS = {
4
+ card: ['top', 'bottom', 'top-left', 'top-right', 'bottom-left', 'bottom-right'],
5
+ bar: ['top', 'bottom'],
6
+ modal: ['center']
7
+ };
8
+ export const SUBSCRIPTION_WIDGET_LAYOUTS = Object.keys(SUBSCRIPTION_WIDGET_POSITIONS);
9
+ export const SUBSCRIPTION_WIDGET_CORNERS = ['bottom-right', 'bottom-left', 'top-right', 'top-left'];
10
+ export const SUBSCRIPTION_WIDGET_TRIGGERS = ['auto', 'launcher', 'manual'];
11
+ /** Inclusive bounds every numeric field is clamped to. */
12
+ export const SUBSCRIPTION_WIDGET_LIMITS = {
13
+ delay: [0, 3600],
14
+ cappingCount: [0, 100],
15
+ cappingInterval: [0, 8760],
16
+ borderRadius: [0, 32],
17
+ offset: [0, 100],
18
+ size: [32, 80],
19
+ zIndex: [0, 2147483647]
20
+ };
21
+ export const SUBSCRIPTION_WIDGET_TEXT_MAX_LENGTH = 300;
22
+ export const SUBSCRIPTION_WIDGET_DEFAULT_CONFIG = {
23
+ enable: false,
24
+ prompt: {
25
+ layout: 'card',
26
+ position: 'top',
27
+ borderRadius: 12,
28
+ overlay: false
29
+ },
30
+ launcher: {
31
+ enabled: false,
32
+ position: 'bottom-right',
33
+ offset: 20,
34
+ size: 48
35
+ },
36
+ display: {
37
+ trigger: 'auto',
38
+ delay: 5,
39
+ cappingCount: 3,
40
+ cappingInterval: 4
41
+ },
42
+ texts: {
43
+ title: 'Stay in the loop',
44
+ message: 'Allow notifications to get our news and special offers first.',
45
+ acceptText: 'Allow',
46
+ declineText: 'Not now',
47
+ closeText: 'Got it',
48
+ blockedTitle: 'Notifications are blocked',
49
+ blockedMessage: 'To get our notifications, allow them for this site in your browser settings.',
50
+ launcherLabel: 'Subscribe to notifications',
51
+ launcherBlockedLabel: 'Notifications are blocked'
52
+ },
53
+ colors: WEB_POPUP_DEFAULT_COLORS,
54
+ fontFamily: WEB_POPUP_FONT_STACK,
55
+ zIndex: 2147483000
56
+ };
57
+ export const SUBSCRIPTION_WIDGET_KEYFRAMES = `
58
+ @keyframes pw-subw-fade { from { opacity: 0; } }
59
+ @keyframes pw-subw-drop { from { opacity: 0; translate: 0 -16px; } }
60
+ @keyframes pw-subw-rise { from { opacity: 0; translate: 0 16px; } }
61
+ `;
@@ -0,0 +1,16 @@
1
+ import { type SubscriptionWidgetConfig, type SubscriptionWidgetHistory, type SubscriptionWidgetPermission, type SubscriptionWidgetPromptVariant } from './types';
2
+ /**
3
+ * The widget configuration from API-writable json: enums whitelisted, numbers
4
+ * clamped, strings trimmed and capped, urls sanitized, defaults for the rest.
5
+ */
6
+ export declare function normalizeSubscriptionWidgetConfig(raw: unknown): SubscriptionWidgetConfig;
7
+ /** True when the visitor can still act on the ask: not subscribed and push is supported. */
8
+ export declare function subscriptionWidgetActionable(permission: SubscriptionWidgetPermission): boolean;
9
+ /** Which prompt a visitor gets for their permission state. */
10
+ export declare function subscriptionWidgetPromptVariant(permission: SubscriptionWidgetPermission): SubscriptionWidgetPromptVariant;
11
+ /** Whether the launcher is on screen. */
12
+ export declare function subscriptionWidgetLauncherVisible(config: SubscriptionWidgetConfig, permission: SubscriptionWidgetPermission): boolean;
13
+ /** Whether the prompt may open by itself now; the host still waits `display.delay` before showing it. */
14
+ export declare function subscriptionWidgetCanAutoShow(config: SubscriptionWidgetConfig, permission: SubscriptionWidgetPermission, history: SubscriptionWidgetHistory, nowMs: number): boolean;
15
+ /** The history after one more auto show at `nowMs`. */
16
+ export declare function nextSubscriptionWidgetHistory(history: SubscriptionWidgetHistory, nowMs: number): SubscriptionWidgetHistory;
@@ -0,0 +1,110 @@
1
+ import { SUBSCRIPTION_WIDGET_CORNERS, SUBSCRIPTION_WIDGET_DEFAULT_CONFIG, SUBSCRIPTION_WIDGET_LAYOUTS, SUBSCRIPTION_WIDGET_LIMITS, SUBSCRIPTION_WIDGET_POSITIONS, SUBSCRIPTION_WIDGET_TEXT_MAX_LENGTH, SUBSCRIPTION_WIDGET_TRIGGERS } from './constants';
2
+ import { normalizeWebPopupColors, normalizeWebPopupDark, safeUrl } from '../web-popups/helpers';
3
+ const HOUR_MS = 3600 * 1000;
4
+ function record(value) {
5
+ return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {};
6
+ }
7
+ function oneOf(value, allowed, fallback) {
8
+ return allowed.includes(value) ? value : fallback;
9
+ }
10
+ function clamped(value, [min, max], fallback) {
11
+ if (typeof value !== 'number' || !Number.isFinite(value)) return fallback;
12
+ return Math.max(min, Math.min(max, Math.round(value)));
13
+ }
14
+ function text(value, fallback) {
15
+ if (typeof value !== 'string') return fallback;
16
+ const trimmed = value.trim();
17
+ return trimmed ? trimmed.slice(0, SUBSCRIPTION_WIDGET_TEXT_MAX_LENGTH) : fallback;
18
+ }
19
+ function imageUrl(value) {
20
+ const url = safeUrl(typeof value === 'string' ? value : undefined);
21
+ return url && /^(https?:\/\/|\/)/i.test(url) ? url : undefined;
22
+ }
23
+ function normalizeTexts(raw) {
24
+ const defaults = SUBSCRIPTION_WIDGET_DEFAULT_CONFIG.texts;
25
+ const keys = Object.keys(defaults);
26
+ return Object.fromEntries(keys.map(key => [key, text(raw[key], defaults[key])]));
27
+ }
28
+ /**
29
+ * The widget configuration from API-writable json: enums whitelisted, numbers
30
+ * clamped, strings trimmed and capped, urls sanitized, defaults for the rest.
31
+ */
32
+ export function normalizeSubscriptionWidgetConfig(raw) {
33
+ const defaults = SUBSCRIPTION_WIDGET_DEFAULT_CONFIG;
34
+ const config = record(raw);
35
+ const prompt = record(config.prompt);
36
+ const launcher = record(config.launcher);
37
+ const display = record(config.display);
38
+ const limits = SUBSCRIPTION_WIDGET_LIMITS;
39
+ const layout = oneOf(prompt.layout, SUBSCRIPTION_WIDGET_LAYOUTS, defaults.prompt.layout);
40
+ const positions = SUBSCRIPTION_WIDGET_POSITIONS[layout];
41
+ const trigger = oneOf(display.trigger, SUBSCRIPTION_WIDGET_TRIGGERS, defaults.display.trigger);
42
+ const iconUrl = imageUrl(prompt.iconUrl);
43
+ const colors = normalizeWebPopupColors({
44
+ colors: config.colors
45
+ });
46
+ const dark = normalizeWebPopupDark({
47
+ dark: config.dark
48
+ }, colors);
49
+ return {
50
+ enable: config.enable === true,
51
+ prompt: {
52
+ layout,
53
+ position: oneOf(prompt.position, positions, positions[0]),
54
+ ...(iconUrl ? {
55
+ iconUrl
56
+ } : {}),
57
+ borderRadius: clamped(prompt.borderRadius, limits.borderRadius, defaults.prompt.borderRadius),
58
+ overlay: layout === 'modal' || prompt.overlay === true
59
+ },
60
+ launcher: {
61
+ enabled: trigger === 'launcher' || launcher.enabled === true,
62
+ position: oneOf(launcher.position, SUBSCRIPTION_WIDGET_CORNERS, defaults.launcher.position),
63
+ offset: clamped(launcher.offset, limits.offset, defaults.launcher.offset),
64
+ size: clamped(launcher.size, limits.size, defaults.launcher.size)
65
+ },
66
+ display: {
67
+ trigger,
68
+ delay: clamped(display.delay, limits.delay, defaults.display.delay),
69
+ cappingCount: clamped(display.cappingCount, limits.cappingCount, defaults.display.cappingCount),
70
+ cappingInterval: clamped(display.cappingInterval, limits.cappingInterval, defaults.display.cappingInterval)
71
+ },
72
+ texts: normalizeTexts(record(config.texts)),
73
+ colors,
74
+ ...(dark ? {
75
+ dark
76
+ } : {}),
77
+ fontFamily: text(config.fontFamily, defaults.fontFamily),
78
+ zIndex: clamped(config.zIndex, limits.zIndex, defaults.zIndex)
79
+ };
80
+ }
81
+ /** True when the visitor can still act on the ask: not subscribed and push is supported. */
82
+ export function subscriptionWidgetActionable(permission) {
83
+ return permission === 'default' || permission === 'denied';
84
+ }
85
+ /** Which prompt a visitor gets for their permission state. */
86
+ export function subscriptionWidgetPromptVariant(permission) {
87
+ return permission === 'denied' ? 'blocked' : 'ask';
88
+ }
89
+ /** Whether the launcher is on screen. */
90
+ export function subscriptionWidgetLauncherVisible(config, permission) {
91
+ return config.enable && config.launcher.enabled && subscriptionWidgetActionable(permission);
92
+ }
93
+ /** Whether the prompt may open by itself now; the host still waits `display.delay` before showing it. */
94
+ export function subscriptionWidgetCanAutoShow(config, permission, history, nowMs) {
95
+ const {
96
+ trigger,
97
+ cappingCount,
98
+ cappingInterval
99
+ } = config.display;
100
+ if (!config.enable || trigger !== 'auto' || permission !== 'default') return false;
101
+ if (cappingCount > 0 && history.shownCount >= cappingCount) return false;
102
+ return history.lastShownAt === null || nowMs - history.lastShownAt >= cappingInterval * HOUR_MS;
103
+ }
104
+ /** The history after one more auto show at `nowMs`. */
105
+ export function nextSubscriptionWidgetHistory(history, nowMs) {
106
+ return {
107
+ shownCount: history.shownCount + 1,
108
+ lastShownAt: nowMs
109
+ };
110
+ }
@@ -0,0 +1,4 @@
1
+ export { SubscriptionWidgetReader, SubscriptionPromptBox, SubscriptionLauncherButton } from './SubscriptionWidgetReader';
2
+ export { normalizeSubscriptionWidgetConfig, subscriptionWidgetActionable, subscriptionWidgetPromptVariant, subscriptionWidgetLauncherVisible, subscriptionWidgetCanAutoShow, nextSubscriptionWidgetHistory, } from './helpers';
3
+ export { SUBSCRIPTION_WIDGET_DEFAULT_CONFIG, SUBSCRIPTION_WIDGET_POSITIONS, SUBSCRIPTION_WIDGET_LAYOUTS, SUBSCRIPTION_WIDGET_CORNERS, SUBSCRIPTION_WIDGET_TRIGGERS, SUBSCRIPTION_WIDGET_LIMITS, SUBSCRIPTION_WIDGET_TEXT_MAX_LENGTH, } from './constants';
4
+ export type { SubscriptionWidgetConfig, SubscriptionWidgetCorner, SubscriptionWidgetDisplay, SubscriptionWidgetHistory, SubscriptionWidgetLauncher, SubscriptionWidgetLayout, SubscriptionWidgetPermission, SubscriptionWidgetPosition, SubscriptionWidgetPrompt, SubscriptionWidgetPromptVariant, SubscriptionWidgetReaderMode, SubscriptionWidgetReaderProps, SubscriptionWidgetTexts, SubscriptionWidgetTrigger, } from './types';
@@ -0,0 +1,3 @@
1
+ export { SubscriptionWidgetReader, SubscriptionPromptBox, SubscriptionLauncherButton } from './SubscriptionWidgetReader';
2
+ export { normalizeSubscriptionWidgetConfig, subscriptionWidgetActionable, subscriptionWidgetPromptVariant, subscriptionWidgetLauncherVisible, subscriptionWidgetCanAutoShow, nextSubscriptionWidgetHistory } from './helpers';
3
+ export { SUBSCRIPTION_WIDGET_DEFAULT_CONFIG, SUBSCRIPTION_WIDGET_POSITIONS, SUBSCRIPTION_WIDGET_LAYOUTS, SUBSCRIPTION_WIDGET_CORNERS, SUBSCRIPTION_WIDGET_TRIGGERS, SUBSCRIPTION_WIDGET_LIMITS, SUBSCRIPTION_WIDGET_TEXT_MAX_LENGTH } from './constants';