@pushwoosh/websdk-common 6.17.1 → 6.19.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.
@@ -0,0 +1,150 @@
1
+ import { INBOX_KNOWN_LAYOUTS, INBOX_MAX_CAROUSEL_SLIDES, INBOX_MONTHS, INBOX_WIDGET_DEFAULT_APPEARANCE, INBOX_WIDGET_STYLE_PREFIX, INBOX_WIDGET_STYLE_VARIABLES } from './constants';
2
+ const MINUTE = 60 * 1000;
3
+ const HOUR = 60 * MINUTE;
4
+ const DAY = 24 * HOUR;
5
+ const RULE_BREAKERS = /[;{}]/;
6
+ /** Picks the content to render: explicit `content` wins, otherwise the `language` entry of `localizedContent` with default/en/first fallbacks. */
7
+ export function pickInboxLocalizedContent(content, localizedContent, language) {
8
+ if (content) {
9
+ return content;
10
+ }
11
+ if (!localizedContent) {
12
+ return null;
13
+ }
14
+ const keys = Object.keys(localizedContent);
15
+ if (keys.length === 0) {
16
+ return null;
17
+ }
18
+ if (language && localizedContent[language]) {
19
+ return localizedContent[language];
20
+ }
21
+ return localizedContent.default || localizedContent.en || localizedContent[keys[0]];
22
+ }
23
+ /** Slides a cell draws: the ones with an image, at most {@link INBOX_MAX_CAROUSEL_SLIDES}. */
24
+ export function getInboxValidSlides(slides) {
25
+ return (Array.isArray(slides) ? slides : []).filter(slide => !!slide && !!slide.imageUrl).slice(0, INBOX_MAX_CAROUSEL_SLIDES);
26
+ }
27
+ /** Hero image of the banner/captioned layouts: the banner, with the icon standing in when none was sent. */
28
+ export function getInboxHeroUrl(content) {
29
+ return content.bannerUrl || content.iconUrl || '';
30
+ }
31
+ /** Resolves the layout a cell draws: the requested one when it can render, the heuristic for unknown ones, classic otherwise. */
32
+ export function resolveInboxLayout(content) {
33
+ const hasTitle = !!content.title;
34
+ const hasBody = !!content.body;
35
+ const hero = getInboxHeroUrl(content);
36
+ let requested = content.layoutType;
37
+ if (!INBOX_KNOWN_LAYOUTS.includes(requested)) {
38
+ if (content.bannerUrl && !hasTitle) {
39
+ requested = 'banner';
40
+ } else if (content.bannerUrl && hasTitle && hasBody) {
41
+ requested = 'captioned';
42
+ } else {
43
+ requested = 'classic';
44
+ }
45
+ }
46
+ switch (requested) {
47
+ case 'banner':
48
+ return hero ? 'banner' : 'classic';
49
+ case 'captioned':
50
+ return hero && hasTitle && hasBody ? 'captioned' : 'classic';
51
+ case 'carousel':
52
+ return hasTitle && hasBody && getInboxValidSlides(content.carousel).length > 0 ? 'carousel' : 'classic';
53
+ default:
54
+ return 'classic';
55
+ }
56
+ }
57
+ /** True when the content has nothing to render yet, so a preview shows the layout placeholder. */
58
+ export function isInboxContentEmpty(content) {
59
+ return !content.title && !content.body && !content.iconUrl && !content.bannerUrl && getInboxValidSlides(content.carousel).length === 0;
60
+ }
61
+ /** The layout whose placeholder an empty preview shows: the requested one when known, else classic. */
62
+ export function getInboxEmptyLayout(content) {
63
+ const requested = content.layoutType;
64
+ return INBOX_KNOWN_LAYOUTS.includes(requested) ? requested : 'classic';
65
+ }
66
+ /** Title/body pair of the mobile cells: in classic the body stands in for a missing title, as the mobile SDKs draw it. */
67
+ export function getInboxMobileTexts(content, layout) {
68
+ if (layout === 'classic') {
69
+ return {
70
+ title: content.title || content.body || '',
71
+ body: content.title && content.body || ''
72
+ };
73
+ }
74
+ return {
75
+ title: content.title || '',
76
+ body: content.body || ''
77
+ };
78
+ }
79
+ /** The colored initial a classic cell shows when there is no icon. */
80
+ export function getInboxIconInitial(content) {
81
+ return (content.title || content.body || '?').trim().charAt(0).toUpperCase() || '?';
82
+ }
83
+ /** Date label of a web widget message: relative within a day of `now`, then `D Mon YYYY, H:mm` in local time. */
84
+ export function formatInboxMessageTime(sendDate, now = Date.now()) {
85
+ const date = new Date(sendDate);
86
+ const gap = now - date.getTime();
87
+ if (gap <= MINUTE) {
88
+ return 'Just now';
89
+ }
90
+ if (gap < HOUR) {
91
+ return `${Math.floor(gap / MINUTE)} minutes ago`;
92
+ }
93
+ if (gap < DAY) {
94
+ return `${Math.floor(gap / HOUR)} hours ago`;
95
+ }
96
+ const minutes = `0${date.getMinutes()}`.slice(-2);
97
+ return `${date.getDate()} ${INBOX_MONTHS[date.getMonth()]} ${date.getFullYear()}, ${date.getHours()}:${minutes}`;
98
+ }
99
+ /** Newest first. */
100
+ export function compareInboxBySendDate(one, two) {
101
+ return new Date(two).getTime() - new Date(one).getTime();
102
+ }
103
+ /** Full appearance from a partial one; the arrow border follows a visible widget border unless set explicitly. */
104
+ export function resolveInboxWidgetAppearance(appearance) {
105
+ const {
106
+ borderColor
107
+ } = appearance || {};
108
+ return {
109
+ ...INBOX_WIDGET_DEFAULT_APPEARANCE,
110
+ arrowBorderColor: borderColor && borderColor !== 'transparent' ? borderColor : INBOX_WIDGET_DEFAULT_APPEARANCE.arrowBorderColor,
111
+ ...appearance
112
+ };
113
+ }
114
+ /** One appearance value as its CSS custom property value; `null` leaves it unset so the stylesheet default applies. */
115
+ export function formatInboxStyleValue(type, value) {
116
+ switch (type) {
117
+ case 'size':
118
+ {
119
+ if (value === null || value === undefined || value === '') {
120
+ return null;
121
+ }
122
+ const size = Number(value);
123
+ return Number.isFinite(size) ? `${size}px` : null;
124
+ }
125
+ case 'number':
126
+ {
127
+ const parsed = parseFloat(String(value));
128
+ return Number.isFinite(parsed) ? String(parsed) : null;
129
+ }
130
+ default:
131
+ {
132
+ const text = String(value ?? '').trim();
133
+ return text && !RULE_BREAKERS.test(text) ? text : null;
134
+ }
135
+ }
136
+ }
137
+ /** The `--pw-inbox-*` custom properties of an appearance, ready for a `style` object or `setProperty`. */
138
+ export function inboxWidgetStyleVariables(appearance) {
139
+ const variables = {};
140
+ INBOX_WIDGET_STYLE_VARIABLES.forEach(({
141
+ name,
142
+ type
143
+ }) => {
144
+ const formatted = formatInboxStyleValue(type, appearance[name]);
145
+ if (formatted !== null) {
146
+ variables[`--${INBOX_WIDGET_STYLE_PREFIX}-${name}`] = formatted;
147
+ }
148
+ });
149
+ return variables;
150
+ }
@@ -0,0 +1,6 @@
1
+ export { InboxWidgetReader, InboxWebCell, inboxWidgetPreviewFrame } from './InboxWidgetReader';
2
+ export { InboxCellPreview } from './InboxCellPreview';
3
+ export { pickInboxLocalizedContent, getInboxValidSlides, getInboxHeroUrl, resolveInboxLayout, isInboxContentEmpty, getInboxEmptyLayout, getInboxMobileTexts, getInboxIconInitial, formatInboxMessageTime, compareInboxBySendDate, resolveInboxWidgetAppearance, formatInboxStyleValue, inboxWidgetStyleVariables, } from './helpers';
4
+ export { INBOX_MAX_CAROUSEL_SLIDES, INBOX_KNOWN_LAYOUTS, INBOX_WIDGET_STYLE_PREFIX, INBOX_MONTHS, INBOX_WIDGET_STYLE_VARIABLES, INBOX_WIDGET_DEFAULT_APPEARANCE, INBOX_WIDGET_CSS, } from './constants';
5
+ export type { InboxWebCellProps } from './InboxWidgetReader';
6
+ export type { InboxLayout, InboxSlide, InboxCellContent, InboxWidgetMessage, InboxWidgetPosition, InboxWidgetAppearance, InboxWidgetStyleName, InboxWidgetStyleType, InboxWidgetStyleVariable, InboxWidgetReaderMode, InboxWidgetReaderProps, InboxCellPlatform, InboxCellPreviewProps, } from './types';
package/inbox/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { InboxWidgetReader, InboxWebCell, inboxWidgetPreviewFrame } from './InboxWidgetReader';
2
+ export { InboxCellPreview } from './InboxCellPreview';
3
+ export { pickInboxLocalizedContent, getInboxValidSlides, getInboxHeroUrl, resolveInboxLayout, isInboxContentEmpty, getInboxEmptyLayout, getInboxMobileTexts, getInboxIconInitial, formatInboxMessageTime, compareInboxBySendDate, resolveInboxWidgetAppearance, formatInboxStyleValue, inboxWidgetStyleVariables } from './helpers';
4
+ export { INBOX_MAX_CAROUSEL_SLIDES, INBOX_KNOWN_LAYOUTS, INBOX_WIDGET_STYLE_PREFIX, INBOX_MONTHS, INBOX_WIDGET_STYLE_VARIABLES, INBOX_WIDGET_DEFAULT_APPEARANCE, INBOX_WIDGET_CSS } from './constants';
@@ -0,0 +1,126 @@
1
+ import { type ReactNode } from 'react';
2
+ /** Cell layout of an inbox message. */
3
+ export type InboxLayout = 'classic' | 'captioned' | 'banner' | 'carousel';
4
+ /** One carousel slide of an inbox message. */
5
+ export interface InboxSlide {
6
+ /** Slide image URL; a slide without it is dropped. */
7
+ imageUrl?: string;
8
+ /** Caption overlaid at the bottom of the slide. */
9
+ caption?: string;
10
+ /** Where a tap on the slide goes; the host validates it before navigating. */
11
+ link?: string;
12
+ }
13
+ /** Content of one inbox cell, in the shape both the Control Panel and the WebSDK map their data to. */
14
+ export interface InboxCellContent {
15
+ /** Cell title. */
16
+ title?: string;
17
+ /** Cell text. */
18
+ body?: string;
19
+ /** Small avatar icon URL; without it classic renders a colored initial. */
20
+ iconUrl?: string;
21
+ /** The campaign's own banner for the banner/captioned layouts. */
22
+ bannerUrl?: string;
23
+ /** Requested layout; unknown or absent values fall back to the heuristic of {@link resolveInboxLayout}. */
24
+ layoutType?: string;
25
+ /** Carousel slides; only the first valid ones are rendered. */
26
+ carousel?: InboxSlide[];
27
+ }
28
+ /** One message of the web inbox widget. */
29
+ export interface InboxWidgetMessage extends InboxCellContent {
30
+ /** Message code, rendered as `data-pw-inbox-message-id`. */
31
+ code: string;
32
+ /** Send date as an ISO string (UTC). */
33
+ sendDate: string;
34
+ /** The message was seen in the widget. */
35
+ isRead: boolean;
36
+ /** The message action was performed. */
37
+ isActionPerformed: boolean;
38
+ }
39
+ /** Position of the widget relative to its trigger. */
40
+ export type InboxWidgetPosition = 'left' | 'right' | 'top' | 'bottom';
41
+ /** Appearance of the web inbox widget; every color and size is also a `--pw-inbox-<name>` CSS custom property. */
42
+ export interface InboxWidgetAppearance {
43
+ title: string;
44
+ bgColor: string;
45
+ textColor: string;
46
+ fontFamily: string;
47
+ borderRadius: number;
48
+ borderColor: string;
49
+ badgeBgColor: string;
50
+ badgeTextColor: string;
51
+ widgetWidth: number;
52
+ zIndex: number;
53
+ messageTitleColor: string;
54
+ timeTextColor: string;
55
+ emptyInboxTitle: string;
56
+ emptyInboxText: string;
57
+ emptyInboxIconUrl: string;
58
+ emptyInboxTitleColor: string;
59
+ emptyInboxTextColor: string;
60
+ arrowBorderColor: string;
61
+ /** Keep the pre-3.75 look of the classic cell; the other layouts are unaffected. */
62
+ legacyClassicCell: boolean;
63
+ unreadDotColor: string;
64
+ avatarBgColor: string;
65
+ avatarTextColor: string;
66
+ avatarSize: number;
67
+ mediaRadius: number;
68
+ slideCaptionColor: string;
69
+ slideScrimColor: string;
70
+ pagerDotActiveColor: string;
71
+ pagerDotColor: string;
72
+ }
73
+ /** Appearance keys exposed as `--pw-inbox-<name>` CSS custom properties. */
74
+ export type InboxWidgetStyleName = Exclude<keyof InboxWidgetAppearance, 'title' | 'emptyInboxTitle' | 'emptyInboxText' | 'emptyInboxIconUrl' | 'legacyClassicCell'>;
75
+ /** How an appearance value is formatted into its CSS custom property. */
76
+ export type InboxWidgetStyleType = 'number' | 'color' | 'string' | 'size';
77
+ /** One CSS custom property of the widget. */
78
+ export interface InboxWidgetStyleVariable {
79
+ name: InboxWidgetStyleName;
80
+ type: InboxWidgetStyleType;
81
+ }
82
+ /** `live` renders into the host's `.pw-inbox-widget` element; `preview` draws the widget box itself, inline. */
83
+ export type InboxWidgetReaderMode = 'live' | 'preview';
84
+ /** Props of {@link InboxWidgetReader}. */
85
+ export interface InboxWidgetReaderProps {
86
+ mode: InboxWidgetReaderMode;
87
+ /** Messages in display order. */
88
+ messages: InboxWidgetMessage[];
89
+ /** Appearance; missing keys take {@link INBOX_WIDGET_DEFAULT_APPEARANCE}. */
90
+ appearance?: Partial<InboxWidgetAppearance>;
91
+ /** Reference time for the relative dates, ms since epoch (default: now). */
92
+ now?: number;
93
+ /** A tap on a message outside its remove icon, pager and slide link. */
94
+ onMessageClick?: (code: string) => void;
95
+ /** A tap on the remove icon of a message. */
96
+ onMessageRemove?: (code: string) => void;
97
+ /** A tap on a slide that carries a link. */
98
+ onSlideLinkClick?: (code: string, link: string) => void;
99
+ }
100
+ /** Platform skin of {@link InboxCellPreview}. */
101
+ export type InboxCellPlatform = 'ios' | 'android' | 'web';
102
+ /** Props of {@link InboxCellPreview}. */
103
+ export interface InboxCellPreviewProps {
104
+ /** Platform skin (default `ios`). */
105
+ platform?: InboxCellPlatform;
106
+ /** Content to draw; empty content draws the placeholder of the requested layout. */
107
+ content?: InboxCellContent | null;
108
+ /** Date label of the mobile cells (default `12:12`); the web cell derives it from `sendDate`. */
109
+ dateLabel?: string;
110
+ /** Send date of the web cell (default: now). */
111
+ sendDate?: string;
112
+ /** Draw the unread dot (default `true`). */
113
+ unread?: boolean;
114
+ /** Show the loading placeholder. */
115
+ isLoading?: boolean;
116
+ /** Title of the empty placeholder. */
117
+ emptyTitle?: ReactNode;
118
+ /** Description of the empty placeholder. */
119
+ emptyDescription?: ReactNode;
120
+ /** Renders the title/body of the mobile cells (e.g. a rich-text viewer); plain text by default. */
121
+ renderText?: (text: string) => ReactNode;
122
+ /** Appearance of the web cell. */
123
+ appearance?: Partial<InboxWidgetAppearance>;
124
+ /** CSS width of the preview (default `100%`). */
125
+ width?: string | number;
126
+ }
package/inbox/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pushwoosh/websdk-common",
3
- "version": "6.17.1",
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)",
3
+ "version": "6.19.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; inbox: web inbox widget and inbox cell previews)",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
7
7
  "types": "index.d.ts",
