@synergonai/push-web 0.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/LICENSE +201 -0
- package/README.md +113 -0
- package/dist/index.cjs +433 -0
- package/dist/index.d.cts +118 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.js +421 -0
- package/dist/sw-template.cjs +67 -0
- package/dist/sw-template.d.cts +3 -0
- package/dist/sw-template.d.ts +3 -0
- package/dist/sw-template.js +32 -0
- package/package.json +44 -0
- package/public/firebase-messaging-sw.js +124 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
SynergonPush: () => SynergonPush
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
var import_push_core2 = require("@synergonai/push-core");
|
|
27
|
+
|
|
28
|
+
// src/firebase-bootstrap.ts
|
|
29
|
+
var import_app = require("firebase/app");
|
|
30
|
+
var import_messaging = require("firebase/messaging");
|
|
31
|
+
|
|
32
|
+
// src/fcm-fetch-patch.ts
|
|
33
|
+
var FCM_REGISTRATIONS_HOST = "fcmregistrations.googleapis.com";
|
|
34
|
+
var PATCH_FLAG = "__synergonFcmFetchPatched";
|
|
35
|
+
function installFcmFetchPatch() {
|
|
36
|
+
if (typeof globalThis === "undefined" || typeof globalThis.fetch !== "function") return;
|
|
37
|
+
const g = globalThis;
|
|
38
|
+
if (g[PATCH_FLAG]) return;
|
|
39
|
+
const originalFetch = globalThis.fetch.bind(globalThis);
|
|
40
|
+
globalThis.fetch = async function patchedFetch(input, init) {
|
|
41
|
+
try {
|
|
42
|
+
const url = resolveUrl(input);
|
|
43
|
+
if (!url || !url.includes(FCM_REGISTRATIONS_HOST)) {
|
|
44
|
+
return originalFetch(input, init);
|
|
45
|
+
}
|
|
46
|
+
const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
|
47
|
+
if (method !== "POST" && method !== "PATCH") {
|
|
48
|
+
return originalFetch(input, init);
|
|
49
|
+
}
|
|
50
|
+
const sourceBody = init?.body ?? (input instanceof Request ? await readRequestBody(input) : void 0);
|
|
51
|
+
if (typeof sourceBody !== "string") {
|
|
52
|
+
return originalFetch(input, init);
|
|
53
|
+
}
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(sourceBody);
|
|
57
|
+
} catch {
|
|
58
|
+
return originalFetch(input, init);
|
|
59
|
+
}
|
|
60
|
+
if (!parsed?.web || !("applicationPubKey" in parsed.web)) {
|
|
61
|
+
return originalFetch(input, init);
|
|
62
|
+
}
|
|
63
|
+
delete parsed.web.applicationPubKey;
|
|
64
|
+
const patchedBody = JSON.stringify(parsed);
|
|
65
|
+
if (input instanceof Request) {
|
|
66
|
+
const clonedInit = {
|
|
67
|
+
method,
|
|
68
|
+
headers: cloneHeaders(input.headers),
|
|
69
|
+
body: patchedBody,
|
|
70
|
+
credentials: input.credentials,
|
|
71
|
+
mode: input.mode,
|
|
72
|
+
referrer: input.referrer
|
|
73
|
+
};
|
|
74
|
+
return originalFetch(input.url, clonedInit);
|
|
75
|
+
}
|
|
76
|
+
return originalFetch(input, { ...init, body: patchedBody });
|
|
77
|
+
} catch {
|
|
78
|
+
return originalFetch(input, init);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
g[PATCH_FLAG] = true;
|
|
82
|
+
}
|
|
83
|
+
function resolveUrl(input) {
|
|
84
|
+
if (typeof input === "string") return input;
|
|
85
|
+
if (input instanceof URL) return input.href;
|
|
86
|
+
if (input instanceof Request) return input.url;
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
async function readRequestBody(req) {
|
|
90
|
+
try {
|
|
91
|
+
return await req.clone().text();
|
|
92
|
+
} catch {
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function cloneHeaders(h) {
|
|
97
|
+
const out = new Headers();
|
|
98
|
+
h.forEach((v, k) => out.append(k, v));
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/firebase-bootstrap.ts
|
|
103
|
+
var FirebaseBootstrap = class _FirebaseBootstrap {
|
|
104
|
+
constructor(app, messaging, opts) {
|
|
105
|
+
this.app = app;
|
|
106
|
+
this.messaging = messaging;
|
|
107
|
+
this.vapidKey = opts.vapidKey;
|
|
108
|
+
this.swReg = opts.serviceWorkerRegistration;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Returns null if the runtime can't support FCM (no service worker
|
|
112
|
+
* support, http://, etc). Callers should treat null as a hard "no push
|
|
113
|
+
* here" and surface that to the user.
|
|
114
|
+
*/
|
|
115
|
+
static async create(opts) {
|
|
116
|
+
if (!await (0, import_messaging.isSupported)()) return null;
|
|
117
|
+
installFcmFetchPatch();
|
|
118
|
+
const app = (0, import_app.initializeApp)(opts.firebaseConfig);
|
|
119
|
+
const messaging = (0, import_messaging.getMessaging)(app);
|
|
120
|
+
return new _FirebaseBootstrap(app, messaging, opts);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Fetch the current FCM registration token. Triggers the browser's
|
|
124
|
+
* notification permission prompt if not yet granted.
|
|
125
|
+
*/
|
|
126
|
+
async getToken() {
|
|
127
|
+
const token = await (0, import_messaging.getToken)(this.messaging, {
|
|
128
|
+
vapidKey: this.vapidKey,
|
|
129
|
+
serviceWorkerRegistration: this.swReg
|
|
130
|
+
});
|
|
131
|
+
if (!token) {
|
|
132
|
+
throw new Error("FCM returned an empty token. User may have denied notifications.");
|
|
133
|
+
}
|
|
134
|
+
return token;
|
|
135
|
+
}
|
|
136
|
+
/** Subscribe to foreground messages (page visible). Returns an unsubscribe. */
|
|
137
|
+
onMessage(handler) {
|
|
138
|
+
return (0, import_messaging.onMessage)(this.messaging, handler);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// src/storage-localstorage.ts
|
|
143
|
+
var LocalStorageAdapter = class _LocalStorageAdapter {
|
|
144
|
+
constructor() {
|
|
145
|
+
this.available = _LocalStorageAdapter.isAvailable();
|
|
146
|
+
}
|
|
147
|
+
async get(key) {
|
|
148
|
+
if (!this.available) return null;
|
|
149
|
+
try {
|
|
150
|
+
const raw = window.localStorage.getItem(key);
|
|
151
|
+
if (raw === null) return null;
|
|
152
|
+
return JSON.parse(raw);
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async set(key, value) {
|
|
158
|
+
if (!this.available) return;
|
|
159
|
+
try {
|
|
160
|
+
window.localStorage.setItem(key, JSON.stringify(value));
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async remove(key) {
|
|
165
|
+
if (!this.available) return;
|
|
166
|
+
try {
|
|
167
|
+
window.localStorage.removeItem(key);
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Some browsers (Safari private mode) expose localStorage but throw on
|
|
173
|
+
* write. Probe with a no-op write and clean up.
|
|
174
|
+
*/
|
|
175
|
+
static isAvailable() {
|
|
176
|
+
try {
|
|
177
|
+
if (typeof window === "undefined" || !window.localStorage) return false;
|
|
178
|
+
const probe = "__synergon_probe__";
|
|
179
|
+
window.localStorage.setItem(probe, "1");
|
|
180
|
+
window.localStorage.removeItem(probe);
|
|
181
|
+
return true;
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// src/payload.ts
|
|
189
|
+
var import_push_core = require("@synergonai/push-core");
|
|
190
|
+
function normalizePayload(raw) {
|
|
191
|
+
const data = raw.data ?? {};
|
|
192
|
+
const notification = raw.notification ?? {};
|
|
193
|
+
const rawMessageId = data[import_push_core.SYN_MESSAGE_ID_KEY];
|
|
194
|
+
return {
|
|
195
|
+
notification: {
|
|
196
|
+
title: notification.title,
|
|
197
|
+
body: notification.body,
|
|
198
|
+
image: notification.image
|
|
199
|
+
},
|
|
200
|
+
data,
|
|
201
|
+
messageId: rawMessageId && rawMessageId !== import_push_core.PLACEHOLDER_MESSAGE_ID ? rawMessageId : void 0,
|
|
202
|
+
campaignId: data[import_push_core.SYN_CAMPAIGN_ID_KEY]
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/index.ts
|
|
207
|
+
var SynergonPushClass = class {
|
|
208
|
+
constructor() {
|
|
209
|
+
this.apiClient = null;
|
|
210
|
+
this.eventQueue = null;
|
|
211
|
+
this.firebase = null;
|
|
212
|
+
this.storage = null;
|
|
213
|
+
this.logger = {};
|
|
214
|
+
this.initOpts = null;
|
|
215
|
+
this.messageHandler = null;
|
|
216
|
+
this.unsubMessage = null;
|
|
217
|
+
this.deviceId = null;
|
|
218
|
+
this.fcmToken = null;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Bootstrap the SDK. Idempotent — safe to call from React StrictMode
|
|
222
|
+
* double-render, etc. Returns the SDK status snapshot.
|
|
223
|
+
*/
|
|
224
|
+
async init(opts) {
|
|
225
|
+
if (this.initOpts) {
|
|
226
|
+
this.logger.warn?.("[push-web] SynergonPush.init called twice; ignoring second call.");
|
|
227
|
+
return this.getStatus();
|
|
228
|
+
}
|
|
229
|
+
this.initOpts = opts;
|
|
230
|
+
this.logger = opts.logger ?? {};
|
|
231
|
+
this.storage = new LocalStorageAdapter();
|
|
232
|
+
this.apiClient = new import_push_core2.ApiClient({ config: opts });
|
|
233
|
+
this.eventQueue = new import_push_core2.EventQueue({
|
|
234
|
+
api: this.apiClient,
|
|
235
|
+
storage: this.storage,
|
|
236
|
+
logger: this.logger
|
|
237
|
+
});
|
|
238
|
+
let bootstrap;
|
|
239
|
+
try {
|
|
240
|
+
bootstrap = await this.apiClient.bootstrap();
|
|
241
|
+
} catch (err) {
|
|
242
|
+
this.logger.error?.("[push-web] bootstrap failed", { error: err.message });
|
|
243
|
+
throw new Error(
|
|
244
|
+
"Synergon bootstrap failed. Either the publishable key is invalid or the account is missing its Firebase Web config. Configure it in Synergon admin \u2192 Campaigns \u2192 Push \u2192 Firebase Account."
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
let swReg;
|
|
248
|
+
if (opts.serviceWorkerPath) {
|
|
249
|
+
try {
|
|
250
|
+
swReg = await navigator.serviceWorker.register(opts.serviceWorkerPath);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
this.logger.warn?.("[push-web] service worker registration failed", {
|
|
253
|
+
path: opts.serviceWorkerPath,
|
|
254
|
+
error: err.message
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
this.firebase = await FirebaseBootstrap.create({
|
|
259
|
+
firebaseConfig: bootstrap.firebaseConfig,
|
|
260
|
+
vapidKey: bootstrap.vapidKey,
|
|
261
|
+
serviceWorkerRegistration: swReg
|
|
262
|
+
});
|
|
263
|
+
this.deviceId = await this.storage.get(import_push_core2.STORAGE_KEYS.deviceId);
|
|
264
|
+
this.fcmToken = await this.storage.get(import_push_core2.STORAGE_KEYS.lastFcmToken);
|
|
265
|
+
this.eventQueue.flush().catch((err) => this.logger.warn?.("[push-web] flush at boot failed", { error: err.message }));
|
|
266
|
+
if (typeof window !== "undefined") {
|
|
267
|
+
window.addEventListener("online", () => {
|
|
268
|
+
this.eventQueue?.flush().catch(() => void 0);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
this.subscribeForegroundMessages();
|
|
272
|
+
return this.getStatus();
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Prompt the user for notification permission, fetch a token from
|
|
276
|
+
* Firebase, then register with Synergon. Throws if the user denies or
|
|
277
|
+
* if Firebase is unavailable.
|
|
278
|
+
*/
|
|
279
|
+
async requestPermissionAndRegister(opts = {}) {
|
|
280
|
+
this.assertInitialized();
|
|
281
|
+
if (typeof Notification === "undefined") {
|
|
282
|
+
throw new Error("Notification API unavailable in this environment.");
|
|
283
|
+
}
|
|
284
|
+
const permission = await Notification.requestPermission();
|
|
285
|
+
if (permission !== "granted") {
|
|
286
|
+
throw new Error(`Notification permission ${permission}.`);
|
|
287
|
+
}
|
|
288
|
+
return this.register(opts);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Register only if the user has already granted permission. Returns
|
|
292
|
+
* null if not granted (no prompt). Useful when the integrator wants to
|
|
293
|
+
* call this on every app load (idempotent) without nagging the user.
|
|
294
|
+
*/
|
|
295
|
+
async registerIfPermitted(opts = {}) {
|
|
296
|
+
this.assertInitialized();
|
|
297
|
+
if (typeof Notification === "undefined") return null;
|
|
298
|
+
if (Notification.permission !== "granted") return null;
|
|
299
|
+
return this.register(opts);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Fetch (or refresh) the FCM token and POST it to /v1/campaigns/push/devices.
|
|
303
|
+
* Persists `deviceId` and `fcmToken` locally so subsequent calls are no-ops
|
|
304
|
+
* if the token hasn't changed.
|
|
305
|
+
*/
|
|
306
|
+
async register(opts = {}) {
|
|
307
|
+
this.assertInitialized();
|
|
308
|
+
const fb = this.firebase;
|
|
309
|
+
if (!fb) throw new Error("Firebase Messaging not supported in this browser.");
|
|
310
|
+
const token = await fb.getToken();
|
|
311
|
+
if (this.fcmToken && this.fcmToken === token && this.deviceId) {
|
|
312
|
+
this.logger.debug?.("[push-web] token unchanged; skipping re-register", {
|
|
313
|
+
deviceId: this.deviceId
|
|
314
|
+
});
|
|
315
|
+
return this.cachedDeviceResponse();
|
|
316
|
+
}
|
|
317
|
+
const response = await this.apiClient.registerDevice({
|
|
318
|
+
fcmToken: token,
|
|
319
|
+
platform: "web",
|
|
320
|
+
appVersion: opts.appVersion,
|
|
321
|
+
locale: typeof navigator !== "undefined" ? navigator.language : void 0,
|
|
322
|
+
identifierValue: opts.identifierValue,
|
|
323
|
+
leadId: opts.leadId,
|
|
324
|
+
marketingOptIn: opts.marketingOptIn
|
|
325
|
+
});
|
|
326
|
+
this.deviceId = response.id;
|
|
327
|
+
this.fcmToken = token;
|
|
328
|
+
await this.storage.set(import_push_core2.STORAGE_KEYS.deviceId, response.id);
|
|
329
|
+
await this.storage.set(import_push_core2.STORAGE_KEYS.lastFcmToken, token);
|
|
330
|
+
if (opts.identifierValue !== void 0) {
|
|
331
|
+
await this.storage.set(import_push_core2.STORAGE_KEYS.identifierValue, opts.identifierValue);
|
|
332
|
+
}
|
|
333
|
+
if (opts.marketingOptIn !== void 0) {
|
|
334
|
+
await this.storage.set(import_push_core2.STORAGE_KEYS.marketingOptIn, opts.marketingOptIn);
|
|
335
|
+
}
|
|
336
|
+
this.logger.info?.("[push-web] registered with Synergon", { deviceId: response.id });
|
|
337
|
+
return response;
|
|
338
|
+
}
|
|
339
|
+
/** Foreground message subscription. Returns an unsubscribe callable. */
|
|
340
|
+
onMessage(handler) {
|
|
341
|
+
this.messageHandler = handler;
|
|
342
|
+
return () => {
|
|
343
|
+
this.messageHandler = null;
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Manually fire a delivered/opened/clicked event. The SDK's own
|
|
348
|
+
* onMessage path fires `delivered` automatically — call this only when
|
|
349
|
+
* you want to attribute custom events (e.g. a button click in a
|
|
350
|
+
* notification-triggered modal).
|
|
351
|
+
*/
|
|
352
|
+
async reportEvent(idOrPayload, eventType) {
|
|
353
|
+
this.assertInitialized();
|
|
354
|
+
const ids = typeof idOrPayload === "string" ? { messageId: idOrPayload } : idOrPayload;
|
|
355
|
+
await this.eventQueue.enqueue({
|
|
356
|
+
...ids,
|
|
357
|
+
eventType,
|
|
358
|
+
platform: "web",
|
|
359
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Clear local state. Note: this does NOT delete the device on the
|
|
364
|
+
* Synergon side — the public endpoint doesn't permit publishable-key
|
|
365
|
+
* authenticated deletes. Have your backend call DELETE
|
|
366
|
+
* /v1/campaigns/push/devices/:id if you want full removal.
|
|
367
|
+
*/
|
|
368
|
+
async unregister() {
|
|
369
|
+
this.assertInitialized();
|
|
370
|
+
await this.storage.remove(import_push_core2.STORAGE_KEYS.deviceId);
|
|
371
|
+
await this.storage.remove(import_push_core2.STORAGE_KEYS.lastFcmToken);
|
|
372
|
+
await this.storage.remove(import_push_core2.STORAGE_KEYS.identifierValue);
|
|
373
|
+
await this.storage.remove(import_push_core2.STORAGE_KEYS.marketingOptIn);
|
|
374
|
+
this.deviceId = null;
|
|
375
|
+
this.fcmToken = null;
|
|
376
|
+
this.unsubMessage?.();
|
|
377
|
+
this.unsubMessage = null;
|
|
378
|
+
}
|
|
379
|
+
getStatus() {
|
|
380
|
+
const permission = typeof Notification === "undefined" ? "unsupported" : Notification.permission;
|
|
381
|
+
return {
|
|
382
|
+
initialized: this.initOpts !== null,
|
|
383
|
+
permission,
|
|
384
|
+
registered: this.deviceId !== null,
|
|
385
|
+
deviceId: this.deviceId,
|
|
386
|
+
fcmToken: this.fcmToken
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
// ─── internals ───────────────────────────────────────────────────────
|
|
390
|
+
subscribeForegroundMessages() {
|
|
391
|
+
if (!this.firebase) return;
|
|
392
|
+
this.unsubMessage = this.firebase.onMessage(async (raw) => {
|
|
393
|
+
const payload = normalizePayload(raw);
|
|
394
|
+
const canIdentify = !!payload.messageId || !!(this.fcmToken && payload.campaignId);
|
|
395
|
+
if (canIdentify) {
|
|
396
|
+
try {
|
|
397
|
+
await this.eventQueue?.enqueue({
|
|
398
|
+
messageId: payload.messageId,
|
|
399
|
+
fcmToken: payload.messageId ? void 0 : this.fcmToken,
|
|
400
|
+
campaignId: payload.messageId ? void 0 : payload.campaignId,
|
|
401
|
+
eventType: "delivered",
|
|
402
|
+
platform: "web",
|
|
403
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
404
|
+
});
|
|
405
|
+
} catch (err) {
|
|
406
|
+
this.logger.warn?.("[push-web] failed to report delivered", {
|
|
407
|
+
error: err.message
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
this.messageHandler?.(payload);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
cachedDeviceResponse() {
|
|
415
|
+
return {
|
|
416
|
+
id: this.deviceId,
|
|
417
|
+
status: "active",
|
|
418
|
+
platform: "web",
|
|
419
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
420
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
assertInitialized() {
|
|
424
|
+
if (!this.initOpts) {
|
|
425
|
+
throw new Error("SynergonPush.init() must be called before any other SDK method.");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
var SynergonPush = new SynergonPushClass();
|
|
430
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
431
|
+
0 && (module.exports = {
|
|
432
|
+
SynergonPush
|
|
433
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { ClientConfig, DeviceRegisterResponse, SynergonPushPayload } from '@synergonai/push-core';
|
|
2
|
+
export { SynergonPushPayload } from '@synergonai/push-core';
|
|
3
|
+
|
|
4
|
+
type PermissionState = 'granted' | 'denied' | 'default' | 'unsupported';
|
|
5
|
+
interface SynergonPushStatus {
|
|
6
|
+
/** Whether the SDK has been initialized via init(). */
|
|
7
|
+
initialized: boolean;
|
|
8
|
+
/** Notification permission state. */
|
|
9
|
+
permission: PermissionState;
|
|
10
|
+
/** True if a device row exists locally + on the server. */
|
|
11
|
+
registered: boolean;
|
|
12
|
+
/** Synergon device id, if registered. */
|
|
13
|
+
deviceId: string | null;
|
|
14
|
+
/** Most recent FCM token observed by the SDK. */
|
|
15
|
+
fcmToken: string | null;
|
|
16
|
+
}
|
|
17
|
+
interface InitOptions extends ClientConfig {
|
|
18
|
+
/**
|
|
19
|
+
* Optional path to the service worker. If omitted, firebase/messaging
|
|
20
|
+
* auto-registers `/firebase-messaging-sw.js`.
|
|
21
|
+
*
|
|
22
|
+
* Integrators should host the file at the path they specify here.
|
|
23
|
+
* Copy from `@synergonai/push-web/service-worker.js`.
|
|
24
|
+
*/
|
|
25
|
+
serviceWorkerPath?: string;
|
|
26
|
+
}
|
|
27
|
+
interface RegisterOptions {
|
|
28
|
+
/** Email / phone / opaque id you want to attribute this device to (cross-channel). */
|
|
29
|
+
identifierValue?: string;
|
|
30
|
+
/** Opt-in to marketing pushes. Required for marketing-category templates. */
|
|
31
|
+
marketingOptIn?: boolean;
|
|
32
|
+
/** Optional CRM lead id if your backend already has one. */
|
|
33
|
+
leadId?: string;
|
|
34
|
+
/** Override app version reported to Synergon. */
|
|
35
|
+
appVersion?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Public SDK entrypoint. Intended to be used as a singleton — tenants call
|
|
39
|
+
* `await SynergonPush.init(...)` once at app boot, then `register()` after
|
|
40
|
+
* the user agrees to receive notifications.
|
|
41
|
+
*
|
|
42
|
+
* Lifecycle:
|
|
43
|
+
* init()
|
|
44
|
+
* → SDK reads any previously-cached deviceId / fcmToken from localStorage
|
|
45
|
+
* → SDK subscribes to firebase/messaging foreground events
|
|
46
|
+
* requestPermissionAndRegister() OR registerIfPermitted()
|
|
47
|
+
* → prompt + fetch token from Firebase
|
|
48
|
+
* → POST /v1/campaigns/push/devices
|
|
49
|
+
* → on success: persist deviceId locally
|
|
50
|
+
* on every foreground message:
|
|
51
|
+
* → fire /events/delivered (idempotent on server)
|
|
52
|
+
* → invoke integrator's onMessage callback
|
|
53
|
+
* unregister()
|
|
54
|
+
* → clear local state (server-side delete is integrator-proxied via their backend)
|
|
55
|
+
*/
|
|
56
|
+
declare class SynergonPushClass {
|
|
57
|
+
private apiClient;
|
|
58
|
+
private eventQueue;
|
|
59
|
+
private firebase;
|
|
60
|
+
private storage;
|
|
61
|
+
private logger;
|
|
62
|
+
private initOpts;
|
|
63
|
+
private messageHandler;
|
|
64
|
+
private unsubMessage;
|
|
65
|
+
private deviceId;
|
|
66
|
+
private fcmToken;
|
|
67
|
+
/**
|
|
68
|
+
* Bootstrap the SDK. Idempotent — safe to call from React StrictMode
|
|
69
|
+
* double-render, etc. Returns the SDK status snapshot.
|
|
70
|
+
*/
|
|
71
|
+
init(opts: InitOptions): Promise<SynergonPushStatus>;
|
|
72
|
+
/**
|
|
73
|
+
* Prompt the user for notification permission, fetch a token from
|
|
74
|
+
* Firebase, then register with Synergon. Throws if the user denies or
|
|
75
|
+
* if Firebase is unavailable.
|
|
76
|
+
*/
|
|
77
|
+
requestPermissionAndRegister(opts?: RegisterOptions): Promise<DeviceRegisterResponse>;
|
|
78
|
+
/**
|
|
79
|
+
* Register only if the user has already granted permission. Returns
|
|
80
|
+
* null if not granted (no prompt). Useful when the integrator wants to
|
|
81
|
+
* call this on every app load (idempotent) without nagging the user.
|
|
82
|
+
*/
|
|
83
|
+
registerIfPermitted(opts?: RegisterOptions): Promise<DeviceRegisterResponse | null>;
|
|
84
|
+
/**
|
|
85
|
+
* Fetch (or refresh) the FCM token and POST it to /v1/campaigns/push/devices.
|
|
86
|
+
* Persists `deviceId` and `fcmToken` locally so subsequent calls are no-ops
|
|
87
|
+
* if the token hasn't changed.
|
|
88
|
+
*/
|
|
89
|
+
register(opts?: RegisterOptions): Promise<DeviceRegisterResponse>;
|
|
90
|
+
/** Foreground message subscription. Returns an unsubscribe callable. */
|
|
91
|
+
onMessage(handler: (payload: SynergonPushPayload) => void): () => void;
|
|
92
|
+
/**
|
|
93
|
+
* Manually fire a delivered/opened/clicked event. The SDK's own
|
|
94
|
+
* onMessage path fires `delivered` automatically — call this only when
|
|
95
|
+
* you want to attribute custom events (e.g. a button click in a
|
|
96
|
+
* notification-triggered modal).
|
|
97
|
+
*/
|
|
98
|
+
reportEvent(idOrPayload: string | {
|
|
99
|
+
messageId?: string;
|
|
100
|
+
fcmToken?: string;
|
|
101
|
+
campaignId?: string;
|
|
102
|
+
}, eventType: 'delivered' | 'opened' | 'clicked'): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Clear local state. Note: this does NOT delete the device on the
|
|
105
|
+
* Synergon side — the public endpoint doesn't permit publishable-key
|
|
106
|
+
* authenticated deletes. Have your backend call DELETE
|
|
107
|
+
* /v1/campaigns/push/devices/:id if you want full removal.
|
|
108
|
+
*/
|
|
109
|
+
unregister(): Promise<void>;
|
|
110
|
+
getStatus(): SynergonPushStatus;
|
|
111
|
+
private subscribeForegroundMessages;
|
|
112
|
+
private cachedDeviceResponse;
|
|
113
|
+
private assertInitialized;
|
|
114
|
+
}
|
|
115
|
+
/** Singleton entrypoint. Stateful across imports. */
|
|
116
|
+
declare const SynergonPush: SynergonPushClass;
|
|
117
|
+
|
|
118
|
+
export { type InitOptions, type PermissionState, type RegisterOptions, SynergonPush, type SynergonPushStatus };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { ClientConfig, DeviceRegisterResponse, SynergonPushPayload } from '@synergonai/push-core';
|
|
2
|
+
export { SynergonPushPayload } from '@synergonai/push-core';
|
|
3
|
+
|
|
4
|
+
type PermissionState = 'granted' | 'denied' | 'default' | 'unsupported';
|
|
5
|
+
interface SynergonPushStatus {
|
|
6
|
+
/** Whether the SDK has been initialized via init(). */
|
|
7
|
+
initialized: boolean;
|
|
8
|
+
/** Notification permission state. */
|
|
9
|
+
permission: PermissionState;
|
|
10
|
+
/** True if a device row exists locally + on the server. */
|
|
11
|
+
registered: boolean;
|
|
12
|
+
/** Synergon device id, if registered. */
|
|
13
|
+
deviceId: string | null;
|
|
14
|
+
/** Most recent FCM token observed by the SDK. */
|
|
15
|
+
fcmToken: string | null;
|
|
16
|
+
}
|
|
17
|
+
interface InitOptions extends ClientConfig {
|
|
18
|
+
/**
|
|
19
|
+
* Optional path to the service worker. If omitted, firebase/messaging
|
|
20
|
+
* auto-registers `/firebase-messaging-sw.js`.
|
|
21
|
+
*
|
|
22
|
+
* Integrators should host the file at the path they specify here.
|
|
23
|
+
* Copy from `@synergonai/push-web/service-worker.js`.
|
|
24
|
+
*/
|
|
25
|
+
serviceWorkerPath?: string;
|
|
26
|
+
}
|
|
27
|
+
interface RegisterOptions {
|
|
28
|
+
/** Email / phone / opaque id you want to attribute this device to (cross-channel). */
|
|
29
|
+
identifierValue?: string;
|
|
30
|
+
/** Opt-in to marketing pushes. Required for marketing-category templates. */
|
|
31
|
+
marketingOptIn?: boolean;
|
|
32
|
+
/** Optional CRM lead id if your backend already has one. */
|
|
33
|
+
leadId?: string;
|
|
34
|
+
/** Override app version reported to Synergon. */
|
|
35
|
+
appVersion?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Public SDK entrypoint. Intended to be used as a singleton — tenants call
|
|
39
|
+
* `await SynergonPush.init(...)` once at app boot, then `register()` after
|
|
40
|
+
* the user agrees to receive notifications.
|
|
41
|
+
*
|
|
42
|
+
* Lifecycle:
|
|
43
|
+
* init()
|
|
44
|
+
* → SDK reads any previously-cached deviceId / fcmToken from localStorage
|
|
45
|
+
* → SDK subscribes to firebase/messaging foreground events
|
|
46
|
+
* requestPermissionAndRegister() OR registerIfPermitted()
|
|
47
|
+
* → prompt + fetch token from Firebase
|
|
48
|
+
* → POST /v1/campaigns/push/devices
|
|
49
|
+
* → on success: persist deviceId locally
|
|
50
|
+
* on every foreground message:
|
|
51
|
+
* → fire /events/delivered (idempotent on server)
|
|
52
|
+
* → invoke integrator's onMessage callback
|
|
53
|
+
* unregister()
|
|
54
|
+
* → clear local state (server-side delete is integrator-proxied via their backend)
|
|
55
|
+
*/
|
|
56
|
+
declare class SynergonPushClass {
|
|
57
|
+
private apiClient;
|
|
58
|
+
private eventQueue;
|
|
59
|
+
private firebase;
|
|
60
|
+
private storage;
|
|
61
|
+
private logger;
|
|
62
|
+
private initOpts;
|
|
63
|
+
private messageHandler;
|
|
64
|
+
private unsubMessage;
|
|
65
|
+
private deviceId;
|
|
66
|
+
private fcmToken;
|
|
67
|
+
/**
|
|
68
|
+
* Bootstrap the SDK. Idempotent — safe to call from React StrictMode
|
|
69
|
+
* double-render, etc. Returns the SDK status snapshot.
|
|
70
|
+
*/
|
|
71
|
+
init(opts: InitOptions): Promise<SynergonPushStatus>;
|
|
72
|
+
/**
|
|
73
|
+
* Prompt the user for notification permission, fetch a token from
|
|
74
|
+
* Firebase, then register with Synergon. Throws if the user denies or
|
|
75
|
+
* if Firebase is unavailable.
|
|
76
|
+
*/
|
|
77
|
+
requestPermissionAndRegister(opts?: RegisterOptions): Promise<DeviceRegisterResponse>;
|
|
78
|
+
/**
|
|
79
|
+
* Register only if the user has already granted permission. Returns
|
|
80
|
+
* null if not granted (no prompt). Useful when the integrator wants to
|
|
81
|
+
* call this on every app load (idempotent) without nagging the user.
|
|
82
|
+
*/
|
|
83
|
+
registerIfPermitted(opts?: RegisterOptions): Promise<DeviceRegisterResponse | null>;
|
|
84
|
+
/**
|
|
85
|
+
* Fetch (or refresh) the FCM token and POST it to /v1/campaigns/push/devices.
|
|
86
|
+
* Persists `deviceId` and `fcmToken` locally so subsequent calls are no-ops
|
|
87
|
+
* if the token hasn't changed.
|
|
88
|
+
*/
|
|
89
|
+
register(opts?: RegisterOptions): Promise<DeviceRegisterResponse>;
|
|
90
|
+
/** Foreground message subscription. Returns an unsubscribe callable. */
|
|
91
|
+
onMessage(handler: (payload: SynergonPushPayload) => void): () => void;
|
|
92
|
+
/**
|
|
93
|
+
* Manually fire a delivered/opened/clicked event. The SDK's own
|
|
94
|
+
* onMessage path fires `delivered` automatically — call this only when
|
|
95
|
+
* you want to attribute custom events (e.g. a button click in a
|
|
96
|
+
* notification-triggered modal).
|
|
97
|
+
*/
|
|
98
|
+
reportEvent(idOrPayload: string | {
|
|
99
|
+
messageId?: string;
|
|
100
|
+
fcmToken?: string;
|
|
101
|
+
campaignId?: string;
|
|
102
|
+
}, eventType: 'delivered' | 'opened' | 'clicked'): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Clear local state. Note: this does NOT delete the device on the
|
|
105
|
+
* Synergon side — the public endpoint doesn't permit publishable-key
|
|
106
|
+
* authenticated deletes. Have your backend call DELETE
|
|
107
|
+
* /v1/campaigns/push/devices/:id if you want full removal.
|
|
108
|
+
*/
|
|
109
|
+
unregister(): Promise<void>;
|
|
110
|
+
getStatus(): SynergonPushStatus;
|
|
111
|
+
private subscribeForegroundMessages;
|
|
112
|
+
private cachedDeviceResponse;
|
|
113
|
+
private assertInitialized;
|
|
114
|
+
}
|
|
115
|
+
/** Singleton entrypoint. Stateful across imports. */
|
|
116
|
+
declare const SynergonPush: SynergonPushClass;
|
|
117
|
+
|
|
118
|
+
export { type InitOptions, type PermissionState, type RegisterOptions, SynergonPush, type SynergonPushStatus };
|