@phoenix-iq/notifications 0.0.0-stage → 1.1.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 +111 -2
- package/dist/banner.d.ts +41 -0
- package/dist/banner.js +183 -0
- package/dist/bell.d.ts +31 -0
- package/dist/bell.js +331 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +501 -0
- package/dist/store.d.ts +49 -0
- package/dist/store.js +48 -0
- package/dist/theme.d.ts +25 -0
- package/dist/theme.js +93 -0
- package/package.json +42 -4
- package/phoenix-notifications-sw.js +149 -0
package/README.md
CHANGED
|
@@ -1,3 +1,112 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @phoenix-iq/notifications (web SDK)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Browser push for apps on `notifications.phnx-iq.com`, our replacement for OneSignal's web SDK. It subscribes the browser
|
|
4
|
+
(standard Web Push with the app's VAPID key), binds it to your signed-in user with an identity token your backend signs,
|
|
5
|
+
hands notification clicks to your app, and shows the subscription bell and banner when the app has them on. TypeScript, no
|
|
6
|
+
dependencies.
|
|
7
|
+
|
|
8
|
+
How the whole service works, the server API and the identity tokens: [docs/notifications.md](../../docs/notifications.md).
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @phoenix-iq/notifications
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 1. Serve the service worker
|
|
15
|
+
|
|
16
|
+
Serve `phoenix-notifications-sw.js` (in this package) at your site's root, so it is reachable at
|
|
17
|
+
`https://your-site/phoenix-notifications-sw.js`. It must be on your own site: a service worker only controls its own origin.
|
|
18
|
+
Copy it from `node_modules/@phoenix-iq/notifications/phoenix-notifications-sw.js` in your build, or with Vite emit it from a
|
|
19
|
+
small plugin (HQ's dashboard `vite.config.ts` does).
|
|
20
|
+
|
|
21
|
+
## 2. Initialise and log in
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { Notifications } from '@phoenix-iq/notifications';
|
|
25
|
+
|
|
26
|
+
await Notifications.init({ appId: '<app id from HQ: Notification management>' });
|
|
27
|
+
|
|
28
|
+
// from a click ("Get notifications"): browsers only show the prompt on a user gesture
|
|
29
|
+
const permission = await Notifications.requestPermission(); // 'granted' | 'denied' | 'default' | 'unsupported'
|
|
30
|
+
|
|
31
|
+
// after your own sign-in, with the identity token your backend returned
|
|
32
|
+
await Notifications.login(identityToken);
|
|
33
|
+
|
|
34
|
+
// on sign-out: the browser keeps your broadcasts as a guest, but not that user's notifications
|
|
35
|
+
await Notifications.logout();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`init` subscribes quietly when the person already allowed notifications (and did not unsubscribe here). `login` before the
|
|
39
|
+
permission is granted is kept and applied once it is. Nothing throws on an unsupported browser; check
|
|
40
|
+
`Notifications.isSupported()` and `Notifications.permission()` to decide what to show.
|
|
41
|
+
|
|
42
|
+
## The subscription bell
|
|
43
|
+
|
|
44
|
+
Switch it on in HQ (the app → Web push → Subscription bell) and `init` draws it: a round bell in the bottom right or left
|
|
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.
|
|
51
|
+
|
|
52
|
+
## The subscription banner
|
|
53
|
+
|
|
54
|
+
OneSignal's slide prompt. Switch it on in HQ (the app → Web push → Subscription banner) and `init` shows it, when its time
|
|
55
|
+
comes, to a browser never asked: a card at the top or bottom with a message, "Subscribe" (the browser's own question) and
|
|
56
|
+
"Later". HQ sets the delay after the page opens, the page view it starts from and how many days "Later" keeps it away; the
|
|
57
|
+
SDK counts these in the browser (IndexedDB). Texts left empty are the SDK's own in the page's language. On a phone a top
|
|
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.
|
|
63
|
+
|
|
64
|
+
## Subscription state
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const { permission, subscribed } = await Notifications.getState();
|
|
68
|
+
Notifications.onSubscriptionChange(({ permission, subscribed }) => render(permission, subscribed));
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`unsubscribe()` stops notifications here and is remembered: the browser keeps its permission, but later page loads do not
|
|
72
|
+
subscribe again until `requestPermission()` (which then subscribes without asking). Up to 1.0.0 the next `init` subscribed
|
|
73
|
+
again.
|
|
74
|
+
|
|
75
|
+
When the app has a welcome notification (HQ: Web push → Welcome notification), the service pushes it right after a browser
|
|
76
|
+
subscribes; nothing to do here.
|
|
77
|
+
|
|
78
|
+
## 3. Handle clicks
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
Notifications.onClick(({ url, data }) => router.navigate(new URL(url ?? '/', location.origin).pathname));
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A click on a notification focuses an open tab of your site and calls your handler; without a handler the SDK opens the link.
|
|
85
|
+
With no tab open, the browser opens the link directly.
|
|
86
|
+
|
|
87
|
+
## Options
|
|
88
|
+
|
|
89
|
+
| Option | Default | |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `appId` | (required) | The app's id: HQ → Notification management → the app |
|
|
92
|
+
| `serverUrl` | `https://notifications.phnx-iq.com` | `''` uses your own origin (development behind a proxy) |
|
|
93
|
+
| `serviceWorkerPath` | `/phoenix-notifications-sw.js` | |
|
|
94
|
+
| `serviceWorkerScope` | `/` | |
|
|
95
|
+
| `language` | the page's (`<html lang>`), then the browser's | Picks the Arabic or English text of a notification, and the bell's language |
|
|
96
|
+
| `appVersion` | none | Stored with the device; in-app messages can target versions below one |
|
|
97
|
+
| `bell` | as HQ set it | `false` never draws the subscription bell (a site with its own button) |
|
|
98
|
+
| `banner` | as HQ set it | `false` never shows the subscription banner |
|
|
99
|
+
|
|
100
|
+
## Notes
|
|
101
|
+
|
|
102
|
+
- 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
|
+
- The device secret the server gives the browser is kept in IndexedDB; it only lets this browser manage its own subscription.
|
|
104
|
+
- 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 banner in every state without a server (serve `sdk/web`, open `/example/bell.html`).
|
|
106
|
+
|
|
107
|
+
## Publishing
|
|
108
|
+
|
|
109
|
+
`npm run build` compiles `src` to a fresh `dist` (ES modules and type declarations, no source maps); the package ships
|
|
110
|
+
`dist`, the service worker and this README, never `src`. `npm run lint` checks `src` and the service worker. To release,
|
|
111
|
+
raise `version` in `package.json` and run `npm publish` here (it builds first); the package is public on npmjs.com under
|
|
112
|
+
`@phoenix-iq`.
|
package/dist/banner.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { SubscriptionState } from './index.js';
|
|
2
|
+
/**
|
|
3
|
+
* The subscription banner (OneSignal's slide prompt): a card that slides in at the top or the bottom of the page with a short
|
|
4
|
+
* message, "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 bell 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
|
+
export interface BannerPlacement {
|
|
35
|
+
/** The app's icon, shown beside the message (else a bell in the banner's colour). */
|
|
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;
|
package/dist/banner.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
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, as OneSignal's does */
|
|
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
|
+
/* 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
|
+
}
|
|
50
|
+
:host([data-shown]) .card { opacity: 1; transform: none; }
|
|
51
|
+
.icon {
|
|
52
|
+
flex: none; display: grid; place-items: center; width: 44px; height: 44px; border-radius: 12px; overflow: hidden;
|
|
53
|
+
background: var(--bell); color: var(--bell-text);
|
|
54
|
+
}
|
|
55
|
+
.icon img { width: 100%; height: 100%; object-fit: cover; }
|
|
56
|
+
.body { flex: 1; min-width: 0; }
|
|
57
|
+
.message { margin: 0; overflow-wrap: anywhere; }
|
|
58
|
+
.error { margin: 6px 0 0; color: var(--error); font-size: 13px; }
|
|
59
|
+
.actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-top: 12px; }
|
|
60
|
+
.actions button { min-height: 38px; padding: 7px 16px; border-radius: 9px; border: 1px solid transparent; font-weight: 600; cursor: pointer; }
|
|
61
|
+
.actions .later { background: transparent; color: inherit; border-color: var(--border); }
|
|
62
|
+
.actions .later:hover { background: var(--hover); }
|
|
63
|
+
.actions .accept { background: var(--bell); color: var(--bell-text); }
|
|
64
|
+
.actions .accept:disabled { opacity: 0.6; cursor: progress; }
|
|
65
|
+
.actions button:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
|
|
66
|
+
@media (prefers-reduced-motion: reduce) {
|
|
67
|
+
.card { transition: opacity 0.2s ease; transform: none !important; }
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
/** Slides the banner in; `destroy()` takes it away at once. */
|
|
71
|
+
export function mountBanner(api, settings, language, placement = {}) {
|
|
72
|
+
const lang = uiLanguage(language);
|
|
73
|
+
const t = TEXTS[lang];
|
|
74
|
+
const { iconUrl, aboveBell } = placement;
|
|
75
|
+
const host = document.createElement('phoenix-notifications-banner');
|
|
76
|
+
host.dataset.position = settings.position === 'bottom' ? 'bottom' : 'top';
|
|
77
|
+
if (aboveBell)
|
|
78
|
+
host.dataset.aboveBell = '';
|
|
79
|
+
applyColors(host, colorOr(settings.color));
|
|
80
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
81
|
+
// only constants go into this markup; HQ's texts and the icon's address are set as text and a property below
|
|
82
|
+
shadow.innerHTML = `<style>${STYLE}</style>
|
|
83
|
+
<div class="wrap"><div class="card" role="dialog" aria-label="${t.label}" aria-describedby="phx-banner-message" dir="${lang === 'ar' ? 'rtl' : 'ltr'}" lang="${lang}">
|
|
84
|
+
<div class="icon"></div>
|
|
85
|
+
<div class="body">
|
|
86
|
+
<p class="message" id="phx-banner-message" dir="auto"></p>
|
|
87
|
+
<p class="error" role="alert" hidden></p>
|
|
88
|
+
<div class="actions"><button type="button" class="later"></button><button type="button" class="accept"></button></div>
|
|
89
|
+
</div>
|
|
90
|
+
</div></div>`;
|
|
91
|
+
const $ = (selector) => shadow.querySelector(selector);
|
|
92
|
+
const icon = $('.icon');
|
|
93
|
+
const message = $('.message');
|
|
94
|
+
const errorLine = $('.error');
|
|
95
|
+
const actions = $('.actions');
|
|
96
|
+
const later = $('.later');
|
|
97
|
+
const accept = $('.accept');
|
|
98
|
+
message.textContent = settings.message?.trim() || t.message;
|
|
99
|
+
later.textContent = settings.cancelText?.trim() || t.cancel;
|
|
100
|
+
accept.textContent = settings.acceptText?.trim() || t.accept;
|
|
101
|
+
errorLine.textContent = t.failed;
|
|
102
|
+
if (iconUrl && /^https?:\/\//i.test(iconUrl)) {
|
|
103
|
+
const image = document.createElement('img');
|
|
104
|
+
image.alt = '';
|
|
105
|
+
image.src = iconUrl;
|
|
106
|
+
// a broken icon falls back to the bell
|
|
107
|
+
image.addEventListener('error', () => (icon.innerHTML = svg(ICONS.bell, 24)), { once: true });
|
|
108
|
+
icon.append(image);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
icon.innerHTML = svg(ICONS.bell, 24);
|
|
112
|
+
}
|
|
113
|
+
let busy = false;
|
|
114
|
+
let gone = false;
|
|
115
|
+
let leaveTimer = 0;
|
|
116
|
+
function leave() {
|
|
117
|
+
if (gone)
|
|
118
|
+
return;
|
|
119
|
+
gone = true;
|
|
120
|
+
stop();
|
|
121
|
+
delete host.dataset.shown;
|
|
122
|
+
leaveTimer = window.setTimeout(() => host.remove(), LEAVE_MS);
|
|
123
|
+
}
|
|
124
|
+
async function subscribe() {
|
|
125
|
+
if (busy)
|
|
126
|
+
return;
|
|
127
|
+
busy = true;
|
|
128
|
+
accept.disabled = true;
|
|
129
|
+
errorLine.hidden = true;
|
|
130
|
+
try {
|
|
131
|
+
// the browser's prompt opens inside this call, while the click still counts as the person's gesture
|
|
132
|
+
const state = await api.subscribe();
|
|
133
|
+
if (state.subscribed) {
|
|
134
|
+
message.textContent = t.thanks;
|
|
135
|
+
actions.hidden = true;
|
|
136
|
+
window.setTimeout(leave, THANKS_MS);
|
|
137
|
+
}
|
|
138
|
+
else if (state.permission === 'granted') {
|
|
139
|
+
errorLine.hidden = false;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// blocked, or the browser's question was closed without an answer: as good as "Later"
|
|
143
|
+
if (state.permission === 'default')
|
|
144
|
+
await api.dismiss();
|
|
145
|
+
leave();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
console.warn('[notifications] subscribing failed', error);
|
|
150
|
+
errorLine.hidden = false;
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
busy = false;
|
|
154
|
+
accept.disabled = false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
accept.addEventListener('click', () => void subscribe());
|
|
158
|
+
later.addEventListener('click', () => {
|
|
159
|
+
void api.dismiss().catch(() => undefined);
|
|
160
|
+
leave();
|
|
161
|
+
});
|
|
162
|
+
shadow.addEventListener('keydown', ((event) => {
|
|
163
|
+
if (event.key === 'Escape' && !busy)
|
|
164
|
+
later.click();
|
|
165
|
+
}));
|
|
166
|
+
// subscribed elsewhere (the bell, the site's own button) or blocked: nothing left to ask
|
|
167
|
+
const stop = api.onChange((state) => {
|
|
168
|
+
if (!busy && (state.subscribed || state.permission === 'denied'))
|
|
169
|
+
leave();
|
|
170
|
+
});
|
|
171
|
+
document.body.appendChild(host);
|
|
172
|
+
applyTheme(host);
|
|
173
|
+
// two frames, so the card starts from its hidden place and slides
|
|
174
|
+
requestAnimationFrame(() => requestAnimationFrame(() => (host.dataset.shown = '')));
|
|
175
|
+
return {
|
|
176
|
+
destroy() {
|
|
177
|
+
gone = true;
|
|
178
|
+
stop();
|
|
179
|
+
window.clearTimeout(leaveTimer);
|
|
180
|
+
host.remove();
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
package/dist/bell.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SubscriptionState } from './index.js';
|
|
2
|
+
/**
|
|
3
|
+
* The subscription bell (OneSignal's "subscription bell"): a round button in a bottom corner of the site that subscribes this
|
|
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. HQ switches it on per app; the SDK draws it from the web config.
|
|
6
|
+
*
|
|
7
|
+
* 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
|
+
* language (Arabic right to left, else English), the colour HQ gave it, and a light or dark panel to suit the page.
|
|
9
|
+
*/
|
|
10
|
+
export type BellPosition = 'bottom_right' | 'bottom_left';
|
|
11
|
+
/** The bell as HQ set it up (the web config's `bell`). */
|
|
12
|
+
export interface BellSettings {
|
|
13
|
+
position: BellPosition;
|
|
14
|
+
/** `#rrggbb`; null: the SDK's blue. */
|
|
15
|
+
color?: string | null;
|
|
16
|
+
/** Only browsers that are not subscribed see it (after its thanks, a browser that subscribes no longer does). */
|
|
17
|
+
hideWhenSubscribed?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** What the bell needs from the SDK. */
|
|
20
|
+
export interface BellApi {
|
|
21
|
+
state(): Promise<SubscriptionState>;
|
|
22
|
+
/** Asks the browser (synchronously, inside the click) and subscribes; resolves with the state afterwards. */
|
|
23
|
+
subscribe(): Promise<SubscriptionState>;
|
|
24
|
+
unsubscribe(): Promise<void>;
|
|
25
|
+
onChange(handler: (state: SubscriptionState) => void): () => void;
|
|
26
|
+
}
|
|
27
|
+
export interface BellHandle {
|
|
28
|
+
destroy(): void;
|
|
29
|
+
}
|
|
30
|
+
/** Draws the bell into the page; `destroy()` takes it away again. */
|
|
31
|
+
export declare function mountBell(api: BellApi, settings: BellSettings, language: string): BellHandle;
|