@@ -18,6 +18,10 @@
18
18
  "types": "./subscription-widget/index.d.ts",
19
19
  "default": "./subscription-widget/index.js"
20
20
  },
21
+ "./inbox": {
22
+ "types": "./inbox/index.d.ts",
23
+ "default": "./inbox/index.js"
24
+ },
21
25
  "./package.json": "./package.json"
22
26
  },
23
27
  "sideEffects": false,
@@ -9,7 +9,7 @@ export declare function SubscriptionPromptBox({ config, variant, colors, busy, o
9
9
  onAccept?: () => void;
10
10
  onDecline?: () => void;
11
11
  }): import("react").JSX.Element;
12
- /** The launcher bell without placement. */
12
+ /** The launcher without placement: the authored picture, or the round bell button. */
13
13
  export declare function SubscriptionLauncherButton({ config, blocked, colors, onClick }: {
14
14
  config: SubscriptionWidgetConfig;
15
15
  blocked: boolean;
@@ -17,7 +17,7 @@ export declare function SubscriptionLauncherButton({ config, blocked, colors, on
17
17
  onClick?: () => void;
18
18
  }): import("react").JSX.Element;
19
19
  /**
20
- * The launcher, or while `open` the prompt in its place; 'live' pins it to the viewport, 'preview' to the
21
- * nearest positioned parent with no enter animation. The host owns `open` and every side effect.
20
+ * The prompt while `open` and the launcher, each only when switched on; 'live' pins them to the viewport,
21
+ * 'preview' to the nearest positioned parent with no enter animation. The host owns `open` and every side effect.
22
22
  */
23
23
  export declare function SubscriptionWidgetReader({ config, permission, open, mode, colorScheme, busy, onAccept, onDecline, onLauncherClick, }: SubscriptionWidgetReaderProps): import("react").JSX.Element | null;
@@ -195,7 +195,7 @@ export function SubscriptionPromptBox({
195
195
  })]
196
196
  });
