@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 +30 -5
- package/subscription-widget/SubscriptionWidgetReader.d.ts +23 -0
- package/subscription-widget/SubscriptionWidgetReader.js +307 -0
- package/subscription-widget/constants.d.ts +19 -0
- package/subscription-widget/constants.js +61 -0
- package/subscription-widget/helpers.d.ts +16 -0
- package/subscription-widget/helpers.js +110 -0
- package/subscription-widget/index.d.ts +4 -0
- package/subscription-widget/index.js +3 -0
- package/subscription-widget/types.d.ts +90 -0
- package/subscription-widget/types.js +1 -0
- package/web-popups/WebPopupReader.d.ts +4 -0
- package/web-popups/WebPopupReader.js +1394 -0
- package/web-popups/colorScheme.d.ts +2 -0
- package/web-popups/colorScheme.js +22 -0
- package/web-popups/constants.d.ts +71 -0
- package/web-popups/constants.js +216 -0
- package/web-popups/glyphs.d.ts +8 -0
- package/web-popups/glyphs.js +30 -0
- package/web-popups/helpers.d.ts +84 -0
- package/web-popups/helpers.js +687 -0
- package/web-popups/index.d.ts +9 -0
- package/web-popups/index.js +8 -0
- package/web-popups/layout.d.ts +39 -0
- package/web-popups/layout.js +290 -0
- package/web-popups/palette.d.ts +34 -0
- package/web-popups/palette.js +157 -0
- package/web-popups/templates.d.ts +3 -0
- package/web-popups/templates.js +125 -0
- package/web-popups/timer.d.ts +16 -0
- package/web-popups/timer.js +194 -0
- package/web-popups/types.d.ts +660 -0
- package/web-popups/types.js +10 -0
package/package.json
CHANGED
|
@@ -1,23 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushwoosh/websdk-common",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
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';
|