@phoenix-iq/notifications 1.1.0 → 1.2.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/README.md +28 -27
- package/dist/banner.d.ts +5 -11
- package/dist/banner.js +3 -8
- package/dist/bell.d.ts +12 -5
- package/dist/bell.js +43 -5
- package/dist/index.d.ts +9 -7
- package/dist/index.js +22 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# @phoenix-iq/notifications (web SDK)
|
|
2
2
|
|
|
3
|
-
Browser push for apps on `notifications.phnx-iq.com
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
dependencies.
|
|
3
|
+
Browser push for apps on `notifications.phnx-iq.com`. It subscribes the browser (standard Web Push with the app's VAPID key),
|
|
4
|
+
binds it to your signed-in user with an identity token your backend signs, hands notification clicks to your app, and draws
|
|
5
|
+
the subscribe prompt HQ chose for the app: a bell, the app's logo or a pop-up. TypeScript, no dependencies.
|
|
7
6
|
|
|
8
7
|
How the whole service works, the server API and the identity tokens: [docs/notifications.md](../../docs/notifications.md).
|
|
9
8
|
|
|
@@ -25,7 +24,7 @@ import { Notifications } from '@phoenix-iq/notifications';
|
|
|
25
24
|
|
|
26
25
|
await Notifications.init({ appId: '<app id from HQ: Notification management>' });
|
|
27
26
|
|
|
28
|
-
// from a click ("Get notifications"): browsers only
|
|
27
|
+
// from a click on your own button ("Get notifications"): browsers only ask on a user gesture
|
|
29
28
|
const permission = await Notifications.requestPermission(); // 'granted' | 'denied' | 'default' | 'unsupported'
|
|
30
29
|
|
|
31
30
|
// after your own sign-in, with the identity token your backend returned
|
|
@@ -39,27 +38,26 @@ await Notifications.logout();
|
|
|
39
38
|
permission is granted is kept and applied once it is. Nothing throws on an unsupported browser; check
|
|
40
39
|
`Notifications.isSupported()` and `Notifications.permission()` to decide what to show.
|
|
41
40
|
|
|
42
|
-
## The
|
|
41
|
+
## The subscribe prompt
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
corner, in the colour you chose. The first click asks the browser; then the bell shows that this browser is subscribed and
|
|
46
|
-
offers "Unsubscribe", or, when notifications were blocked, says how to allow them again. It speaks the page's language
|
|
47
|
-
(`<html lang="ar">` → Arabic, right to left; else English, or the `language` option), takes the page's font, and picks a
|
|
48
|
-
light or dark panel from the page's background. It sits in a shadow root, so your CSS and its CSS stay apart, and it is
|
|
49
|
-
hidden when printing. A change in HQ shows on the next page load. With *Hide the bell once subscribed*, only browsers that
|
|
50
|
-
are not subscribed see it.
|
|
43
|
+
HQ chooses one per app (the app → Web push → How visitors subscribe) and `init` draws it; a change shows on the next page load.
|
|
51
44
|
|
|
52
|
-
|
|
45
|
+
- **Bell** or **logo**: a round button in the bottom right or left corner, in the colour HQ set, showing a bell, or the app's
|
|
46
|
+
icon with a small bell on it (a logo that fails to load gives way to the bell). The first click asks the browser; then the
|
|
47
|
+
button shows that this browser is subscribed and offers "Unsubscribe", or, when notifications were blocked, says how to
|
|
48
|
+
allow them again. With *Hide after subscribing*, only browsers that are not subscribed see it.
|
|
49
|
+
- **Pop-up**: shown, when its time comes, to a browser never asked: a card at the top or bottom with a message, "Subscribe"
|
|
50
|
+
(the browser's own question) and "Later". HQ sets the delay after the page opens, the page view it starts from and how many
|
|
51
|
+
days "Later" keeps it away; the SDK counts these in the browser (IndexedDB). Texts left empty are the SDK's own in the
|
|
52
|
+
page's language. On a phone a top pop-up comes from the bottom, clear of your header; the strip around the card lets
|
|
53
|
+
clicks through.
|
|
54
|
+
- **None**: nothing is drawn. Ask from your own button with `requestPermission()`, and use `unsubscribe()` and the state
|
|
55
|
+
below.
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
banner comes from the bottom, clear of your header, and a bottom one sits above the bell. Like the bell it lives in a shadow
|
|
59
|
-
root; the strip around the card lets clicks through.
|
|
60
|
-
|
|
61
|
-
A site with its own "Get notifications" button passes `bell: false` and `banner: false`, and uses `requestPermission()`,
|
|
62
|
-
`unsubscribe()` and the state below.
|
|
57
|
+
They speak the page's language (`<html lang="ar">` → Arabic, right to left; else English, or the `language` option), take
|
|
58
|
+
the page's font, pick a light or dark look from the page's background, live in a shadow root (your CSS and theirs stay apart)
|
|
59
|
+
and are hidden when printing. `bell: false` and `banner: false` in `init` keep the button or the pop-up off a page even when
|
|
60
|
+
HQ chose it.
|
|
63
61
|
|
|
64
62
|
## Subscription state
|
|
65
63
|
|
|
@@ -92,17 +90,20 @@ With no tab open, the browser opens the link directly.
|
|
|
92
90
|
| `serverUrl` | `https://notifications.phnx-iq.com` | `''` uses your own origin (development behind a proxy) |
|
|
93
91
|
| `serviceWorkerPath` | `/phoenix-notifications-sw.js` | |
|
|
94
92
|
| `serviceWorkerScope` | `/` | |
|
|
95
|
-
| `language` | the page's (`<html lang>`), then the browser's | Picks the Arabic or English text of a notification, and the
|
|
93
|
+
| `language` | the page's (`<html lang>`), then the browser's | Picks the Arabic or English text of a notification, and the prompt's language |
|
|
96
94
|
| `appVersion` | none | Stored with the device; in-app messages can target versions below one |
|
|
97
|
-
| `bell` | as HQ
|
|
98
|
-
| `banner` | as HQ
|
|
95
|
+
| `bell` | as HQ chose | `false` never draws the floating button (bell or logo) |
|
|
96
|
+
| `banner` | as HQ chose | `false` never shows the pop-up |
|
|
99
97
|
|
|
100
98
|
## Notes
|
|
101
99
|
|
|
102
100
|
- iPhone and iPad: web push works only for a site added to the home screen (iOS 16.4+), as with any web push provider.
|
|
103
101
|
- The device secret the server gives the browser is kept in IndexedDB; it only lets this browser manage its own subscription.
|
|
104
102
|
- Your site must be in the app's sites (the app → Web push), or registration is refused.
|
|
105
|
-
- `example/bell.html` draws the bell and the
|
|
103
|
+
- `example/bell.html` draws the button (bell or logo) and the pop-up in every state without a server (serve `sdk/web`, open
|
|
104
|
+
`/example/bell.html`).
|
|
105
|
+
- Up to 1.1.0 the SDK read a switch on the bell and one on the banner; from 1.2.0 it follows the one prompt HQ chose and can
|
|
106
|
+
draw the logo. An older SDK still works with a newer server: it draws a bell where HQ chose the logo.
|
|
106
107
|
|
|
107
108
|
## Publishing
|
|
108
109
|
|
package/dist/banner.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { SubscriptionState } from './index.js';
|
|
2
2
|
/**
|
|
3
|
-
* The
|
|
4
|
-
*
|
|
3
|
+
* The pop-up of the banner prompt: a card that slides in at the top or the bottom of the page with a short message,
|
|
4
|
+
* "Subscribe" and "Later". "Subscribe" opens the browser's own question; "Later" keeps it away for some days. The SDK
|
|
5
5
|
* decides when it shows (a browser never asked, from a page view, after a delay); this only draws it and answers its buttons.
|
|
6
6
|
*
|
|
7
|
-
* Like the
|
|
7
|
+
* Like the floating button it lives in a shadow root, takes the page's font and language and a light or dark look to suit the page.
|
|
8
8
|
* Texts HQ wrote are shown as they are; the empty ones are the SDK's own, in the page's language.
|
|
9
9
|
*/
|
|
10
10
|
export type BannerPosition = 'top' | 'bottom';
|
|
@@ -31,11 +31,5 @@ export interface BannerApi {
|
|
|
31
31
|
export interface BannerHandle {
|
|
32
32
|
destroy(): void;
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
iconUrl?: string | null;
|
|
37
|
-
/** The bell is on the page: a bottom banner keeps clear of it on narrow screens. */
|
|
38
|
-
aboveBell?: boolean;
|
|
39
|
-
}
|
|
40
|
-
/** Slides the banner in; `destroy()` takes it away at once. */
|
|
41
|
-
export declare function mountBanner(api: BannerApi, settings: BannerSettings, language: string, placement?: BannerPlacement): BannerHandle;
|
|
34
|
+
/** Slides the banner in, with the app's icon beside the message (else a bell in its colour); `destroy()` takes it away at once. */
|
|
35
|
+
export declare function mountBanner(api: BannerApi, settings: BannerSettings, language: string, iconUrl?: string | null): BannerHandle;
|
package/dist/banner.js
CHANGED
|
@@ -41,11 +41,9 @@ button { font: inherit; margin: 0; -webkit-tap-highlight-color: transparent; }
|
|
|
41
41
|
:host([data-position="top"]) .card { transform: translateY(-24px); }
|
|
42
42
|
:host([data-position="bottom"]) .card { transform: translateY(24px); }
|
|
43
43
|
@media (max-width: 600px) {
|
|
44
|
-
/* on a phone the top of the page is the site's header and menu: the banner comes from the bottom
|
|
44
|
+
/* on a phone the top of the page is the site's header and menu: the banner comes from the bottom */
|
|
45
45
|
:host([data-position="top"]) { top: auto; bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }
|
|
46
46
|
:host([data-position="top"]) .card { transform: translateY(24px); }
|
|
47
|
-
/* and where it would cover the bell in its corner, it sits above it */
|
|
48
|
-
:host([data-above-bell]) { bottom: calc(84px + env(safe-area-inset-bottom, 0px)); }
|
|
49
47
|
}
|
|
50
48
|
:host([data-shown]) .card { opacity: 1; transform: none; }
|
|
51
49
|
.icon {
|
|
@@ -67,15 +65,12 @@ button { font: inherit; margin: 0; -webkit-tap-highlight-color: transparent; }
|
|
|
67
65
|
.card { transition: opacity 0.2s ease; transform: none !important; }
|
|
68
66
|
}
|
|
69
67
|
`;
|
|
70
|
-
/** Slides the banner in; `destroy()` takes it away at once. */
|
|
71
|
-
export function mountBanner(api, settings, language,
|
|
68
|
+
/** Slides the banner in, with the app's icon beside the message (else a bell in its colour); `destroy()` takes it away at once. */
|
|
69
|
+
export function mountBanner(api, settings, language, iconUrl) {
|
|
72
70
|
const lang = uiLanguage(language);
|
|
73
71
|
const t = TEXTS[lang];
|
|
74
|
-
const { iconUrl, aboveBell } = placement;
|
|
75
72
|
const host = document.createElement('phoenix-notifications-banner');
|
|
76
73
|
host.dataset.position = settings.position === 'bottom' ? 'bottom' : 'top';
|
|
77
|
-
if (aboveBell)
|
|
78
|
-
host.dataset.aboveBell = '';
|
|
79
74
|
applyColors(host, colorOr(settings.color));
|
|
80
75
|
const shadow = host.attachShadow({ mode: 'open' });
|
|
81
76
|
// only constants go into this markup; HQ's texts and the icon's address are set as text and a property below
|
package/dist/bell.d.ts
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import type { SubscriptionState } from './index.js';
|
|
2
2
|
/**
|
|
3
|
-
* The
|
|
4
|
-
* browser on a click, shows that it is subscribed and lets the person unsubscribe, or explains how to allow notifications
|
|
5
|
-
* again once they blocked them.
|
|
3
|
+
* The floating subscribe button of the bell and logo prompts: a round button in a bottom corner of the site that subscribes
|
|
4
|
+
* this browser on a click, shows that it is subscribed and lets the person unsubscribe, or explains how to allow notifications
|
|
5
|
+
* again once they blocked them. It shows a bell, or the app's logo with a small bell on it. HQ chooses the prompt per app;
|
|
6
|
+
* the SDK draws it from the web config.
|
|
6
7
|
*
|
|
7
8
|
* It lives in a shadow root, so the site's CSS cannot reach it and its CSS cannot leak out. It takes the page's font and
|
|
8
9
|
* language (Arabic right to left, else English), the colour HQ gave it, and a light or dark panel to suit the page.
|
|
9
10
|
*/
|
|
10
11
|
export type BellPosition = 'bottom_right' | 'bottom_left';
|
|
11
|
-
/**
|
|
12
|
+
/** What the button shows: a bell, or the app's logo (its icon). */
|
|
13
|
+
export type BellFace = 'bell' | 'logo';
|
|
14
|
+
/** The button as HQ set it up (the web config's `bell`). */
|
|
12
15
|
export interface BellSettings {
|
|
13
16
|
position: BellPosition;
|
|
14
17
|
/** `#rrggbb`; null: the SDK's blue. */
|
|
15
18
|
color?: string | null;
|
|
16
19
|
/** Only browsers that are not subscribed see it (after its thanks, a browser that subscribes no longer does). */
|
|
17
20
|
hideWhenSubscribed?: boolean;
|
|
21
|
+
/** `bell` unless the logo prompt is chosen. */
|
|
22
|
+
face?: BellFace;
|
|
23
|
+
/** The logo's address (the app's icon); without one, or when it fails to load, the button shows the bell. */
|
|
24
|
+
logoUrl?: string | null;
|
|
18
25
|
}
|
|
19
26
|
/** What the bell needs from the SDK. */
|
|
20
27
|
export interface BellApi {
|
|
@@ -27,5 +34,5 @@ export interface BellApi {
|
|
|
27
34
|
export interface BellHandle {
|
|
28
35
|
destroy(): void;
|
|
29
36
|
}
|
|
30
|
-
/** Draws the
|
|
37
|
+
/** Draws the button into the page; `destroy()` takes it away again. */
|
|
31
38
|
export declare function mountBell(api: BellApi, settings: BellSettings, language: string): BellHandle;
|
package/dist/bell.js
CHANGED
|
@@ -66,6 +66,13 @@ button { font: inherit; margin: 0; -webkit-tap-highlight-color: transparent; }
|
|
|
66
66
|
position: absolute; top: -3px; right: -3px; display: grid; place-items: center; width: 18px; height: 18px; border-radius: 50%;
|
|
67
67
|
background: #16a34a; color: #ffffff; border: 2px solid #ffffff;
|
|
68
68
|
}
|
|
69
|
+
/* the logo face: the app's icon on a white disc ringed in the colour; its badge says what the button is for */
|
|
70
|
+
:host([data-face="logo"]) .bell { background: #ffffff; box-shadow: 0 0 0 2px var(--bell), 0 6px 20px rgba(0, 0, 0, 0.28), 0 1px 3px rgba(0, 0, 0, 0.22); }
|
|
71
|
+
:host([data-face="logo"]) .icon { width: 100%; height: 100%; border-radius: 50%; overflow: hidden; }
|
|
72
|
+
.logo { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
73
|
+
:host([data-face="logo"][data-view="blocked"]) .logo { filter: grayscale(1); }
|
|
74
|
+
.badge[data-kind="bell"] { background: var(--bell); color: var(--bell-text); }
|
|
75
|
+
.badge[data-kind="blocked"] { background: #6b7280; }
|
|
69
76
|
.bubble {
|
|
70
77
|
position: absolute; bottom: 7px; width: max-content; max-width: min(260px, calc(100vw - 104px)); padding: 7px 11px; border-radius: 8px;
|
|
71
78
|
background: var(--tip); color: var(--tip-text); font-size: 13px; line-height: 1.45; text-align: start;
|
|
@@ -105,10 +112,11 @@ button { font: inherit; margin: 0; -webkit-tap-highlight-color: transparent; }
|
|
|
105
112
|
:host([data-attention][data-view="unsubscribed"]) .bell::after { animation: none; }
|
|
106
113
|
}
|
|
107
114
|
`;
|
|
108
|
-
/** Draws the
|
|
115
|
+
/** Draws the button into the page; `destroy()` takes it away again. */
|
|
109
116
|
export function mountBell(api, settings, language) {
|
|
110
117
|
const lang = uiLanguage(language);
|
|
111
118
|
const t = TEXTS[lang];
|
|
119
|
+
const logoUrl = settings.face === 'logo' && /^https?:\/\//i.test(settings.logoUrl ?? '') ? settings.logoUrl : null;
|
|
112
120
|
const host = document.createElement('phoenix-notifications-bell');
|
|
113
121
|
host.dataset.side = settings.position === 'bottom_left' ? 'left' : 'right';
|
|
114
122
|
host.dataset.view = 'unsubscribed';
|
|
@@ -146,18 +154,48 @@ export function mountBell(api, settings, language) {
|
|
|
146
154
|
let iconFor = null;
|
|
147
155
|
/** Hidden until the first read of the state, so it never shows a wrong state (or shows at all, when it hides) first. */
|
|
148
156
|
let settled = false;
|
|
157
|
+
/** The logo while it loads and after; a logo that fails to load gives way to the bell. */
|
|
158
|
+
let face = logoUrl ? 'logo' : 'bell';
|
|
159
|
+
let logo = null;
|
|
149
160
|
const viewOf = (state) => (state.permission === 'denied' ? 'blocked' : state.subscribed ? 'subscribed' : 'unsubscribed');
|
|
161
|
+
function drawFace() {
|
|
162
|
+
if (face === 'bell') {
|
|
163
|
+
icon.innerHTML = svg(view === 'blocked' ? ICONS.bellOff : ICONS.bell, 24);
|
|
164
|
+
badge.innerHTML = svg(ICONS.check, 11, 3);
|
|
165
|
+
delete badge.dataset.kind;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!logo) {
|
|
169
|
+
logo = document.createElement('img');
|
|
170
|
+
logo.className = 'logo';
|
|
171
|
+
logo.alt = '';
|
|
172
|
+
logo.src = logoUrl;
|
|
173
|
+
logo.addEventListener('error', () => {
|
|
174
|
+
face = 'bell';
|
|
175
|
+
iconFor = null;
|
|
176
|
+
render();
|
|
177
|
+
}, { once: true });
|
|
178
|
+
}
|
|
179
|
+
icon.replaceChildren(logo);
|
|
180
|
+
// the logo alone does not say "notifications": a bell on it does, a check once subscribed, a struck bell once blocked
|
|
181
|
+
badge.innerHTML = view === 'subscribed' ? svg(ICONS.check, 11, 3) : svg(view === 'blocked' ? ICONS.bellOff : ICONS.bell, 11, 2.4);
|
|
182
|
+
if (view === 'subscribed')
|
|
183
|
+
delete badge.dataset.kind;
|
|
184
|
+
else
|
|
185
|
+
badge.dataset.kind = view === 'blocked' ? 'blocked' : 'bell';
|
|
186
|
+
}
|
|
150
187
|
function render() {
|
|
151
188
|
host.dataset.view = view;
|
|
189
|
+
host.dataset.face = face;
|
|
152
190
|
const hidden = !settled || (!!settings.hideWhenSubscribed && view === 'subscribed' && !open && !busy && !message);
|
|
153
191
|
host.style.display = hidden ? 'none' : '';
|
|
154
192
|
bell.setAttribute('aria-label', t[view]);
|
|
155
193
|
bell.setAttribute('aria-expanded', String(open));
|
|
156
|
-
if (iconFor !== view) {
|
|
157
|
-
|
|
158
|
-
iconFor = view
|
|
194
|
+
if (iconFor !== `${face}:${view}`) {
|
|
195
|
+
drawFace();
|
|
196
|
+
iconFor = `${face}:${view}`;
|
|
159
197
|
}
|
|
160
|
-
badge.hidden = view !== 'subscribed';
|
|
198
|
+
badge.hidden = face === 'bell' && view !== 'subscribed';
|
|
161
199
|
panel.hidden = !open;
|
|
162
200
|
if (!open)
|
|
163
201
|
return;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type StoredDevice } from './store.js';
|
|
2
2
|
/**
|
|
3
3
|
* Phoenix Notifications web SDK: subscribes the browser to web push for an app on notifications.phnx-iq.com, binds it to
|
|
4
|
-
* the signed-in user with an identity token from the app's own backend, and hands notification clicks to the app.
|
|
5
|
-
*
|
|
4
|
+
* the signed-in user with an identity token from the app's own backend, and hands notification clicks to the app. It also
|
|
5
|
+
* draws the prompt HQ chose for the app (a bell, the app's logo or a pop-up), so a site needs no button of its own.
|
|
6
6
|
*
|
|
7
7
|
* Nothing here throws for an unsupported browser or a denied permission: check `isSupported()` / `permission()`.
|
|
8
8
|
* The service worker file (phoenix-notifications-sw.js) must be served from the site, at `serviceWorkerPath`.
|
|
@@ -16,17 +16,19 @@ export interface InitOptions {
|
|
|
16
16
|
serverUrl?: string;
|
|
17
17
|
serviceWorkerPath?: string;
|
|
18
18
|
serviceWorkerScope?: string;
|
|
19
|
-
/** Language the device reports (picks Arabic or English text, and the
|
|
19
|
+
/** Language the device reports (picks Arabic or English text, and the prompt's language); defaults to the page's, then the browser's. */
|
|
20
20
|
language?: string;
|
|
21
21
|
appVersion?: string;
|
|
22
22
|
/**
|
|
23
|
-
* The
|
|
24
|
-
* never draws it
|
|
23
|
+
* The floating button (a bell or the app's logo), drawn when HQ chose one of those as the app's prompt (the app → Web push
|
|
24
|
+
* → How visitors subscribe). `false` never draws it. A site with its own button usually has "None" chosen in HQ instead.
|
|
25
25
|
*/
|
|
26
26
|
bell?: boolean;
|
|
27
|
-
/** The
|
|
27
|
+
/** The pop-up, shown when HQ chose it as the app's prompt. `false` never shows it. */
|
|
28
28
|
banner?: boolean;
|
|
29
29
|
}
|
|
30
|
+
/** How the app's sites offer notifications, as HQ chose: one prompt, or none. */
|
|
31
|
+
export type Prompt = 'none' | 'bell' | 'logo' | 'banner';
|
|
30
32
|
export type PushPermission = 'granted' | 'denied' | 'default' | 'unsupported';
|
|
31
33
|
/** Whether this browser gets the app's notifications, for a site's own button. */
|
|
32
34
|
export interface SubscriptionState {
|
|
@@ -47,7 +49,7 @@ export declare const Notifications: {
|
|
|
47
49
|
/** True when this browser can receive web push here (a secure context with service workers and the Push API). */
|
|
48
50
|
isSupported(): boolean;
|
|
49
51
|
/**
|
|
50
|
-
* Registers the service worker, and draws the
|
|
52
|
+
* Registers the service worker, and draws the prompt HQ chose for the app (a bell, the logo or a pop-up). When the person
|
|
51
53
|
* already allowed notifications (and did not unsubscribe here), subscribes quietly.
|
|
52
54
|
*/
|
|
53
55
|
init(init: InitOptions): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -3,14 +3,21 @@ import { mountBell } from './bell.js';
|
|
|
3
3
|
import { keys, store } from './store.js';
|
|
4
4
|
/**
|
|
5
5
|
* Phoenix Notifications web SDK: subscribes the browser to web push for an app on notifications.phnx-iq.com, binds it to
|
|
6
|
-
* the signed-in user with an identity token from the app's own backend, and hands notification clicks to the app.
|
|
7
|
-
*
|
|
6
|
+
* the signed-in user with an identity token from the app's own backend, and hands notification clicks to the app. It also
|
|
7
|
+
* draws the prompt HQ chose for the app (a bell, the app's logo or a pop-up), so a site needs no button of its own.
|
|
8
8
|
*
|
|
9
9
|
* Nothing here throws for an unsupported browser or a denied permission: check `isSupported()` / `permission()`.
|
|
10
10
|
* The service worker file (phoenix-notifications-sw.js) must be served from the site, at `serviceWorkerPath`.
|
|
11
11
|
*/
|
|
12
12
|
export const DEFAULT_SERVER_URL = 'https://notifications.phnx-iq.com';
|
|
13
13
|
export const DEFAULT_SERVICE_WORKER_PATH = '/phoenix-notifications-sw.js';
|
|
14
|
+
function promptOf(web) {
|
|
15
|
+
if (!web)
|
|
16
|
+
return 'none';
|
|
17
|
+
if (web.prompt)
|
|
18
|
+
return web.prompt;
|
|
19
|
+
return web.bell?.enabled ? 'bell' : web.banner?.enabled ? 'banner' : 'none';
|
|
20
|
+
}
|
|
14
21
|
export class NotificationsError extends Error {
|
|
15
22
|
status;
|
|
16
23
|
constructor(status, message) {
|
|
@@ -276,11 +283,12 @@ function watchPermission() {
|
|
|
276
283
|
.then((status) => status.addEventListener('change', changed))
|
|
277
284
|
.catch(() => undefined);
|
|
278
285
|
}
|
|
279
|
-
/** Draws the bell
|
|
286
|
+
/** Draws the floating button (a bell, or the app's logo) when it is the app's prompt. A failure only means no button. */
|
|
280
287
|
async function showBell(appId) {
|
|
281
288
|
try {
|
|
282
289
|
const web = await webConfig();
|
|
283
|
-
|
|
290
|
+
const prompt = promptOf(web);
|
|
291
|
+
if (bell?.appId !== appId || !web?.bell || (prompt !== 'bell' && prompt !== 'logo'))
|
|
284
292
|
return;
|
|
285
293
|
if (document.readyState === 'loading')
|
|
286
294
|
await new Promise((resolve) => document.addEventListener('DOMContentLoaded', resolve, { once: true }));
|
|
@@ -294,10 +302,10 @@ async function showBell(appId) {
|
|
|
294
302
|
},
|
|
295
303
|
unsubscribe: () => Notifications.unsubscribe(),
|
|
296
304
|
onChange: (handler) => Notifications.onSubscriptionChange(handler),
|
|
297
|
-
}, web.bell, language());
|
|
305
|
+
}, { ...web.bell, face: prompt === 'logo' ? 'logo' : 'bell', logoUrl: web.iconUrl }, language());
|
|
298
306
|
}
|
|
299
307
|
catch (error) {
|
|
300
|
-
console.warn('[notifications] the
|
|
308
|
+
console.warn('[notifications] the subscribe button is not available', error);
|
|
301
309
|
}
|
|
302
310
|
}
|
|
303
311
|
function whenDocumentReady() {
|
|
@@ -306,15 +314,15 @@ function whenDocumentReady() {
|
|
|
306
314
|
return new Promise((resolve) => document.addEventListener('DOMContentLoaded', () => resolve(), { once: true }));
|
|
307
315
|
}
|
|
308
316
|
/**
|
|
309
|
-
* Slides the
|
|
310
|
-
* unsubscribe here), from the page view HQ set, not within the days after a "Later", and after the delay.
|
|
311
|
-
* view. A failure only means no
|
|
317
|
+
* Slides the pop-up in when it is the app's prompt and its time has come: only for a browser never asked (the permission still
|
|
318
|
+
* to be given, and no unsubscribe here), from the page view HQ set, not within the days after a "Later", and after the delay.
|
|
319
|
+
* Each call is one page view. A failure only means no pop-up.
|
|
312
320
|
*/
|
|
313
321
|
async function showBanner(appId) {
|
|
314
322
|
try {
|
|
315
323
|
const web = await webConfig();
|
|
316
324
|
const settings = web?.banner;
|
|
317
|
-
if (banner?.appId !== appId || !settings
|
|
325
|
+
if (banner?.appId !== appId || !settings || promptOf(web) !== 'banner')
|
|
318
326
|
return;
|
|
319
327
|
if (Notification.permission !== 'default' || (await optedOut()))
|
|
320
328
|
return;
|
|
@@ -328,7 +336,7 @@ async function showBanner(appId) {
|
|
|
328
336
|
return;
|
|
329
337
|
await new Promise((resolve) => setTimeout(resolve, Math.max(0, settings.delaySeconds) * 1000));
|
|
330
338
|
await whenDocumentReady();
|
|
331
|
-
// the page may have moved on meanwhile: another app, the person subscribed with the
|
|
339
|
+
// the page may have moved on meanwhile: another app, the person subscribed with the site's own button, or answered the browser
|
|
332
340
|
if (banner?.appId !== appId || banner.handle || Notification.permission !== 'default' || (await optedOut()))
|
|
333
341
|
return;
|
|
334
342
|
banner.handle = mountBanner({
|
|
@@ -341,10 +349,10 @@ async function showBanner(appId) {
|
|
|
341
349
|
await store.set(key, { ...latest, dismissedAt: Date.now() });
|
|
342
350
|
},
|
|
343
351
|
onChange: (handler) => Notifications.onSubscriptionChange(handler),
|
|
344
|
-
}, settings, language(),
|
|
352
|
+
}, settings, language(), web?.iconUrl);
|
|
345
353
|
}
|
|
346
354
|
catch (error) {
|
|
347
|
-
console.warn('[notifications] the
|
|
355
|
+
console.warn('[notifications] the subscribe pop-up is not available', error);
|
|
348
356
|
}
|
|
349
357
|
}
|
|
350
358
|
export const Notifications = {
|
|
@@ -353,7 +361,7 @@ export const Notifications = {
|
|
|
353
361
|
return supported();
|
|
354
362
|
},
|
|
355
363
|
/**
|
|
356
|
-
* Registers the service worker, and draws the
|
|
364
|
+
* Registers the service worker, and draws the prompt HQ chose for the app (a bell, the logo or a pop-up). When the person
|
|
357
365
|
* already allowed notifications (and did not unsubscribe here), subscribes quietly.
|
|
358
366
|
*/
|
|
359
367
|
async init(init) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phoenix-iq/notifications",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Phoenix Notifications web SDK: browser push subscriptions, the
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Phoenix Notifications web SDK: browser push subscriptions, the subscribe prompt (a bell, the app's logo or a pop-up), verified sign-in and click handling for apps on notifications.phnx-iq.com.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"main": "./dist/index.js",
|