@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.
- package/inbox/InboxCellPreview.d.ts +4 -0
- package/inbox/InboxCellPreview.js +497 -0
- package/inbox/InboxWidgetReader.d.ts +27 -0
- package/inbox/InboxWidgetReader.js +228 -0
- package/inbox/constants.d.ts +15 -0
- package/inbox/constants.js +546 -0
- package/inbox/helpers.d.ts +30 -0
- package/inbox/helpers.js +150 -0
- package/inbox/index.d.ts +6 -0
- package/inbox/index.js +4 -0
- package/inbox/types.d.ts +126 -0
- package/inbox/types.js +1 -0
- package/package.json +6 -2
- package/subscription-widget/SubscriptionWidgetReader.d.ts +3 -3
- package/subscription-widget/SubscriptionWidgetReader.js +62 -34
- package/subscription-widget/constants.d.ts +2 -1
- package/subscription-widget/constants.js +5 -2
- package/subscription-widget/helpers.d.ts +3 -1
- package/subscription-widget/helpers.js +27 -6
- package/subscription-widget/index.d.ts +3 -3
- package/subscription-widget/index.js +2 -2
- package/subscription-widget/types.d.ts +16 -3
package/inbox/helpers.js
ADDED
|
@@ -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
|
+
}
|
package/inbox/index.d.ts
ADDED
|
@@ -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';
|
package/inbox/types.d.ts
ADDED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
268
|
-
|
|
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
|
-
}),
|
|
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
|
-
}),
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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';
|