@phoenix-iq/notifications 1.0.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 +56 -13
- package/dist/banner.d.ts +35 -0
- package/dist/banner.js +178 -0
- package/dist/bell.d.ts +38 -0
- package/dist/bell.js +369 -0
- package/dist/index.d.ts +37 -7
- package/dist/index.js +244 -24
- package/dist/store.d.ts +13 -1
- package/dist/store.js +2 -1
- package/dist/theme.d.ts +25 -0
- package/dist/theme.js +93 -0
- package/package.json +10 -5
- package/phoenix-notifications-sw.js +3 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js.map +0 -1
- package/src/index.ts +0 -331
- package/src/store.ts +0 -74
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @phoenix-iq/notifications (web SDK)
|
|
2
2
|
|
|
3
|
-
Browser push for apps on `notifications.phnx-iq.com
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
6
6
|
|
|
7
7
|
How the whole service works, the server API and the identity tokens: [docs/notifications.md](../../docs/notifications.md).
|
|
8
8
|
|
|
@@ -22,9 +22,9 @@ small plugin (HQ's dashboard `vite.config.ts` does).
|
|
|
22
22
|
```ts
|
|
23
23
|
import { Notifications } from '@phoenix-iq/notifications';
|
|
24
24
|
|
|
25
|
-
await Notifications.init({ appId: '<app id from HQ:
|
|
25
|
+
await Notifications.init({ appId: '<app id from HQ: Notification management>' });
|
|
26
26
|
|
|
27
|
-
// from a click ("Get notifications"): browsers only
|
|
27
|
+
// from a click on your own button ("Get notifications"): browsers only ask on a user gesture
|
|
28
28
|
const permission = await Notifications.requestPermission(); // 'granted' | 'denied' | 'default' | 'unsupported'
|
|
29
29
|
|
|
30
30
|
// after your own sign-in, with the identity token your backend returned
|
|
@@ -34,9 +34,44 @@ await Notifications.login(identityToken);
|
|
|
34
34
|
await Notifications.logout();
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
`init` subscribes quietly when the person already allowed notifications. `login` before the
|
|
38
|
-
and applied once it is. Nothing throws on an unsupported browser; check
|
|
39
|
-
`Notifications.permission()` to decide what to show.
|
|
37
|
+
`init` subscribes quietly when the person already allowed notifications (and did not unsubscribe here). `login` before the
|
|
38
|
+
permission is granted is kept and applied once it is. Nothing throws on an unsupported browser; check
|
|
39
|
+
`Notifications.isSupported()` and `Notifications.permission()` to decide what to show.
|
|
40
|
+
|
|
41
|
+
## The subscribe prompt
|
|
42
|
+
|
|
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.
|
|
44
|
+
|
|
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.
|
|
56
|
+
|
|
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.
|
|
61
|
+
|
|
62
|
+
## Subscription state
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const { permission, subscribed } = await Notifications.getState();
|
|
66
|
+
Notifications.onSubscriptionChange(({ permission, subscribed }) => render(permission, subscribed));
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`unsubscribe()` stops notifications here and is remembered: the browser keeps its permission, but later page loads do not
|
|
70
|
+
subscribe again until `requestPermission()` (which then subscribes without asking). Up to 1.0.0 the next `init` subscribed
|
|
71
|
+
again.
|
|
72
|
+
|
|
73
|
+
When the app has a welcome notification (HQ: Web push → Welcome notification), the service pushes it right after a browser
|
|
74
|
+
subscribes; nothing to do here.
|
|
40
75
|
|
|
41
76
|
## 3. Handle clicks
|
|
42
77
|
|
|
@@ -51,20 +86,28 @@ With no tab open, the browser opens the link directly.
|
|
|
51
86
|
|
|
52
87
|
| Option | Default | |
|
|
53
88
|
|---|---|---|
|
|
54
|
-
| `appId` | (required) | The app's id: HQ →
|
|
89
|
+
| `appId` | (required) | The app's id: HQ → Notification management → the app |
|
|
55
90
|
| `serverUrl` | `https://notifications.phnx-iq.com` | `''` uses your own origin (development behind a proxy) |
|
|
56
91
|
| `serviceWorkerPath` | `/phoenix-notifications-sw.js` | |
|
|
57
92
|
| `serviceWorkerScope` | `/` | |
|
|
58
|
-
| `language` | the browser's | Picks the Arabic or English text of a notification |
|
|
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 |
|
|
59
94
|
| `appVersion` | none | Stored with the device; in-app messages can target versions below one |
|
|
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 |
|
|
60
97
|
|
|
61
98
|
## Notes
|
|
62
99
|
|
|
63
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.
|
|
64
101
|
- The device secret the server gives the browser is kept in IndexedDB; it only lets this browser manage its own subscription.
|
|
65
|
-
- Your site must be in the app's sites (the app →
|
|
102
|
+
- Your site must be in the app's sites (the app → Web push), or registration is refused.
|
|
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.
|
|
66
107
|
|
|
67
108
|
## Publishing
|
|
68
109
|
|
|
69
|
-
`npm run build` compiles `src` to `dist` (ES modules and type declarations
|
|
70
|
-
`
|
|
110
|
+
`npm run build` compiles `src` to a fresh `dist` (ES modules and type declarations, no source maps); the package ships
|
|
111
|
+
`dist`, the service worker and this README, never `src`. `npm run lint` checks `src` and the service worker. To release,
|
|
112
|
+
raise `version` in `package.json` and run `npm publish` here (it builds first); the package is public on npmjs.com under
|
|
113
|
+
`@phoenix-iq`.
|
package/dist/banner.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SubscriptionState } from './index.js';
|
|
2
|
+
/**
|
|
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
|
+
* decides when it shows (a browser never asked, from a page view, after a delay); this only draws it and answers its buttons.
|
|
6
|
+
*
|
|
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
|
+
* Texts HQ wrote are shown as they are; the empty ones are the SDK's own, in the page's language.
|
|
9
|
+
*/
|
|
10
|
+
export type BannerPosition = 'top' | 'bottom';
|
|
11
|
+
/** The banner as HQ set it up (the web config's `banner`). */
|
|
12
|
+
export interface BannerSettings {
|
|
13
|
+
position: BannerPosition;
|
|
14
|
+
message?: string | null;
|
|
15
|
+
acceptText?: string | null;
|
|
16
|
+
cancelText?: string | null;
|
|
17
|
+
/** `#rrggbb` of the "Subscribe" button; null: the SDK's blue. */
|
|
18
|
+
color?: string | null;
|
|
19
|
+
delaySeconds: number;
|
|
20
|
+
pageViews: number;
|
|
21
|
+
repromptDays: number;
|
|
22
|
+
}
|
|
23
|
+
/** What the banner needs from the SDK. */
|
|
24
|
+
export interface BannerApi {
|
|
25
|
+
/** Asks the browser (synchronously, inside the click) and subscribes; resolves with the state afterwards. */
|
|
26
|
+
subscribe(): Promise<SubscriptionState>;
|
|
27
|
+
/** "Later": remembered, so the banner stays away for the days HQ set. */
|
|
28
|
+
dismiss(): Promise<void>;
|
|
29
|
+
onChange(handler: (state: SubscriptionState) => void): () => void;
|
|
30
|
+
}
|
|
31
|
+
export interface BannerHandle {
|
|
32
|
+
destroy(): void;
|
|
33
|
+
}
|
|
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
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { applyColors, applyTheme, colorOr, ICONS, svg, uiLanguage } from './theme.js';
|
|
2
|
+
const THANKS_MS = 2500;
|
|
3
|
+
const LEAVE_MS = 400;
|
|
4
|
+
const TEXTS = {
|
|
5
|
+
en: {
|
|
6
|
+
message: 'Get our news and updates as notifications in this browser. You can unsubscribe at any time.',
|
|
7
|
+
accept: 'Subscribe',
|
|
8
|
+
cancel: 'Later',
|
|
9
|
+
thanks: 'Thanks for subscribing!',
|
|
10
|
+
failed: "This browser couldn't be subscribed. Try again later.",
|
|
11
|
+
label: 'Notifications',
|
|
12
|
+
},
|
|
13
|
+
ar: {
|
|
14
|
+
message: 'اشترك ليصلك جديدنا وآخر أخبارنا إشعاراتٍ على هذا المتصفح، ويمكنك إلغاء الاشتراك متى شئت.',
|
|
15
|
+
accept: 'اشتراك',
|
|
16
|
+
cancel: 'لاحقًا',
|
|
17
|
+
thanks: 'شكرًا لاشتراكك!',
|
|
18
|
+
failed: 'تعذّر الاشتراك من هذا المتصفح. حاول لاحقًا.',
|
|
19
|
+
label: 'الإشعارات',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const STYLE = `
|
|
23
|
+
/* The host only spans the width and lets clicks through; the centring happens inside, where the page's CSS (a reset that
|
|
24
|
+
zeroes every element's margin, say) cannot reach. */
|
|
25
|
+
:host {
|
|
26
|
+
all: initial; position: fixed; z-index: 2147483001; left: 0; right: 0; pointer-events: none; font-family: inherit;
|
|
27
|
+
}
|
|
28
|
+
.wrap { max-width: 484px; margin: 0 auto; padding: 0 12px; }
|
|
29
|
+
:host([data-position="top"]) { top: calc(16px + env(safe-area-inset-top, 0px)); }
|
|
30
|
+
:host([data-position="bottom"]) { bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }
|
|
31
|
+
@media print { :host { display: none !important; } }
|
|
32
|
+
* { box-sizing: border-box; }
|
|
33
|
+
[hidden] { display: none !important; }
|
|
34
|
+
button { font: inherit; margin: 0; -webkit-tap-highlight-color: transparent; }
|
|
35
|
+
.card {
|
|
36
|
+
pointer-events: auto; display: flex; gap: 14px; align-items: flex-start; padding: 16px; border-radius: 16px; text-align: start;
|
|
37
|
+
background: var(--panel); color: var(--text); border: 1px solid var(--border);
|
|
38
|
+
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.32), 0 2px 6px rgba(0, 0, 0, 0.18); font-size: 14px; line-height: 1.55;
|
|
39
|
+
opacity: 0; transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.3s ease;
|
|
40
|
+
}
|
|
41
|
+
:host([data-position="top"]) .card { transform: translateY(-24px); }
|
|
42
|
+
:host([data-position="bottom"]) .card { transform: translateY(24px); }
|
|
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 */
|
|
45
|
+
:host([data-position="top"]) { top: auto; bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }
|
|
46
|
+
:host([data-position="top"]) .card { transform: translateY(24px); }
|
|
47
|
+
}
|
|
48
|
+
:host([data-shown]) .card { opacity: 1; transform: none; }
|
|
49
|
+
.icon {
|
|
50
|
+
flex: none; display: grid; place-items: center; width: 44px; height: 44px; border-radius: 12px; overflow: hidden;
|
|
51
|
+
background: var(--bell); color: var(--bell-text);
|
|
52
|
+
}
|
|
53
|
+
.icon img { width: 100%; height: 100%; object-fit: cover; }
|
|
54
|
+
.body { flex: 1; min-width: 0; }
|
|
55
|
+
.message { margin: 0; overflow-wrap: anywhere; }
|
|
56
|
+
.error { margin: 6px 0 0; color: var(--error); font-size: 13px; }
|
|
57
|
+
.actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-top: 12px; }
|
|
58
|
+
.actions button { min-height: 38px; padding: 7px 16px; border-radius: 9px; border: 1px solid transparent; font-weight: 600; cursor: pointer; }
|
|
59
|
+
.actions .later { background: transparent; color: inherit; border-color: var(--border); }
|
|
60
|
+
.actions .later:hover { background: var(--hover); }
|
|
61
|
+
.actions .accept { background: var(--bell); color: var(--bell-text); }
|
|
62
|
+
.actions .accept:disabled { opacity: 0.6; cursor: progress; }
|
|
63
|
+
.actions button:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
|
|
64
|
+
@media (prefers-reduced-motion: reduce) {
|
|
65
|
+
.card { transition: opacity 0.2s ease; transform: none !important; }
|
|
66
|
+
}
|
|
67
|
+
`;
|
|
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) {
|
|
70
|
+
const lang = uiLanguage(language);
|
|
71
|
+
const t = TEXTS[lang];
|
|
72
|
+
const host = document.createElement('phoenix-notifications-banner');
|
|
73
|
+
host.dataset.position = settings.position === 'bottom' ? 'bottom' : 'top';
|
|
74
|
+
applyColors(host, colorOr(settings.color));
|
|
75
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
76
|
+
// only constants go into this markup; HQ's texts and the icon's address are set as text and a property below
|
|
77
|
+
shadow.innerHTML = `<style>${STYLE}</style>
|
|
78
|
+
<div class="wrap"><div class="card" role="dialog" aria-label="${t.label}" aria-describedby="phx-banner-message" dir="${lang === 'ar' ? 'rtl' : 'ltr'}" lang="${lang}">
|
|
79
|
+
<div class="icon"></div>
|
|
80
|
+
<div class="body">
|
|
81
|
+
<p class="message" id="phx-banner-message" dir="auto"></p>
|
|
82
|
+
<p class="error" role="alert" hidden></p>
|
|
83
|
+
<div class="actions"><button type="button" class="later"></button><button type="button" class="accept"></button></div>
|
|
84
|
+
</div>
|
|
85
|
+
</div></div>`;
|
|
86
|
+
const $ = (selector) => shadow.querySelector(selector);
|
|
87
|
+
const icon = $('.icon');
|
|
88
|
+
const message = $('.message');
|
|
89
|
+
const errorLine = $('.error');
|
|
90
|
+
const actions = $('.actions');
|
|
91
|
+
const later = $('.later');
|
|
92
|
+
const accept = $('.accept');
|
|
93
|
+
message.textContent = settings.message?.trim() || t.message;
|
|
94
|
+
later.textContent = settings.cancelText?.trim() || t.cancel;
|
|
95
|
+
accept.textContent = settings.acceptText?.trim() || t.accept;
|
|
96
|
+
errorLine.textContent = t.failed;
|
|
97
|
+
if (iconUrl && /^https?:\/\//i.test(iconUrl)) {
|
|
98
|
+
const image = document.createElement('img');
|
|
99
|
+
image.alt = '';
|
|
100
|
+
image.src = iconUrl;
|
|
101
|
+
// a broken icon falls back to the bell
|
|
102
|
+
image.addEventListener('error', () => (icon.innerHTML = svg(ICONS.bell, 24)), { once: true });
|
|
103
|
+
icon.append(image);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
icon.innerHTML = svg(ICONS.bell, 24);
|
|
107
|
+
}
|
|
108
|
+
let busy = false;
|
|
109
|
+
let gone = false;
|
|
110
|
+
let leaveTimer = 0;
|
|
111
|
+
function leave() {
|
|
112
|
+
if (gone)
|
|
113
|
+
return;
|
|
114
|
+
gone = true;
|
|
115
|
+
stop();
|
|
116
|
+
delete host.dataset.shown;
|
|
117
|
+
leaveTimer = window.setTimeout(() => host.remove(), LEAVE_MS);
|
|
118
|
+
}
|
|
119
|
+
async function subscribe() {
|
|
120
|
+
if (busy)
|
|
121
|
+
return;
|
|
122
|
+
busy = true;
|
|
123
|
+
accept.disabled = true;
|
|
124
|
+
errorLine.hidden = true;
|
|
125
|
+
try {
|
|
126
|
+
// the browser's prompt opens inside this call, while the click still counts as the person's gesture
|
|
127
|
+
const state = await api.subscribe();
|
|
128
|
+
if (state.subscribed) {
|
|
129
|
+
message.textContent = t.thanks;
|
|
130
|
+
actions.hidden = true;
|
|
131
|
+
window.setTimeout(leave, THANKS_MS);
|
|
132
|
+
}
|
|
133
|
+
else if (state.permission === 'granted') {
|
|
134
|
+
errorLine.hidden = false;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// blocked, or the browser's question was closed without an answer: as good as "Later"
|
|
138
|
+
if (state.permission === 'default')
|
|
139
|
+
await api.dismiss();
|
|
140
|
+
leave();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
console.warn('[notifications] subscribing failed', error);
|
|
145
|
+
errorLine.hidden = false;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
busy = false;
|
|
149
|
+
accept.disabled = false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
accept.addEventListener('click', () => void subscribe());
|
|
153
|
+
later.addEventListener('click', () => {
|
|
154
|
+
void api.dismiss().catch(() => undefined);
|
|
155
|
+
leave();
|
|
156
|
+
});
|
|
157
|
+
shadow.addEventListener('keydown', ((event) => {
|
|
158
|
+
if (event.key === 'Escape' && !busy)
|
|
159
|
+
later.click();
|
|
160
|
+
}));
|
|
161
|
+
// subscribed elsewhere (the bell, the site's own button) or blocked: nothing left to ask
|
|
162
|
+
const stop = api.onChange((state) => {
|
|
163
|
+
if (!busy && (state.subscribed || state.permission === 'denied'))
|
|
164
|
+
leave();
|
|
165
|
+
});
|
|
166
|
+
document.body.appendChild(host);
|
|
167
|
+
applyTheme(host);
|
|
168
|
+
// two frames, so the card starts from its hidden place and slides
|
|
169
|
+
requestAnimationFrame(() => requestAnimationFrame(() => (host.dataset.shown = '')));
|
|
170
|
+
return {
|
|
171
|
+
destroy() {
|
|
172
|
+
gone = true;
|
|
173
|
+
stop();
|
|
174
|
+
window.clearTimeout(leaveTimer);
|
|
175
|
+
host.remove();
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
package/dist/bell.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { SubscriptionState } from './index.js';
|
|
2
|
+
/**
|
|
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.
|
|
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
|
|
9
|
+
* language (Arabic right to left, else English), the colour HQ gave it, and a light or dark panel to suit the page.
|
|
10
|
+
*/
|
|
11
|
+
export type BellPosition = 'bottom_right' | 'bottom_left';
|
|
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`). */
|
|
15
|
+
export interface BellSettings {
|
|
16
|
+
position: BellPosition;
|
|
17
|
+
/** `#rrggbb`; null: the SDK's blue. */
|
|
18
|
+
color?: string | null;
|
|
19
|
+
/** Only browsers that are not subscribed see it (after its thanks, a browser that subscribes no longer does). */
|
|
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;
|
|
25
|
+
}
|
|
26
|
+
/** What the bell needs from the SDK. */
|
|
27
|
+
export interface BellApi {
|
|
28
|
+
state(): Promise<SubscriptionState>;
|
|
29
|
+
/** Asks the browser (synchronously, inside the click) and subscribes; resolves with the state afterwards. */
|
|
30
|
+
subscribe(): Promise<SubscriptionState>;
|
|
31
|
+
unsubscribe(): Promise<void>;
|
|
32
|
+
onChange(handler: (state: SubscriptionState) => void): () => void;
|
|
33
|
+
}
|
|
34
|
+
export interface BellHandle {
|
|
35
|
+
destroy(): void;
|
|
36
|
+
}
|
|
37
|
+
/** Draws the button into the page; `destroy()` takes it away again. */
|
|
38
|
+
export declare function mountBell(api: BellApi, settings: BellSettings, language: string): BellHandle;
|