197
197
  }
198
- /** The launcher bell without placement. */
198
+ /** The launcher without placement: the authored picture, or the round bell button. */
199
199
  export function SubscriptionLauncherButton({
200
200
  config,
201
201
  blocked,
@@ -203,29 +203,56 @@ export function SubscriptionLauncherButton({
203
203
  onClick
204
204
  }) {
205
205
  const {
206
- size
206
+ width,
207
+ height,
208
+ imageUrl
207
209
  } = config.launcher;
208
210
  const label = blocked ? config.texts.launcherBlockedLabel : config.texts.launcherLabel;
209
- const iconSize = Math.round(size / 2);
211
+ const iconSize = Math.round(Math.min(width, height) / 2);
212
+ const base = {
213
+ display: 'flex',
214
+ alignItems: 'center',
215
+ justifyContent: 'center',
216
+ width,
217
+ height,
218
+ padding: 0,
219
+ border: 'none',
220
+ cursor: 'pointer',
221
+ opacity: blocked ? 0.7 : 1
222
+ };
223
+ if (imageUrl) {
224
+ return _jsx("button", {
225
+ type: "button",
226
+ "aria-label": label,
227
+ title: label,
228
+ onClick: onClick,
229
+ style: {
230
+ ...base,
231
+ background: 'none'
232
+ },
233
+ children: _jsx("img", {
234
+ src: imageUrl,
235
+ alt: "",
236
+ width: width,
237
+ height: height,
238
+ style: {
239
+ display: 'block',
240
+ objectFit: 'contain'
241
+ }
242
+ })
243
+ });
244
+ }
210
245
  return _jsx("button", {
211
246
  type: "button",
212
247
  "aria-label": label,
213
248
  title: label,
214
249
  onClick: onClick,
215
250
  style: {
216
- display: 'flex',
217
- alignItems: 'center',
218
- justifyContent: 'center',
219
- width: size,
220
- height: size,
221
- padding: 0,
222
- border: 'none',
251
+ ...base,
223
252
  borderRadius: '50%',
224
- backgroundColor: colors.accent,
225
- color: colors.onAccent,
226
- boxShadow: BOX_SHADOW,
227
- cursor: 'pointer',
228
- opacity: blocked ? 0.7 : 1
253
+ backgroundColor: config.launcher.background ?? colors.accent,
254
+ color: config.launcher.iconColor ?? colors.onAccent,
255
+ boxShadow: BOX_SHADOW
229
256
  },
230
257
  children: _jsxs("svg", {
231
258
  width: iconSize,
@@ -246,8 +273,8 @@ export function SubscriptionLauncherButton({
246
273
  });
247
274
  }
248
275
  /**
249
- * The launcher, or while `open` the prompt in its place; 'live' pins it to the viewport, 'preview' to the
250
- * nearest positioned parent with no enter animation. The host owns `open` and every side effect.
276
+ * The prompt while `open` and the launcher, each only when switched on; 'live' pins them to the viewport,
277
+ * 'preview' to the nearest positioned parent with no enter animation. The host owns `open` and every side effect.
251
278
  */
252
279
  export function SubscriptionWidgetReader({
253
280
  config,
@@ -264,25 +291,13 @@ export function SubscriptionWidgetReader({
264
291
  const colors = resolveWebPopupColors(config, scheme);
265
292
  const live = mode === 'live';
266
293
  const position = live ? 'fixed' : 'absolute';
267
- const launcher = !open && subscriptionWidgetLauncherVisible(config, permission);
268
- if (!config.enable || !open && !launcher) return null;
294
+ const prompt = open && config.prompt.enabled;
295
+ const launcher = subscriptionWidgetLauncherVisible(config, permission);
296
+ if (!prompt && !launcher) return null;
269
297
  return _jsxs(_Fragment, {
270
298
  children: [live && _jsx("style", {
271
299
  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", {
300
+ }), prompt && config.prompt.overlay && _jsx("div", {
286
301
  style: {
287
302
  position,
288
303
  inset: 0,
@@ -292,7 +307,7 @@ export function SubscriptionWidgetReader({
292
307
  pointerEvents: 'auto'
293
308
  },
294
309
  onClick: onDecline
295
- }), open && _jsx("div", {
310
+ }), prompt && _jsx("div", {
296
311
  style: promptFrameStyle(config, position, live),
297
312
  children: _jsx(SubscriptionPromptBox, {
298
313
  config: config,
@@ -302,6 +317,19 @@ export function SubscriptionWidgetReader({
302
317
  onAccept: onAccept,
303
318
  onDecline: onDecline
304
319
  })
320
+ }), launcher && _jsx("div", {
321
+ style: {
322
+ position,
323
+ zIndex: config.zIndex,
324
+ pointerEvents: 'auto',
325
+ ...cornerStyle(config.launcher.position, config.launcher.offset)
326
+ },
327
+ children: _jsx(SubscriptionLauncherButton, {
328
+ config: config,
329
+ blocked: permission === 'denied',
330
+ colors: colors,
331
+ onClick: onLauncherClick
332
+ })
305
333
  })]
306
334
  });
307
335
  }
@@ -11,9 +11,10 @@ export declare const SUBSCRIPTION_WIDGET_LIMITS: {
11
11
  readonly cappingInterval: readonly [0, 8760];
12
12
  readonly borderRadius: readonly [0, 32];
13
13
  readonly offset: readonly [0, 100];
14
- readonly size: readonly [32, 80];
14
+ readonly launcherSize: readonly [16, 200];
15
15
  readonly zIndex: readonly [0, 2147483647];
16
16
  };
17
17
  export declare const SUBSCRIPTION_WIDGET_TEXT_MAX_LENGTH = 300;
18
+ export declare const SUBSCRIPTION_WIDGET_COLOR_MAX_LENGTH = 64;
18
19
  export declare const SUBSCRIPTION_WIDGET_DEFAULT_CONFIG: SubscriptionWidgetConfig;
19
20
  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";
@@ -15,13 +15,15 @@ export const SUBSCRIPTION_WIDGET_LIMITS = {
15
15
  cappingInterval: [0, 8760],
16
16
  borderRadius: [0, 32],
17
17
  offset: [0, 100],
18
- size: [32, 80],
18
+ launcherSize: [16, 200],
19
19
  zIndex: [0, 2147483647]
20
20
  };
21
21
  export const SUBSCRIPTION_WIDGET_TEXT_MAX_LENGTH = 300;
22
+ export const SUBSCRIPTION_WIDGET_COLOR_MAX_LENGTH = 64;
22
23
  export const SUBSCRIPTION_WIDGET_DEFAULT_CONFIG = {
23
24
  enable: false,
24
25
  prompt: {
26
+ enabled: false,
25
27
  layout: 'card',
26
28
  position: 'top',
27
29
  borderRadius: 12,
@@ -31,7 +33,8 @@ export const SUBSCRIPTION_WIDGET_DEFAULT_CONFIG = {
31
33
  enabled: false,
32
34
  position: 'bottom-right',
33
35
  offset: 20,
34
- size: 48
36
+ width: 48,
37
+ height: 48
35
38
  },
36
39
  display: {
37
40
  trigger: 'auto',
@@ -1,4 +1,4 @@
1
- import { type SubscriptionWidgetConfig, type SubscriptionWidgetHistory, type SubscriptionWidgetPermission, type SubscriptionWidgetPromptVariant } from './types';
1
+ import { type SubscriptionWidgetConfig, type SubscriptionWidgetHistory, type SubscriptionWidgetLauncherAction, type SubscriptionWidgetPermission, type SubscriptionWidgetPromptVariant } from './types';
2
2
  /**
3
3
  * The widget configuration from API-writable json: enums whitelisted, numbers
4
4
  * clamped, strings trimmed and capped, urls sanitized, defaults for the rest.
@@ -10,6 +10,8 @@ export declare function subscriptionWidgetActionable(permission: SubscriptionWid
10
10
  export declare function subscriptionWidgetPromptVariant(permission: SubscriptionWidgetPermission): SubscriptionWidgetPromptVariant;
11
11
  /** Whether the launcher is on screen. */
12
12
  export declare function subscriptionWidgetLauncherVisible(config: SubscriptionWidgetConfig, permission: SubscriptionWidgetPermission): boolean;
13
+ /** What a launcher click does for this visitor. */
14
+ export declare function subscriptionWidgetLauncherAction(config: SubscriptionWidgetConfig, permission: SubscriptionWidgetPermission): SubscriptionWidgetLauncherAction;
13
15
  /** Whether the prompt may open by itself now; the host still waits `display.delay` before showing it. */
14
16
  export declare function subscriptionWidgetCanAutoShow(config: SubscriptionWidgetConfig, permission: SubscriptionWidgetPermission, history: SubscriptionWidgetHistory, nowMs: number): boolean;
15
17
  /** The history after one more auto show at `nowMs`. */
@@ -1,4 +1,4 @@
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';
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, SUBSCRIPTION_WIDGET_COLOR_MAX_LENGTH } from './constants';
2
2
  import { normalizeWebPopupColors, normalizeWebPopupDark, safeUrl } from '../web-popups/helpers';
3
3
  const HOUR_MS = 3600 * 1000;
4
4
  function record(value) {
@@ -20,6 +20,12 @@ function imageUrl(value) {
20
20
  const url = safeUrl(typeof value === 'string' ? value : undefined);
21
21
  return url && /^(https?:\/\/|\/)/i.test(url) ? url : undefined;
22
22
  }
23
+ function optionalColor(key, value) {
24
+ const color = typeof value === 'string' ? value.trim().slice(0, SUBSCRIPTION_WIDGET_COLOR_MAX_LENGTH) : '';
25
+ return color ? {
26
+ [key]: color
27
+ } : {};
28
+ }
23
29
  function normalizeTexts(raw) {
24
30
  const defaults = SUBSCRIPTION_WIDGET_DEFAULT_CONFIG.texts;
25
31
  const keys = Object.keys(defaults);
@@ -40,15 +46,19 @@ export function normalizeSubscriptionWidgetConfig(raw) {
40
46
  const positions = SUBSCRIPTION_WIDGET_POSITIONS[layout];
41
47
  const trigger = oneOf(display.trigger, SUBSCRIPTION_WIDGET_TRIGGERS, defaults.display.trigger);
42
48
  const iconUrl = imageUrl(prompt.iconUrl);
49
+ const launcherImageUrl = imageUrl(launcher.imageUrl);
43
50
  const colors = normalizeWebPopupColors({
44
51
  colors: config.colors
45
52
  });
46
53
  const dark = normalizeWebPopupDark({
47
54
  dark: config.dark
48
55
  }, colors);
56
+ const promptEnabled = prompt.enabled === true;
57
+ const launcherEnabled = launcher.enabled === true;
49
58
  return {
50
- enable: config.enable === true,
59
+ enable: promptEnabled || launcherEnabled,
51
60
  prompt: {
61
+ enabled: promptEnabled,
52
62
  layout,
53
63
  position: oneOf(prompt.position, positions, positions[0]),
54
64
  ...(iconUrl ? {
@@ -58,10 +68,16 @@ export function normalizeSubscriptionWidgetConfig(raw) {
58
68
  overlay: layout === 'modal' || prompt.overlay === true
59
69
  },
60
70
  launcher: {
61
- enabled: trigger === 'launcher' || launcher.enabled === true,
71
+ enabled: launcherEnabled,
62
72
  position: oneOf(launcher.position, SUBSCRIPTION_WIDGET_CORNERS, defaults.launcher.position),
63
73
  offset: clamped(launcher.offset, limits.offset, defaults.launcher.offset),
64
- size: clamped(launcher.size, limits.size, defaults.launcher.size)
74
+ width: clamped(launcher.width, limits.launcherSize, defaults.launcher.width),
75
+ height: clamped(launcher.height, limits.launcherSize, defaults.launcher.height),
76
+ ...(launcherImageUrl ? {
77
+ imageUrl: launcherImageUrl
78
+ } : {}),
79
+ ...optionalColor('background', launcher.background),
80
+ ...optionalColor('iconColor', launcher.iconColor)
65
81
  },
66
82
  display: {
67
83
  trigger,
@@ -88,7 +104,12 @@ export function subscriptionWidgetPromptVariant(permission) {
88
104
  }
89
105
  /** Whether the launcher is on screen. */
90
106
  export function subscriptionWidgetLauncherVisible(config, permission) {
91
- return config.enable && config.launcher.enabled && subscriptionWidgetActionable(permission);
107
+ return config.launcher.enabled && subscriptionWidgetActionable(permission);
108
+ }
109
+ /** What a launcher click does for this visitor. */
110
+ export function subscriptionWidgetLauncherAction(config, permission) {
111
+ if (config.prompt.enabled) return 'prompt';
112
+ return permission === 'default' ? 'subscribe' : 'none';
92
113
  }
93
114
  /** Whether the prompt may open by itself now; the host still waits `display.delay` before showing it. */
94
115
  export function subscriptionWidgetCanAutoShow(config, permission, history, nowMs) {
@@ -97,7 +118,7 @@ export function subscriptionWidgetCanAutoShow(config, permission, history, nowMs
97
118
  cappingCount,
98
119
  cappingInterval
99
120
  } = config.display;
100
- if (!config.enable || trigger !== 'auto' || permission !== 'default') return false;
121
+ if (!config.prompt.enabled || trigger !== 'auto' || permission !== 'default') return false;
101
122
  if (cappingCount > 0 && history.shownCount >= cappingCount) return false;
102
123
  return history.lastShownAt === null || nowMs - history.lastShownAt >= cappingInterval * HOUR_MS;
103
124
  }
@@ -1,4 +1,4 @@
1
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';
2
+ export { normalizeSubscriptionWidgetConfig, subscriptionWidgetActionable, subscriptionWidgetPromptVariant, subscriptionWidgetLauncherVisible, subscriptionWidgetLauncherAction, 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, SUBSCRIPTION_WIDGET_COLOR_MAX_LENGTH, } from './constants';
4
+ export type { SubscriptionWidgetConfig, SubscriptionWidgetCorner, SubscriptionWidgetDisplay, SubscriptionWidgetHistory, SubscriptionWidgetLauncher, SubscriptionWidgetLauncherAction, SubscriptionWidgetLayout, SubscriptionWidgetPermission, SubscriptionWidgetPosition, SubscriptionWidgetPrompt, SubscriptionWidgetPromptVariant, SubscriptionWidgetReaderMode, SubscriptionWidgetReaderProps, SubscriptionWidgetTexts, SubscriptionWidgetTrigger, } from './types';
@@ -1,3 +1,3 @@
1
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';
2
+ export { normalizeSubscriptionWidgetConfig, subscriptionWidgetActionable, subscriptionWidgetPromptVariant, subscriptionWidgetLauncherVisible, subscriptionWidgetLauncherAction, 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, SUBSCRIPTION_WIDGET_COLOR_MAX_LENGTH } from './constants';