@fishonfire/bubbles-expo 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/dist/index.js ADDED
@@ -0,0 +1,2060 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ ANDROID_CHANNEL_IMPORTANCE_VALUES: () => ANDROID_CHANNEL_IMPORTANCE_VALUES2,
34
+ BUBBLES_DELIVERY_STATUSES: () => BUBBLES_DELIVERY_STATUSES,
35
+ BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY: () => BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY2,
36
+ BubblesAppAuthenticationError: () => BubblesAppAuthenticationError,
37
+ BubblesNotificationsProvider: () => BubblesNotificationsProvider,
38
+ buildBubblesDeliveryStatusPayload: () => buildBubblesDeliveryStatusPayload,
39
+ buildCreateDevicePayload: () => buildCreateDevicePayload,
40
+ buildStoredDeliveryStatusEventKey: () => buildStoredDeliveryStatusEventKey,
41
+ buildUpdateDevicePayload: () => buildUpdateDevicePayload,
42
+ createBubblesDevice: () => createBubblesDevice,
43
+ createBubblesDeviceClient: () => createBubblesDeviceClient,
44
+ createStoredDeliveryStatusEvent: () => createStoredDeliveryStatusEvent,
45
+ describeNotificationPermissionStatus: () => describeNotificationPermissionStatus,
46
+ enqueueStoredDeliveryStatusEvent: () => enqueueStoredDeliveryStatusEvent,
47
+ ensureDefaultNotificationChannel: () => ensureDefaultNotificationChannel,
48
+ ensureNotificationPermissions: () => ensureNotificationPermissions,
49
+ extractDeviceIdFromDeviceResponse: () => extractDeviceIdFromDeviceResponse,
50
+ flushStoredBubblesDeliveryStatuses: () => flushStoredBubblesDeliveryStatuses,
51
+ getAndroidNotificationChannelImportance: () => getAndroidNotificationChannelImportance,
52
+ getBubblesNotificationsRuntimeConfig: () => getBubblesNotificationsRuntimeConfig,
53
+ getDefaultNotificationChannelConfig: () => getDefaultNotificationChannelConfig,
54
+ getDeviceRegistrationState: () => getDeviceRegistrationState,
55
+ getDeviceToken: () => getDeviceToken,
56
+ getFCMToken: () => getFCMToken,
57
+ getNotificationPermissions: () => getNotificationPermissions,
58
+ isNotificationPermissionGranted: () => isNotificationPermissionGranted,
59
+ markStoredDeliveryStatusEventPosted: () => markStoredDeliveryStatusEventPosted,
60
+ patchStoredDeviceState: () => patchStoredDeviceState,
61
+ postBubblesDeliveryStatus: () => postBubblesDeliveryStatus,
62
+ postStoredBubblesDeliveryStatus: () => postStoredBubblesDeliveryStatus,
63
+ readStoredApiBaseUrl: () => readStoredApiBaseUrl,
64
+ readStoredAppKey: () => readStoredAppKey,
65
+ readStoredDeliveryStatusLedger: () => readStoredDeliveryStatusLedger,
66
+ readStoredDeviceId: () => readStoredDeviceId,
67
+ readStoredDeviceState: () => readStoredDeviceState,
68
+ registerBubblesBackgroundHandlers: () => registerBubblesBackgroundHandlers,
69
+ storeApiBaseUrl: () => storeApiBaseUrl,
70
+ storeAppKey: () => storeAppKey,
71
+ storeDeliveryStatusLedger: () => storeDeliveryStatusLedger,
72
+ storeDeviceId: () => storeDeviceId,
73
+ storeDeviceState: () => storeDeviceState,
74
+ syncBubblesDevice: () => syncBubblesDevice,
75
+ updateBubblesDevice: () => updateBubblesDevice,
76
+ useBubblesNotifications: () => useBubblesNotifications
77
+ });
78
+ module.exports = __toCommonJS(index_exports);
79
+
80
+ // src/config/runtime-config.ts
81
+ var import_expo_constants = __toESM(require("expo-constants"));
82
+
83
+ // plugin/src/shared-config.ts
84
+ var BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY = "bubblesNotificationsExpo";
85
+ var ANDROID_CHANNEL_IMPORTANCE_VALUES = [
86
+ "min",
87
+ "low",
88
+ "default",
89
+ "high",
90
+ "max"
91
+ ];
92
+
93
+ // src/internal/errors.ts
94
+ function createBubblesError(message) {
95
+ return new Error(`[@fishonfire/bubbles-expo] ${message}`);
96
+ }
97
+ function failWithBubblesError(message) {
98
+ throw createBubblesError(message);
99
+ }
100
+ function getErrorMessage(error) {
101
+ return error instanceof Error ? error.message : String(error);
102
+ }
103
+
104
+ // src/internal/validation.ts
105
+ function isPlainObject(value) {
106
+ return value !== null && typeof value === "object" && !Array.isArray(value);
107
+ }
108
+ function getRequiredNonEmptyString(value, fieldName) {
109
+ if (typeof value !== "string") {
110
+ failWithBubblesError(`"${fieldName}" must be a string.`);
111
+ }
112
+ const trimmedValue = value.trim();
113
+ if (!trimmedValue) {
114
+ failWithBubblesError(`"${fieldName}" must not be empty.`);
115
+ }
116
+ return trimmedValue;
117
+ }
118
+ function getRequiredNonEmptyStringFromSource(value, sourceDescription) {
119
+ if (typeof value !== "string") {
120
+ failWithBubblesError(`${sourceDescription} must be a string.`);
121
+ }
122
+ const trimmedValue = value.trim();
123
+ if (!trimmedValue) {
124
+ failWithBubblesError(`${sourceDescription} must not be empty.`);
125
+ }
126
+ return trimmedValue;
127
+ }
128
+ function normalizeIdentifier(value, fieldName) {
129
+ if (typeof value === "number") {
130
+ if (!Number.isFinite(value)) {
131
+ failWithBubblesError(
132
+ `"${fieldName}" must be a finite number when it is numeric.`
133
+ );
134
+ }
135
+ return String(value);
136
+ }
137
+ if (typeof value !== "string") {
138
+ failWithBubblesError(`"${fieldName}" must be a string or number.`);
139
+ }
140
+ const trimmedValue = value.trim();
141
+ if (!trimmedValue) {
142
+ failWithBubblesError(`"${fieldName}" must not be empty.`);
143
+ }
144
+ return trimmedValue;
145
+ }
146
+
147
+ // src/config/runtime-config.ts
148
+ var BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY2 = BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY;
149
+ var ANDROID_CHANNEL_IMPORTANCE_VALUES2 = ANDROID_CHANNEL_IMPORTANCE_VALUES;
150
+ function getAndroidChannelImportance(value, propertyPath) {
151
+ if (typeof value !== "string") {
152
+ failWithBubblesError(
153
+ `"${propertyPath}" must be one of: ${ANDROID_CHANNEL_IMPORTANCE_VALUES2.join(
154
+ ", "
155
+ )}.`
156
+ );
157
+ }
158
+ if (!ANDROID_CHANNEL_IMPORTANCE_VALUES2.includes(
159
+ value
160
+ )) {
161
+ failWithBubblesError(
162
+ `"${propertyPath}" must be one of: ${ANDROID_CHANNEL_IMPORTANCE_VALUES2.join(
163
+ ", "
164
+ )}.`
165
+ );
166
+ }
167
+ return value;
168
+ }
169
+ function getBubblesNotificationsRuntimeConfig() {
170
+ const expoConfig = import_expo_constants.default.expoConfig;
171
+ if (!expoConfig) {
172
+ failWithBubblesError(
173
+ "Expo runtime config is unavailable. Ensure the app is running with Expo config support."
174
+ );
175
+ }
176
+ if (!isPlainObject(expoConfig.extra)) {
177
+ failWithBubblesError(
178
+ `Missing "expo.extra.${BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY2}". Configure the "@fishonfire/bubbles-expo" plugin in app.json.`
179
+ );
180
+ }
181
+ const runtimeConfig = expoConfig.extra[BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY2];
182
+ if (!isPlainObject(runtimeConfig)) {
183
+ failWithBubblesError(
184
+ `Missing "expo.extra.${BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY2}". Configure the "@fishonfire/bubbles-expo" plugin in app.json.`
185
+ );
186
+ }
187
+ return {
188
+ defaultChannelId: getRequiredNonEmptyString(
189
+ runtimeConfig.defaultChannelId,
190
+ `expo.extra.${BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY2}.defaultChannelId`
191
+ ),
192
+ defaultChannelName: getRequiredNonEmptyString(
193
+ runtimeConfig.defaultChannelName,
194
+ `expo.extra.${BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY2}.defaultChannelName`
195
+ ),
196
+ androidChannelImportance: getAndroidChannelImportance(
197
+ runtimeConfig.androidChannelImportance,
198
+ `expo.extra.${BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY2}.androidChannelImportance`
199
+ )
200
+ };
201
+ }
202
+
203
+ // src/storage/device-state.ts
204
+ var import_expo_file_system = require("expo-file-system");
205
+ var STORED_DEVICE_STATE_FILE_NAME = "bubbles-notifications-expo-state.json";
206
+ var LEGACY_DEVICE_ID_FILE_NAME = "device-id.txt";
207
+ var STORED_DEVICE_STATE_VERSION = 1;
208
+ function getOptionalNonEmptyString(value, fieldName) {
209
+ if (value == null) {
210
+ return null;
211
+ }
212
+ if (typeof value !== "string") {
213
+ failWithBubblesError(`Stored "${fieldName}" must be a string when present.`);
214
+ }
215
+ const trimmedValue = value.trim();
216
+ return trimmedValue.length > 0 ? trimmedValue : null;
217
+ }
218
+ function getStoredDeviceStateFile() {
219
+ return new import_expo_file_system.File(import_expo_file_system.Paths.document, STORED_DEVICE_STATE_FILE_NAME);
220
+ }
221
+ function getLegacyDeviceIdFile() {
222
+ return new import_expo_file_system.File(import_expo_file_system.Paths.document, LEGACY_DEVICE_ID_FILE_NAME);
223
+ }
224
+ function readLegacyStoredDeviceId() {
225
+ const legacyDeviceIdFile = getLegacyDeviceIdFile();
226
+ if (!legacyDeviceIdFile.exists) {
227
+ return null;
228
+ }
229
+ const storedDeviceId = legacyDeviceIdFile.textSync().trim();
230
+ return storedDeviceId.length > 0 ? storedDeviceId : null;
231
+ }
232
+ function normalizeStoredDeviceState(value, sourceDescription) {
233
+ if (!isPlainObject(value)) {
234
+ failWithBubblesError(
235
+ `Stored device state in ${sourceDescription} must be a JSON object.`
236
+ );
237
+ }
238
+ if (value.version !== STORED_DEVICE_STATE_VERSION) {
239
+ failWithBubblesError(
240
+ `Stored device state in ${sourceDescription} uses unsupported schema version "${String(
241
+ value.version
242
+ )}".`
243
+ );
244
+ }
245
+ return {
246
+ deviceId: getOptionalNonEmptyString(value.deviceId, "deviceId"),
247
+ apiBaseUrl: getOptionalNonEmptyString(value.apiBaseUrl, "apiBaseUrl"),
248
+ appKey: getOptionalNonEmptyString(value.appKey, "appKey")
249
+ };
250
+ }
251
+ function normalizeStoredDeviceStateInput(value) {
252
+ if (!isPlainObject(value)) {
253
+ failWithBubblesError("Stored device state must be an object.");
254
+ }
255
+ return {
256
+ deviceId: getOptionalNonEmptyString(value.deviceId, "deviceId"),
257
+ apiBaseUrl: getOptionalNonEmptyString(value.apiBaseUrl, "apiBaseUrl"),
258
+ appKey: getOptionalNonEmptyString(value.appKey, "appKey")
259
+ };
260
+ }
261
+ function normalizeStoredDeviceStateUpdate(value) {
262
+ if (!isPlainObject(value)) {
263
+ failWithBubblesError("Stored device state update must be an object.");
264
+ }
265
+ const nextState = {};
266
+ if ("deviceId" in value) {
267
+ nextState.deviceId = getOptionalNonEmptyString(value.deviceId, "deviceId");
268
+ }
269
+ if ("apiBaseUrl" in value) {
270
+ nextState.apiBaseUrl = getOptionalNonEmptyString(
271
+ value.apiBaseUrl,
272
+ "apiBaseUrl"
273
+ );
274
+ }
275
+ if ("appKey" in value) {
276
+ nextState.appKey = getOptionalNonEmptyString(value.appKey, "appKey");
277
+ }
278
+ return nextState;
279
+ }
280
+ function serializeStoredDeviceState(state) {
281
+ const serializedState = {
282
+ version: STORED_DEVICE_STATE_VERSION
283
+ };
284
+ if (state.deviceId) {
285
+ serializedState.deviceId = state.deviceId;
286
+ }
287
+ if (state.apiBaseUrl) {
288
+ serializedState.apiBaseUrl = state.apiBaseUrl;
289
+ }
290
+ if (state.appKey) {
291
+ serializedState.appKey = state.appKey;
292
+ }
293
+ return serializedState;
294
+ }
295
+ function readStoredDeviceStateFileText() {
296
+ const storedDeviceStateFile = getStoredDeviceStateFile();
297
+ if (!storedDeviceStateFile.exists) {
298
+ return null;
299
+ }
300
+ const contents = storedDeviceStateFile.textSync().trim();
301
+ return contents.length > 0 ? contents : null;
302
+ }
303
+ function ensureStoredDeviceStateFile() {
304
+ const storedDeviceStateFile = getStoredDeviceStateFile();
305
+ if (!storedDeviceStateFile.exists) {
306
+ storedDeviceStateFile.create({ intermediates: true });
307
+ }
308
+ return storedDeviceStateFile;
309
+ }
310
+ function withLegacyDeviceIdFallback(state) {
311
+ if (state.deviceId) {
312
+ return state;
313
+ }
314
+ const legacyDeviceId = readLegacyStoredDeviceId();
315
+ if (!legacyDeviceId) {
316
+ return state;
317
+ }
318
+ return {
319
+ ...state,
320
+ deviceId: legacyDeviceId
321
+ };
322
+ }
323
+ function readStoredDeviceState() {
324
+ const storedDeviceStateFile = getStoredDeviceStateFile();
325
+ const rawState = readStoredDeviceStateFileText();
326
+ if (rawState === null) {
327
+ return withLegacyDeviceIdFallback({
328
+ deviceId: null,
329
+ apiBaseUrl: null,
330
+ appKey: null
331
+ });
332
+ }
333
+ let parsedState;
334
+ try {
335
+ parsedState = JSON.parse(rawState);
336
+ } catch {
337
+ failWithBubblesError(
338
+ `Stored device state at "${storedDeviceStateFile.uri}" is not valid JSON.`
339
+ );
340
+ }
341
+ return withLegacyDeviceIdFallback(
342
+ normalizeStoredDeviceState(parsedState, `"${storedDeviceStateFile.uri}"`)
343
+ );
344
+ }
345
+ function storeDeviceState(state) {
346
+ const normalizedState = normalizeStoredDeviceStateInput(state);
347
+ const storedDeviceStateFile = ensureStoredDeviceStateFile();
348
+ storedDeviceStateFile.write(
349
+ JSON.stringify(serializeStoredDeviceState(normalizedState), null, 2)
350
+ );
351
+ return normalizedState;
352
+ }
353
+ function patchStoredDeviceState(update) {
354
+ const normalizedUpdate = normalizeStoredDeviceStateUpdate(update);
355
+ return storeDeviceState({
356
+ ...readStoredDeviceState(),
357
+ ...normalizedUpdate
358
+ });
359
+ }
360
+ function readStoredDeviceId() {
361
+ return readStoredDeviceState().deviceId;
362
+ }
363
+ function readStoredApiBaseUrl() {
364
+ return readStoredDeviceState().apiBaseUrl;
365
+ }
366
+ function readStoredAppKey() {
367
+ return readStoredDeviceState().appKey;
368
+ }
369
+ function storeDeviceId(deviceId) {
370
+ return patchStoredDeviceState({ deviceId });
371
+ }
372
+ function storeApiBaseUrl(apiBaseUrl) {
373
+ return patchStoredDeviceState({ apiBaseUrl });
374
+ }
375
+ function storeAppKey(appKey) {
376
+ return patchStoredDeviceState({ appKey });
377
+ }
378
+
379
+ // src/api/device-client.ts
380
+ var import_bubbles_js2 = require("@fishonfire/bubbles-js");
381
+
382
+ // src/api/device-payload.ts
383
+ var import_bubbles_js = require("@fishonfire/bubbles-js");
384
+ function normalizeOptionalString(value, fieldName) {
385
+ if (value == null) {
386
+ return null;
387
+ }
388
+ if (typeof value !== "string") {
389
+ failWithBubblesError(`"${fieldName}" must be a string when present.`);
390
+ }
391
+ const trimmedValue = value.trim();
392
+ return trimmedValue.length > 0 ? trimmedValue : null;
393
+ }
394
+ function normalizeRequiredNullableString(value, fieldName) {
395
+ if (value === null) {
396
+ return null;
397
+ }
398
+ if (value === void 0) {
399
+ failWithBubblesError(
400
+ `"${fieldName}" is required. Pass a push token string or null.`
401
+ );
402
+ }
403
+ return getRequiredNonEmptyString(value, fieldName);
404
+ }
405
+ function normalizeAppId(value) {
406
+ if (typeof value === "number") {
407
+ if (!Number.isFinite(value)) {
408
+ failWithBubblesError('"appId" must be a finite number.');
409
+ }
410
+ return value;
411
+ }
412
+ if (typeof value === "string") {
413
+ return getRequiredNonEmptyString(value, "appId");
414
+ }
415
+ failWithBubblesError('"appId" must be a string or number.');
416
+ }
417
+ function normalizePlatform(value) {
418
+ if (value === "android" || value === "ios") {
419
+ return value;
420
+ }
421
+ failWithBubblesError('"platform" must be either "android" or "ios".');
422
+ }
423
+ function normalizeAliasing(value) {
424
+ if (value == null) {
425
+ return [];
426
+ }
427
+ if (!Array.isArray(value)) {
428
+ failWithBubblesError('"aliasing" must be an array of strings when present.');
429
+ }
430
+ return value.map(
431
+ (entry, index) => normalizeOptionalString(entry, `aliasing[${index}]`)
432
+ ).filter((entry) => entry !== null);
433
+ }
434
+ function normalizeNotificationsEnabled(value) {
435
+ if (typeof value !== "boolean") {
436
+ failWithBubblesError('"notificationsEnabled" must be a boolean.');
437
+ }
438
+ return value;
439
+ }
440
+ function resolveLocaleAndTimeZone(input) {
441
+ const resolved = input ?? (0, import_bubbles_js.getLocaleAndTimeZone)();
442
+ return {
443
+ locale: getRequiredNonEmptyString(resolved.locale, "locale"),
444
+ timeZone: normalizeOptionalString(resolved.timeZone, "timeZone")
445
+ };
446
+ }
447
+ function buildBaseDevicePayload(options) {
448
+ const { locale, timeZone } = resolveLocaleAndTimeZone(
449
+ options.localeAndTimeZone
450
+ );
451
+ const payload = {
452
+ user_id: getRequiredNonEmptyString(options.userId, "userId"),
453
+ aliasing: normalizeAliasing(options.aliasing),
454
+ platform: normalizePlatform(options.platform),
455
+ push_token: normalizeRequiredNullableString(options.pushToken, "pushToken"),
456
+ locale,
457
+ notifications_enabled: normalizeNotificationsEnabled(
458
+ options.notificationsEnabled
459
+ )
460
+ };
461
+ if ("fid" in options) {
462
+ payload.fid = normalizeOptionalString(options.fid, "fid");
463
+ }
464
+ const appVersion = normalizeOptionalString(options.appVersion, "appVersion");
465
+ if (appVersion) {
466
+ payload.app_version = appVersion;
467
+ }
468
+ if (timeZone) {
469
+ payload.timezone = timeZone;
470
+ }
471
+ return payload;
472
+ }
473
+ function buildCreateDevicePayload(options) {
474
+ return {
475
+ app_id: normalizeAppId(options.appId),
476
+ ...buildBaseDevicePayload(options)
477
+ };
478
+ }
479
+ function buildUpdateDevicePayload(options) {
480
+ return buildBaseDevicePayload(options);
481
+ }
482
+
483
+ // src/api/device-client.ts
484
+ var APP_KEY_HEADER_NAME = "X-App-Key";
485
+ var INVALID_APP_KEY_ERROR = "invalid app key for app";
486
+ var BubblesAppAuthenticationError = class extends Error {
487
+ constructor(cause) {
488
+ super("Bubbles app configuration/authentication failed: invalid app key for app.");
489
+ this.name = "BubblesAppAuthenticationError";
490
+ this.cause = cause;
491
+ }
492
+ };
493
+ function resolveDeviceClient(options) {
494
+ if (options.client) {
495
+ return options.client;
496
+ }
497
+ return createBubblesDeviceClient({
498
+ ...options.clientOptions,
499
+ appKey: options.appKey,
500
+ baseUrl: getRequiredNonEmptyString(options.apiBaseUrl, "apiBaseUrl")
501
+ });
502
+ }
503
+ function createBubblesDeviceClient(options) {
504
+ const { appKey: rawAppKey, ...clientOptions } = options;
505
+ const appKey = rawAppKey ? getRequiredNonEmptyString(rawAppKey, "appKey") : null;
506
+ return new import_bubbles_js2.DeviceClient({
507
+ ...clientOptions,
508
+ baseUrl: getRequiredNonEmptyString(clientOptions.baseUrl, "apiBaseUrl"),
509
+ defaultHeaders: {
510
+ ...clientOptions.defaultHeaders,
511
+ ...appKey ? { [APP_KEY_HEADER_NAME]: appKey } : {}
512
+ }
513
+ });
514
+ }
515
+ function withAppKeyHeader(appKey, requestOptions) {
516
+ return {
517
+ ...requestOptions,
518
+ headers: {
519
+ ...requestOptions?.headers,
520
+ [APP_KEY_HEADER_NAME]: getRequiredNonEmptyString(appKey, "appKey")
521
+ }
522
+ };
523
+ }
524
+ function isInvalidAppKeyResponse(error) {
525
+ if (!error || typeof error !== "object") {
526
+ return false;
527
+ }
528
+ const status = Reflect.get(error, "status");
529
+ const body = Reflect.get(error, "body");
530
+ return status === 401 && !!body && typeof body === "object" && !Array.isArray(body) && Reflect.get(body, "error") === INVALID_APP_KEY_ERROR;
531
+ }
532
+ function throwAppAuthenticationError(error) {
533
+ if (isInvalidAppKeyResponse(error)) {
534
+ throw new BubblesAppAuthenticationError(error);
535
+ }
536
+ throw error;
537
+ }
538
+ function extractDeviceIdFromDeviceResponse(value) {
539
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
540
+ return null;
541
+ }
542
+ const id = Reflect.get(value, "id");
543
+ if (typeof id === "number") {
544
+ return Number.isFinite(id) ? String(id) : null;
545
+ }
546
+ if (typeof id === "string") {
547
+ const trimmedId = id.trim();
548
+ return trimmedId.length > 0 ? trimmedId : null;
549
+ }
550
+ return null;
551
+ }
552
+ async function createBubblesDevice(options) {
553
+ const client = resolveDeviceClient(options);
554
+ const payload = buildCreateDevicePayload(options);
555
+ try {
556
+ return await client.createDevice(
557
+ payload,
558
+ withAppKeyHeader(options.appKey, options.requestOptions)
559
+ );
560
+ } catch (error) {
561
+ throwAppAuthenticationError(error);
562
+ }
563
+ }
564
+ async function updateBubblesDevice(options) {
565
+ const client = resolveDeviceClient(options);
566
+ const payload = buildUpdateDevicePayload(options);
567
+ try {
568
+ return await client.updateDevice(
569
+ normalizeIdentifier(options.deviceId, "deviceId"),
570
+ payload,
571
+ withAppKeyHeader(options.appKey, options.requestOptions)
572
+ );
573
+ } catch (error) {
574
+ throwAppAuthenticationError(error);
575
+ }
576
+ }
577
+ async function syncBubblesDevice(options) {
578
+ const client = resolveDeviceClient(options);
579
+ if (options.deviceId !== null) {
580
+ const deviceId = normalizeIdentifier(options.deviceId, "deviceId");
581
+ let response2;
582
+ try {
583
+ response2 = await client.updateDevice(
584
+ deviceId,
585
+ buildUpdateDevicePayload(options),
586
+ withAppKeyHeader(options.appKey, options.updateRequestOptions)
587
+ );
588
+ } catch (error) {
589
+ throwAppAuthenticationError(error);
590
+ }
591
+ return {
592
+ action: "updated",
593
+ deviceId,
594
+ response: response2
595
+ };
596
+ }
597
+ let response;
598
+ try {
599
+ response = await client.createDevice(
600
+ buildCreateDevicePayload(options),
601
+ withAppKeyHeader(options.appKey, options.createRequestOptions)
602
+ );
603
+ } catch (error) {
604
+ throwAppAuthenticationError(error);
605
+ }
606
+ return {
607
+ action: "created",
608
+ deviceId: extractDeviceIdFromDeviceResponse(response),
609
+ response
610
+ };
611
+ }
612
+
613
+ // src/api/delivery-status.ts
614
+ var BUBBLES_DELIVERY_STATUSES = {
615
+ notificationReceived: "received",
616
+ notificationShown: "shown",
617
+ notificationClicked: "clicked",
618
+ notificationsDisabled: "disabled"
619
+ };
620
+ function normalizeError(value) {
621
+ if (value == null) {
622
+ return void 0;
623
+ }
624
+ if (typeof value !== "string") {
625
+ failWithBubblesError('"error" must be a string when present.');
626
+ }
627
+ const trimmedValue = value.trim();
628
+ if (!trimmedValue) {
629
+ failWithBubblesError('"error" must not be empty when present.');
630
+ }
631
+ return trimmedValue;
632
+ }
633
+ function normalizeStatus(value) {
634
+ if (value === void 0) {
635
+ return void 0;
636
+ }
637
+ const supportedValues = Object.values(BUBBLES_DELIVERY_STATUSES);
638
+ if (typeof value !== "string" || !supportedValues.includes(value)) {
639
+ failWithBubblesError(
640
+ `"status" must be one of: ${supportedValues.join(", ")}.`
641
+ );
642
+ }
643
+ return value;
644
+ }
645
+ function resolveDeviceClient2(options) {
646
+ if (options.client) {
647
+ return options.client;
648
+ }
649
+ return createBubblesDeviceClient({
650
+ ...options.clientOptions,
651
+ appKey: getRequiredNonEmptyString(options.appKey, "appKey"),
652
+ baseUrl: getRequiredNonEmptyString(options.apiBaseUrl, "apiBaseUrl")
653
+ });
654
+ }
655
+ function withAppKeyHeader2(appKey, requestOptions) {
656
+ return {
657
+ ...requestOptions,
658
+ headers: {
659
+ ...requestOptions?.headers,
660
+ "X-App-Key": getRequiredNonEmptyString(appKey, "appKey")
661
+ }
662
+ };
663
+ }
664
+ function isInvalidAppKeyResponse2(error) {
665
+ if (!error || typeof error !== "object") {
666
+ return false;
667
+ }
668
+ const status = Reflect.get(error, "status");
669
+ const body = Reflect.get(error, "body");
670
+ return status === 401 && !!body && typeof body === "object" && !Array.isArray(body) && Reflect.get(body, "error") === "invalid app key for app";
671
+ }
672
+ function buildBubblesDeliveryStatusPayload(options) {
673
+ const status = normalizeStatus(options.status);
674
+ const error = normalizeError(options.error);
675
+ if (!status && !error) {
676
+ failWithBubblesError(
677
+ 'A delivery status payload must include "status" and/or "error".'
678
+ );
679
+ }
680
+ return {
681
+ ...status ? { status } : {},
682
+ ...error ? { error } : {}
683
+ };
684
+ }
685
+ async function postBubblesDeliveryStatus(options) {
686
+ const client = resolveDeviceClient2(options);
687
+ try {
688
+ await client.postDeliveryStatus(
689
+ normalizeIdentifier(options.deviceId, "deviceId"),
690
+ normalizeIdentifier(options.notificationId, "notificationId"),
691
+ buildBubblesDeliveryStatusPayload(options),
692
+ withAppKeyHeader2(options.appKey, options.requestOptions)
693
+ );
694
+ } catch (error) {
695
+ if (isInvalidAppKeyResponse2(error)) {
696
+ throw new BubblesAppAuthenticationError(error);
697
+ }
698
+ throw error;
699
+ }
700
+ }
701
+
702
+ // src/storage/delivery-status-ledger.ts
703
+ var import_expo_file_system2 = require("expo-file-system");
704
+ var DELIVERY_STATUS_LEDGER_FILE_NAME = "bubbles-notifications-expo-delivery-status-ledger.json";
705
+ var DELIVERY_STATUS_LEDGER_VERSION = 1;
706
+ var MAX_POSTED_EVENT_KEYS = 200;
707
+ function getDeliveryStatusLedgerFile() {
708
+ return new import_expo_file_system2.File(import_expo_file_system2.Paths.document, DELIVERY_STATUS_LEDGER_FILE_NAME);
709
+ }
710
+ function getOptionalNonEmptyString2(value) {
711
+ if (value == null) {
712
+ return void 0;
713
+ }
714
+ if (typeof value !== "string") {
715
+ return void 0;
716
+ }
717
+ const trimmedValue = value.trim();
718
+ return trimmedValue.length > 0 ? trimmedValue : void 0;
719
+ }
720
+ function requireString(value, sourceDescription) {
721
+ if (typeof value !== "string") {
722
+ failWithBubblesError(`${sourceDescription} must be a string.`);
723
+ }
724
+ const trimmedValue = value.trim();
725
+ if (!trimmedValue) {
726
+ failWithBubblesError(`${sourceDescription} must not be empty.`);
727
+ }
728
+ return trimmedValue;
729
+ }
730
+ function buildStoredDeliveryStatusEventKey(options) {
731
+ return JSON.stringify([
732
+ options.notificationId,
733
+ options.status ?? null,
734
+ options.error ?? null,
735
+ options.actionId ?? null
736
+ ]);
737
+ }
738
+ function createStoredDeliveryStatusEvent(options) {
739
+ const error = getOptionalNonEmptyString2(options.error);
740
+ const actionId = getOptionalNonEmptyString2(options.actionId);
741
+ return {
742
+ key: buildStoredDeliveryStatusEventKey({
743
+ notificationId: options.notificationId,
744
+ status: options.status,
745
+ error,
746
+ actionId
747
+ }),
748
+ notificationId: options.notificationId,
749
+ ...options.status ? { status: options.status } : {},
750
+ ...error ? { error } : {},
751
+ ...actionId ? { actionId } : {},
752
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
753
+ };
754
+ }
755
+ function normalizeStoredDeliveryStatusEvent(value, sourceDescription) {
756
+ if (!isPlainObject(value)) {
757
+ failWithBubblesError(`${sourceDescription} must be a JSON object.`);
758
+ }
759
+ const status = getOptionalNonEmptyString2(value.status);
760
+ const error = getOptionalNonEmptyString2(value.error);
761
+ const actionId = getOptionalNonEmptyString2(value.actionId);
762
+ return {
763
+ key: requireString(value.key, `${sourceDescription}.key`),
764
+ notificationId: requireString(
765
+ value.notificationId,
766
+ `${sourceDescription}.notificationId`
767
+ ),
768
+ ...status ? { status } : {},
769
+ ...error ? { error } : {},
770
+ ...actionId ? { actionId } : {},
771
+ createdAt: requireString(value.createdAt, `${sourceDescription}.createdAt`)
772
+ };
773
+ }
774
+ function normalizeStringArray(value) {
775
+ if (!Array.isArray(value)) {
776
+ return [];
777
+ }
778
+ return value.map((item) => getOptionalNonEmptyString2(item)).filter((item) => item !== void 0);
779
+ }
780
+ function normalizeDeliveryStatusLedger(value, sourceDescription) {
781
+ if (!isPlainObject(value)) {
782
+ failWithBubblesError(
783
+ `Stored delivery status ledger in ${sourceDescription} must be a JSON object.`
784
+ );
785
+ }
786
+ if (value.version !== DELIVERY_STATUS_LEDGER_VERSION) {
787
+ failWithBubblesError(
788
+ `Stored delivery status ledger in ${sourceDescription} uses unsupported schema version "${String(
789
+ value.version
790
+ )}".`
791
+ );
792
+ }
793
+ return {
794
+ pendingEvents: Array.isArray(value.pendingEvents) ? value.pendingEvents.map(
795
+ (event, index) => normalizeStoredDeliveryStatusEvent(
796
+ event,
797
+ `${sourceDescription}.pendingEvents[${index}]`
798
+ )
799
+ ) : [],
800
+ postedEventKeys: normalizeStringArray(value.postedEventKeys).slice(
801
+ -MAX_POSTED_EVENT_KEYS
802
+ )
803
+ };
804
+ }
805
+ function serializeDeliveryStatusLedger(ledger) {
806
+ return {
807
+ version: DELIVERY_STATUS_LEDGER_VERSION,
808
+ pendingEvents: ledger.pendingEvents,
809
+ postedEventKeys: ledger.postedEventKeys.slice(-MAX_POSTED_EVENT_KEYS)
810
+ };
811
+ }
812
+ function readDeliveryStatusLedgerFileText() {
813
+ const ledgerFile = getDeliveryStatusLedgerFile();
814
+ if (!ledgerFile.exists) {
815
+ return null;
816
+ }
817
+ const contents = ledgerFile.textSync().trim();
818
+ return contents.length > 0 ? contents : null;
819
+ }
820
+ function ensureDeliveryStatusLedgerFile() {
821
+ const ledgerFile = getDeliveryStatusLedgerFile();
822
+ if (!ledgerFile.exists) {
823
+ ledgerFile.create({ intermediates: true });
824
+ }
825
+ return ledgerFile;
826
+ }
827
+ function readStoredDeliveryStatusLedger() {
828
+ const ledgerFile = getDeliveryStatusLedgerFile();
829
+ const rawLedger = readDeliveryStatusLedgerFileText();
830
+ if (rawLedger === null) {
831
+ return {
832
+ pendingEvents: [],
833
+ postedEventKeys: []
834
+ };
835
+ }
836
+ let parsedLedger;
837
+ try {
838
+ parsedLedger = JSON.parse(rawLedger);
839
+ } catch {
840
+ failWithBubblesError(
841
+ `Stored delivery status ledger at "${ledgerFile.uri}" is not valid JSON.`
842
+ );
843
+ }
844
+ return normalizeDeliveryStatusLedger(
845
+ parsedLedger,
846
+ `"${ledgerFile.uri}"`
847
+ );
848
+ }
849
+ function storeDeliveryStatusLedger(ledger) {
850
+ const normalizedLedger = {
851
+ pendingEvents: ledger.pendingEvents,
852
+ postedEventKeys: ledger.postedEventKeys.slice(-MAX_POSTED_EVENT_KEYS)
853
+ };
854
+ const ledgerFile = ensureDeliveryStatusLedgerFile();
855
+ ledgerFile.write(
856
+ JSON.stringify(serializeDeliveryStatusLedger(normalizedLedger), null, 2)
857
+ );
858
+ return normalizedLedger;
859
+ }
860
+ function enqueueStoredDeliveryStatusEvent(event) {
861
+ const ledger = readStoredDeliveryStatusLedger();
862
+ if (ledger.postedEventKeys.includes(event.key) || ledger.pendingEvents.some((pendingEvent) => pendingEvent.key === event.key)) {
863
+ return ledger;
864
+ }
865
+ return storeDeliveryStatusLedger({
866
+ ...ledger,
867
+ pendingEvents: [...ledger.pendingEvents, event]
868
+ });
869
+ }
870
+ function markStoredDeliveryStatusEventPosted(event) {
871
+ const ledger = readStoredDeliveryStatusLedger();
872
+ const postedEventKeys = ledger.postedEventKeys.includes(event.key) ? ledger.postedEventKeys : [...ledger.postedEventKeys, event.key];
873
+ return storeDeliveryStatusLedger({
874
+ pendingEvents: ledger.pendingEvents.filter(
875
+ (pendingEvent) => pendingEvent.key !== event.key
876
+ ),
877
+ postedEventKeys
878
+ });
879
+ }
880
+
881
+ // src/notifications/delivery-status.ts
882
+ function warnMissingApiBaseUrl(source) {
883
+ console.warn(
884
+ `[@fishonfire/bubbles-expo] Skipping ${source} delivery-status update because "apiBaseUrl" has not been persisted yet.`
885
+ );
886
+ }
887
+ function warnMissingAppKey(source) {
888
+ console.warn(
889
+ `[@fishonfire/bubbles-expo] Skipping ${source} delivery-status update because "appKey" has not been persisted yet.`
890
+ );
891
+ }
892
+ async function postStoredBubblesDeliveryStatus(options) {
893
+ const storedDeviceState = options.storedDeviceState ?? readStoredDeviceState();
894
+ const { deviceId, apiBaseUrl, appKey } = storedDeviceState;
895
+ if (!options.notificationId) {
896
+ return storedDeviceState;
897
+ }
898
+ const event = createStoredDeliveryStatusEvent({
899
+ notificationId: options.notificationId,
900
+ status: options.status,
901
+ error: options.error,
902
+ actionId: options.actionId
903
+ });
904
+ enqueueStoredDeliveryStatusEvent(event);
905
+ if (!deviceId) {
906
+ return storedDeviceState;
907
+ }
908
+ if (!apiBaseUrl) {
909
+ warnMissingApiBaseUrl(options.source);
910
+ return storedDeviceState;
911
+ }
912
+ if (!appKey) {
913
+ warnMissingAppKey(options.source);
914
+ return storedDeviceState;
915
+ }
916
+ await flushStoredBubblesDeliveryStatuses({
917
+ apiBaseUrl,
918
+ appKey,
919
+ deviceId
920
+ });
921
+ return storedDeviceState;
922
+ }
923
+ async function flushStoredBubblesDeliveryStatuses(storedDeviceState = readStoredDeviceState()) {
924
+ const { deviceId, apiBaseUrl, appKey } = storedDeviceState;
925
+ if (!deviceId || !apiBaseUrl || !appKey) {
926
+ return storedDeviceState;
927
+ }
928
+ const { pendingEvents } = readStoredDeliveryStatusLedger();
929
+ for (const event of pendingEvents) {
930
+ await postStoredDeliveryStatusEvent(apiBaseUrl, appKey, deviceId, event);
931
+ markStoredDeliveryStatusEventPosted(event);
932
+ }
933
+ return storedDeviceState;
934
+ }
935
+ async function postStoredDeliveryStatusEvent(apiBaseUrl, appKey, deviceId, event) {
936
+ await postBubblesDeliveryStatus({
937
+ apiBaseUrl,
938
+ appKey,
939
+ deviceId,
940
+ notificationId: event.notificationId,
941
+ status: event.status,
942
+ error: event.error
943
+ });
944
+ }
945
+
946
+ // src/runtime/channels.ts
947
+ var Notifications = __toESM(require("expo-notifications"));
948
+ var import_react_native = require("react-native");
949
+ var ANDROID_CHANNEL_IMPORTANCE_MAP = {
950
+ min: Notifications.AndroidImportance.MIN,
951
+ low: Notifications.AndroidImportance.LOW,
952
+ default: Notifications.AndroidImportance.DEFAULT,
953
+ high: Notifications.AndroidImportance.HIGH,
954
+ max: Notifications.AndroidImportance.MAX
955
+ };
956
+ function fail(message) {
957
+ throw new Error(`[@fishonfire/bubbles-expo] ${message}`);
958
+ }
959
+ function getRequiredNonEmptyString2(value, fieldName) {
960
+ if (typeof value !== "string") {
961
+ fail(`"${fieldName}" must be a string.`);
962
+ }
963
+ const trimmedValue = value.trim();
964
+ if (!trimmedValue) {
965
+ fail(`"${fieldName}" must not be empty.`);
966
+ }
967
+ return trimmedValue;
968
+ }
969
+ function getAndroidNotificationChannelImportance(value) {
970
+ return ANDROID_CHANNEL_IMPORTANCE_MAP[value];
971
+ }
972
+ function getDefaultNotificationChannelConfig(options) {
973
+ const runtimeConfig = getBubblesNotificationsRuntimeConfig();
974
+ return {
975
+ channelId: getRequiredNonEmptyString2(
976
+ options?.channelId ?? runtimeConfig.defaultChannelId,
977
+ "channelId"
978
+ ),
979
+ channelName: getRequiredNonEmptyString2(
980
+ options?.channelName ?? runtimeConfig.defaultChannelName,
981
+ "channelName"
982
+ ),
983
+ importance: options?.importance ?? runtimeConfig.androidChannelImportance
984
+ };
985
+ }
986
+ async function ensureDefaultNotificationChannel(options) {
987
+ if (import_react_native.Platform.OS !== "android") {
988
+ return null;
989
+ }
990
+ const channelConfig = getDefaultNotificationChannelConfig(options);
991
+ return Notifications.setNotificationChannelAsync(channelConfig.channelId, {
992
+ name: channelConfig.channelName,
993
+ importance: getAndroidNotificationChannelImportance(channelConfig.importance)
994
+ });
995
+ }
996
+
997
+ // src/background/firebase-message-handler.ts
998
+ var import_messaging = require("@react-native-firebase/messaging");
999
+ var Notifications4 = __toESM(require("expo-notifications"));
1000
+ var import_react_native3 = require("react-native");
1001
+
1002
+ // src/notifications/local-notification.ts
1003
+ var Notifications3 = __toESM(require("expo-notifications"));
1004
+
1005
+ // src/runtime/permissions.ts
1006
+ var Notifications2 = __toESM(require("expo-notifications"));
1007
+ var import_react_native2 = require("react-native");
1008
+
1009
+ // src/internal/platform.ts
1010
+ function isSupportedPlatform(value) {
1011
+ return value === "android" || value === "ios";
1012
+ }
1013
+ function assertSupportedPlatform(value) {
1014
+ if (isSupportedPlatform(value)) {
1015
+ return;
1016
+ }
1017
+ failWithBubblesError(
1018
+ `Unsupported platform: ${value}. This package only supports iOS and Android.`
1019
+ );
1020
+ }
1021
+ function getSupportedPlatform(value) {
1022
+ assertSupportedPlatform(value);
1023
+ return value;
1024
+ }
1025
+
1026
+ // src/runtime/permissions.ts
1027
+ var IOS_GRANTED_AUTHORIZATION_STATUSES = /* @__PURE__ */ new Set([
1028
+ Notifications2.IosAuthorizationStatus.AUTHORIZED,
1029
+ Notifications2.IosAuthorizationStatus.PROVISIONAL,
1030
+ Notifications2.IosAuthorizationStatus.EPHEMERAL
1031
+ ]);
1032
+ function isNotificationPermissionGranted(permissions) {
1033
+ if (import_react_native2.Platform.OS === "ios" && permissions.ios?.status != null) {
1034
+ return IOS_GRANTED_AUTHORIZATION_STATUSES.has(permissions.ios.status);
1035
+ }
1036
+ return permissions.granted;
1037
+ }
1038
+ function describeNotificationPermissionStatus(permissions) {
1039
+ if (import_react_native2.Platform.OS === "ios" && permissions.ios?.status != null) {
1040
+ return Notifications2.IosAuthorizationStatus[permissions.ios.status].toLowerCase();
1041
+ }
1042
+ return permissions.status;
1043
+ }
1044
+ async function getNotificationPermissions(options) {
1045
+ assertSupportedPlatform(import_react_native2.Platform.OS);
1046
+ await ensureDefaultNotificationChannel();
1047
+ const existingPermissions = await Notifications2.getPermissionsAsync();
1048
+ if (isNotificationPermissionGranted(existingPermissions) || options?.requestPermissions === false) {
1049
+ return existingPermissions;
1050
+ }
1051
+ return Notifications2.requestPermissionsAsync(options?.permissionRequestOptions);
1052
+ }
1053
+ async function ensureNotificationPermissions(options) {
1054
+ const permissions = await getNotificationPermissions(options);
1055
+ if (!isNotificationPermissionGranted(permissions)) {
1056
+ failWithBubblesError("Push notification permission was not granted.");
1057
+ }
1058
+ return permissions;
1059
+ }
1060
+
1061
+ // src/notifications/local-notification.ts
1062
+ var DEFAULT_LOCAL_NOTIFICATION_TITLE = "Notification received";
1063
+ var DEFAULT_LOCAL_NOTIFICATION_BODY = "Open the app to view this update.";
1064
+ var DUPLICATE_NOTIFICATION_WINDOW_MS = 5e3;
1065
+ var recentlyScheduledNotificationKeys = /* @__PURE__ */ new Map();
1066
+ function getOptionalNonEmptyString3(value) {
1067
+ if (typeof value !== "string") {
1068
+ return null;
1069
+ }
1070
+ const trimmedValue = value.trim();
1071
+ return trimmedValue.length > 0 ? trimmedValue : null;
1072
+ }
1073
+ function buildDuplicateNotificationKey(payload) {
1074
+ if (payload.notificationId) {
1075
+ return `notification:${payload.notificationId}`;
1076
+ }
1077
+ const dataKeys = Object.keys(payload.data);
1078
+ if (dataKeys.length === 0) {
1079
+ return null;
1080
+ }
1081
+ try {
1082
+ return `data:${JSON.stringify(payload.data)}`;
1083
+ } catch {
1084
+ return null;
1085
+ }
1086
+ }
1087
+ function rememberScheduledNotificationKey(duplicateKey) {
1088
+ const now = Date.now();
1089
+ for (const [key, timestamp] of recentlyScheduledNotificationKeys) {
1090
+ if (now - timestamp > DUPLICATE_NOTIFICATION_WINDOW_MS) {
1091
+ recentlyScheduledNotificationKeys.delete(key);
1092
+ }
1093
+ }
1094
+ const previousTimestamp = recentlyScheduledNotificationKeys.get(duplicateKey);
1095
+ if (previousTimestamp !== void 0 && now - previousTimestamp <= DUPLICATE_NOTIFICATION_WINDOW_MS) {
1096
+ return false;
1097
+ }
1098
+ recentlyScheduledNotificationKeys.set(duplicateKey, now);
1099
+ return true;
1100
+ }
1101
+ async function postLocalNotificationShownDeliveryStatus(options) {
1102
+ try {
1103
+ await postStoredBubblesDeliveryStatus({
1104
+ source: options.source,
1105
+ notificationId: options.payload.notificationId,
1106
+ status: BUBBLES_DELIVERY_STATUSES.notificationShown
1107
+ });
1108
+ } catch (error) {
1109
+ console.error(
1110
+ `[@fishonfire/bubbles-expo] Failed to post ${options.source} shown delivery status.`,
1111
+ error
1112
+ );
1113
+ }
1114
+ }
1115
+ function buildBubblesLocalNotificationRequest(payload, options) {
1116
+ return {
1117
+ content: {
1118
+ title: payload.messageTitle ?? getOptionalNonEmptyString3(options?.title) ?? DEFAULT_LOCAL_NOTIFICATION_TITLE,
1119
+ body: payload.body ?? getOptionalNonEmptyString3(options?.body) ?? DEFAULT_LOCAL_NOTIFICATION_BODY,
1120
+ data: payload.data
1121
+ },
1122
+ // Force app to trigger it 1 second from now instead of null.
1123
+ // This prevents issues during background handling.
1124
+ trigger: {
1125
+ type: Notifications3.SchedulableTriggerInputTypes.TIME_INTERVAL,
1126
+ seconds: 1
1127
+ }
1128
+ };
1129
+ }
1130
+ async function scheduleBubblesLocalNotification(options) {
1131
+ if (!options.skipPermissionCheck) {
1132
+ const permissions = await Notifications3.getPermissionsAsync();
1133
+ const notificationsEnabled = isNotificationPermissionGranted(permissions);
1134
+ if (!notificationsEnabled) {
1135
+ return null;
1136
+ }
1137
+ }
1138
+ const duplicateKey = buildDuplicateNotificationKey(options.payload);
1139
+ if (duplicateKey !== null && !rememberScheduledNotificationKey(duplicateKey)) {
1140
+ return null;
1141
+ }
1142
+ const notificationRequest = buildBubblesLocalNotificationRequest(
1143
+ options.payload,
1144
+ options
1145
+ );
1146
+ const scheduledNotificationId = await Notifications3.scheduleNotificationAsync(
1147
+ notificationRequest
1148
+ );
1149
+ await postLocalNotificationShownDeliveryStatus(options);
1150
+ return scheduledNotificationId;
1151
+ }
1152
+
1153
+ // src/notifications/payload.ts
1154
+ function isPlainObject2(value) {
1155
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1156
+ }
1157
+ function getOptionalNonEmptyString4(value) {
1158
+ if (typeof value !== "string") {
1159
+ return null;
1160
+ }
1161
+ const trimmedValue = value.trim();
1162
+ return trimmedValue.length > 0 ? trimmedValue : null;
1163
+ }
1164
+ function getOptionalIdentifier(value) {
1165
+ if (typeof value === "number") {
1166
+ return Number.isFinite(value) ? String(value) : null;
1167
+ }
1168
+ return getOptionalNonEmptyString4(value);
1169
+ }
1170
+ function getBubblesNotificationData(value) {
1171
+ return isPlainObject2(value) ? value : {};
1172
+ }
1173
+ function getBubblesNotificationDataFromNotification(notification) {
1174
+ return getBubblesNotificationData(notification.request.content.data);
1175
+ }
1176
+ function normalizeBubblesNotificationPayload(value) {
1177
+ const data = getBubblesNotificationData(value);
1178
+ return {
1179
+ data,
1180
+ notificationId: getOptionalIdentifier(data.notification_id),
1181
+ messageTitle: getOptionalNonEmptyString4(data.message_title),
1182
+ body: getOptionalNonEmptyString4(data.body),
1183
+ source: getOptionalNonEmptyString4(data.source),
1184
+ messageId: getOptionalIdentifier(data.message_id),
1185
+ campaignId: getOptionalIdentifier(data.campaign_id),
1186
+ url: getOptionalNonEmptyString4(data.url)
1187
+ };
1188
+ }
1189
+
1190
+ // src/notifications/remote-message.ts
1191
+ function getOptionalNonEmptyString5(value) {
1192
+ if (typeof value !== "string") {
1193
+ return null;
1194
+ }
1195
+ const trimmedValue = value.trim();
1196
+ return trimmedValue.length > 0 ? trimmedValue : null;
1197
+ }
1198
+ function getBubblesNotificationPayloadFromRemoteMessage(remoteMessage) {
1199
+ const payload = normalizeBubblesNotificationPayload(
1200
+ getBubblesNotificationData(remoteMessage.data)
1201
+ );
1202
+ return {
1203
+ ...payload,
1204
+ messageTitle: payload.messageTitle ?? getOptionalNonEmptyString5(remoteMessage.notification?.title),
1205
+ body: payload.body ?? getOptionalNonEmptyString5(remoteMessage.notification?.body)
1206
+ };
1207
+ }
1208
+ function hasFirebaseDisplayNotification(remoteMessage) {
1209
+ return getOptionalNonEmptyString5(remoteMessage.notification?.title) !== null || getOptionalNonEmptyString5(remoteMessage.notification?.body) !== null;
1210
+ }
1211
+
1212
+ // src/background/firebase-message-handler.ts
1213
+ var BACKGROUND_NOTIFICATION_SOURCE = "Firebase background message";
1214
+ var DEFAULT_BACKGROUND_NOTIFICATION_TITLE = "Background notification received";
1215
+ var DEFAULT_BACKGROUND_NOTIFICATION_BODY = "Open the app to view this update.";
1216
+ function supportsFirebaseBackgroundMessages() {
1217
+ return import_react_native3.Platform.OS === "android" || import_react_native3.Platform.OS === "ios";
1218
+ }
1219
+ function supportsBackgroundLocalNotificationPresentation() {
1220
+ return import_react_native3.Platform.OS === "android";
1221
+ }
1222
+ function logBackgroundStatusError(description, error) {
1223
+ console.error(
1224
+ `[@fishonfire/bubbles-expo] Failed to post ${BACKGROUND_NOTIFICATION_SOURCE} ${description} delivery status.`,
1225
+ error
1226
+ );
1227
+ }
1228
+ function getBackgroundNotificationErrorMessage(error) {
1229
+ if (!(error instanceof Error)) {
1230
+ return "Failed to show notification";
1231
+ }
1232
+ return `Failed to show notification: ${getErrorMessage(error)}`;
1233
+ }
1234
+ async function postBackgroundDeliveryStatus(notificationId, status) {
1235
+ try {
1236
+ await postStoredBubblesDeliveryStatus({
1237
+ source: BACKGROUND_NOTIFICATION_SOURCE,
1238
+ notificationId,
1239
+ status
1240
+ });
1241
+ } catch (error) {
1242
+ logBackgroundStatusError(status, error);
1243
+ }
1244
+ }
1245
+ async function postBackgroundDeliveryError(notificationId, errorMessage) {
1246
+ try {
1247
+ await postStoredBubblesDeliveryStatus({
1248
+ source: BACKGROUND_NOTIFICATION_SOURCE,
1249
+ notificationId,
1250
+ error: errorMessage
1251
+ });
1252
+ } catch (error) {
1253
+ logBackgroundStatusError("error", error);
1254
+ }
1255
+ }
1256
+ function postBackgroundNotificationReceived(notificationId) {
1257
+ return postBackgroundDeliveryStatus(
1258
+ notificationId,
1259
+ BUBBLES_DELIVERY_STATUSES.notificationReceived
1260
+ );
1261
+ }
1262
+ function postBackgroundNotificationsDisabled(notificationId) {
1263
+ return postBackgroundDeliveryStatus(
1264
+ notificationId,
1265
+ BUBBLES_DELIVERY_STATUSES.notificationsDisabled
1266
+ );
1267
+ }
1268
+ var hasRegisteredFirebaseBackgroundMessageHandler = false;
1269
+ var handleBubblesFirebaseBackgroundMessage = async (remoteMessage) => {
1270
+ const payload = getBubblesNotificationPayloadFromRemoteMessage(remoteMessage);
1271
+ const permissions = await Notifications4.getPermissionsAsync();
1272
+ const notificationsEnabled = isNotificationPermissionGranted(permissions);
1273
+ if (!notificationsEnabled) {
1274
+ await postBackgroundNotificationsDisabled(payload.notificationId);
1275
+ return;
1276
+ }
1277
+ await postBackgroundNotificationReceived(payload.notificationId);
1278
+ if (hasFirebaseDisplayNotification(remoteMessage)) {
1279
+ return;
1280
+ }
1281
+ if (!supportsBackgroundLocalNotificationPresentation()) {
1282
+ return;
1283
+ }
1284
+ try {
1285
+ const scheduledNotificationId = await scheduleBubblesLocalNotification({
1286
+ source: BACKGROUND_NOTIFICATION_SOURCE,
1287
+ payload,
1288
+ title: DEFAULT_BACKGROUND_NOTIFICATION_TITLE,
1289
+ body: DEFAULT_BACKGROUND_NOTIFICATION_BODY,
1290
+ skipPermissionCheck: true
1291
+ });
1292
+ if (scheduledNotificationId === null) {
1293
+ return;
1294
+ }
1295
+ } catch (error) {
1296
+ await postBackgroundDeliveryError(
1297
+ payload.notificationId,
1298
+ getBackgroundNotificationErrorMessage(error)
1299
+ );
1300
+ throw error;
1301
+ }
1302
+ };
1303
+ function registerBubblesFirebaseBackgroundMessageHandler() {
1304
+ if (hasRegisteredFirebaseBackgroundMessageHandler) {
1305
+ return;
1306
+ }
1307
+ if (!supportsFirebaseBackgroundMessages()) {
1308
+ return;
1309
+ }
1310
+ try {
1311
+ (0, import_messaging.setBackgroundMessageHandler)(
1312
+ (0, import_messaging.getMessaging)(),
1313
+ handleBubblesFirebaseBackgroundMessage
1314
+ );
1315
+ hasRegisteredFirebaseBackgroundMessageHandler = true;
1316
+ } catch (error) {
1317
+ console.error(
1318
+ "[@fishonfire/bubbles-expo] Failed to register Firebase background message handler.",
1319
+ error
1320
+ );
1321
+ }
1322
+ }
1323
+
1324
+ // src/background/setup.ts
1325
+ var hasRegisteredBubblesBackgroundHandlers = false;
1326
+ function registerBubblesBackgroundHandlers() {
1327
+ if (hasRegisteredBubblesBackgroundHandlers) {
1328
+ return;
1329
+ }
1330
+ registerBubblesFirebaseBackgroundMessageHandler();
1331
+ hasRegisteredBubblesBackgroundHandlers = true;
1332
+ }
1333
+
1334
+ // src/runtime/tokens.ts
1335
+ var import_messaging2 = require("@react-native-firebase/messaging");
1336
+ var import_react_native4 = require("react-native");
1337
+ async function getFirebaseMessagingToken(platform) {
1338
+ try {
1339
+ const messagingInstance = (0, import_messaging2.getMessaging)();
1340
+ if (!(0, import_messaging2.isDeviceRegisteredForRemoteMessages)(messagingInstance)) {
1341
+ await (0, import_messaging2.registerDeviceForRemoteMessages)(messagingInstance);
1342
+ }
1343
+ const token = await (0, import_messaging2.getToken)(messagingInstance);
1344
+ return getRequiredNonEmptyStringFromSource(
1345
+ token,
1346
+ `${platform} FCM token from Firebase Messaging`
1347
+ );
1348
+ } catch (error) {
1349
+ const reason = getErrorMessage(error);
1350
+ const platformSpecificSetupHint = platform === "ios" ? " On iOS, also ensure Firebase APNs setup is complete and `expo.ios.googleServicesFile` is configured." : " On Android, ensure `google-services.json` is configured and the native app has been rebuilt.";
1351
+ failWithBubblesError(
1352
+ `Failed to get a ${platform} FCM token from Firebase Messaging. Ensure "@react-native-firebase/app" and "@react-native-firebase/messaging" are installed, the Expo app includes the "@react-native-firebase/messaging" config plugin, and Firebase is configured for this platform.${platformSpecificSetupHint} Original error: ${reason}`
1353
+ );
1354
+ }
1355
+ }
1356
+ async function getDeviceRegistrationState(options) {
1357
+ const platform = getSupportedPlatform(import_react_native4.Platform.OS);
1358
+ const permissions = await getNotificationPermissions(options);
1359
+ const permissionStatus = describeNotificationPermissionStatus(permissions);
1360
+ const notificationsEnabled = isNotificationPermissionGranted(permissions);
1361
+ if (!notificationsEnabled) {
1362
+ return {
1363
+ platform,
1364
+ tokenType: null,
1365
+ token: null,
1366
+ permissionStatus,
1367
+ notificationsEnabled: false
1368
+ };
1369
+ }
1370
+ const token = await getFirebaseMessagingToken(platform);
1371
+ return {
1372
+ platform,
1373
+ tokenType: "fcm",
1374
+ token,
1375
+ permissionStatus,
1376
+ notificationsEnabled: true
1377
+ };
1378
+ }
1379
+ async function getDeviceToken(options) {
1380
+ const registrationState = await getDeviceRegistrationState(options);
1381
+ if (!registrationState.notificationsEnabled || !registrationState.token) {
1382
+ failWithBubblesError("Push notification permission was not granted.");
1383
+ }
1384
+ return {
1385
+ platform: registrationState.platform,
1386
+ tokenType: "fcm",
1387
+ token: registrationState.token
1388
+ };
1389
+ }
1390
+ async function getFCMToken(options) {
1391
+ const tokenResult = await getDeviceToken(options);
1392
+ return tokenResult.token;
1393
+ }
1394
+
1395
+ // src/runtime/BubblesNotificationsProvider.tsx
1396
+ var import_react3 = require("react");
1397
+
1398
+ // src/runtime/context.ts
1399
+ var import_react = require("react");
1400
+ var BubblesNotificationsContext = (0, import_react.createContext)(null);
1401
+
1402
+ // src/runtime/notification-handler.ts
1403
+ var Notifications5 = __toESM(require("expo-notifications"));
1404
+ var DEFAULT_FOREGROUND_PRESENTATION = {
1405
+ shouldShowBanner: true,
1406
+ shouldShowList: true,
1407
+ shouldPlaySound: false,
1408
+ shouldSetBadge: false
1409
+ };
1410
+ function applyBubblesNotificationHandler(options) {
1411
+ Notifications5.setNotificationHandler({
1412
+ handleNotification: async (notification) => {
1413
+ const presentation = {
1414
+ shouldShowBanner: options?.shouldShowBanner ?? DEFAULT_FOREGROUND_PRESENTATION.shouldShowBanner,
1415
+ shouldShowList: options?.shouldShowList ?? DEFAULT_FOREGROUND_PRESENTATION.shouldShowList,
1416
+ shouldPlaySound: options?.shouldPlaySound ?? DEFAULT_FOREGROUND_PRESENTATION.shouldPlaySound,
1417
+ shouldSetBadge: options?.shouldSetBadge ?? DEFAULT_FOREGROUND_PRESENTATION.shouldSetBadge
1418
+ };
1419
+ return presentation;
1420
+ }
1421
+ });
1422
+ }
1423
+ function clearBubblesNotificationHandler() {
1424
+ Notifications5.setNotificationHandler(null);
1425
+ }
1426
+
1427
+ // src/runtime/response-observer.ts
1428
+ var Notifications6 = __toESM(require("expo-notifications"));
1429
+ function buildNotificationResponseKey(response) {
1430
+ const payload = normalizeBubblesNotificationPayload(
1431
+ getBubblesNotificationDataFromNotification(response.notification)
1432
+ );
1433
+ if (payload.notificationId) {
1434
+ return `notification:${payload.notificationId}:action:${response.actionIdentifier}`;
1435
+ }
1436
+ return `request:${response.notification.request.identifier}:action:${response.actionIdentifier}`;
1437
+ }
1438
+ function logNotificationResponseError(description, error) {
1439
+ console.error(
1440
+ `[@fishonfire/bubbles-expo] Failed during notification response ${description}.`,
1441
+ error
1442
+ );
1443
+ }
1444
+ function buildObservedNotificationResponse(response, storedDeviceState) {
1445
+ const payload = normalizeBubblesNotificationPayload(
1446
+ getBubblesNotificationDataFromNotification(response.notification)
1447
+ );
1448
+ return {
1449
+ notificationId: payload.notificationId,
1450
+ event: {
1451
+ notification: response.notification,
1452
+ url: payload.url,
1453
+ notificationId: payload.notificationId,
1454
+ deviceId: storedDeviceState.deviceId,
1455
+ data: payload.data
1456
+ }
1457
+ };
1458
+ }
1459
+ function postNotificationClickedDeliveryStatus(storedDeviceState, notificationId, actionId) {
1460
+ void postStoredBubblesDeliveryStatus({
1461
+ source: "notification response",
1462
+ storedDeviceState,
1463
+ notificationId,
1464
+ status: BUBBLES_DELIVERY_STATUSES.notificationClicked,
1465
+ actionId
1466
+ }).catch((error) => {
1467
+ logNotificationResponseError("delivery-status update", error);
1468
+ });
1469
+ }
1470
+ function handleNotificationResponse(response, options, source) {
1471
+ const storedDeviceState = readStoredDeviceState();
1472
+ const observedResponse = buildObservedNotificationResponse(
1473
+ response,
1474
+ storedDeviceState
1475
+ );
1476
+ postNotificationClickedDeliveryStatus(
1477
+ storedDeviceState,
1478
+ observedResponse.notificationId,
1479
+ response.actionIdentifier
1480
+ );
1481
+ options.onNotificationResponse?.(observedResponse.event);
1482
+ }
1483
+ function processStartupNotificationResponse(processNotificationResponse) {
1484
+ try {
1485
+ const lastResponse = Notifications6.getLastNotificationResponse();
1486
+ if (!lastResponse) {
1487
+ return;
1488
+ }
1489
+ processNotificationResponse(lastResponse);
1490
+ Notifications6.clearLastNotificationResponse();
1491
+ } catch (error) {
1492
+ logNotificationResponseError("initial response handling", error);
1493
+ }
1494
+ }
1495
+ function observeBubblesNotificationResponses(options) {
1496
+ let lastHandledResponseKey = null;
1497
+ const processNotificationResponse = (response) => {
1498
+ const responseKey = buildNotificationResponseKey(response);
1499
+ if (lastHandledResponseKey === responseKey) {
1500
+ return;
1501
+ }
1502
+ lastHandledResponseKey = responseKey;
1503
+ handleNotificationResponse(response, options, "listener");
1504
+ };
1505
+ processStartupNotificationResponse((response) => {
1506
+ const responseKey = buildNotificationResponseKey(response);
1507
+ lastHandledResponseKey = responseKey;
1508
+ handleNotificationResponse(response, options, "startup");
1509
+ });
1510
+ const subscription = Notifications6.addNotificationResponseReceivedListener(
1511
+ processNotificationResponse
1512
+ );
1513
+ return () => {
1514
+ subscription.remove();
1515
+ };
1516
+ }
1517
+
1518
+ // src/runtime/presentation.ts
1519
+ function applyBubblesForegroundPresentation(options) {
1520
+ applyBubblesNotificationHandler(options);
1521
+ }
1522
+ function clearBubblesForegroundPresentation() {
1523
+ clearBubblesNotificationHandler();
1524
+ }
1525
+ function observeBubblesNotificationOpenEvents(options) {
1526
+ return observeBubblesNotificationResponses(options);
1527
+ }
1528
+ async function prepareBubblesNotificationPresentation(options) {
1529
+ return ensureDefaultNotificationChannel(options);
1530
+ }
1531
+
1532
+ // src/runtime/foreground-message-observer.ts
1533
+ var import_messaging3 = require("@react-native-firebase/messaging");
1534
+ var import_react_native5 = require("react-native");
1535
+ function supportsFirebaseForegroundMessages() {
1536
+ return import_react_native5.Platform.OS === "android" || import_react_native5.Platform.OS === "ios";
1537
+ }
1538
+ async function handleFirebaseForegroundMessage(remoteMessage) {
1539
+ const payload = getBubblesNotificationPayloadFromRemoteMessage(remoteMessage);
1540
+ if (!hasFirebaseDisplayNotification(remoteMessage)) {
1541
+ return;
1542
+ }
1543
+ await scheduleBubblesLocalNotification({
1544
+ source: "Firebase foreground message",
1545
+ payload
1546
+ });
1547
+ }
1548
+ function observeBubblesForegroundMessages() {
1549
+ if (!supportsFirebaseForegroundMessages()) {
1550
+ return () => {
1551
+ };
1552
+ }
1553
+ const unsubscribe = (0, import_messaging3.onMessage)(
1554
+ (0, import_messaging3.getMessaging)(),
1555
+ (remoteMessage) => {
1556
+ void handleFirebaseForegroundMessage(remoteMessage).catch(
1557
+ (error) => {
1558
+ console.error(
1559
+ "[@fishonfire/bubbles-expo] Failed while handling a Firebase foreground message.",
1560
+ error
1561
+ );
1562
+ }
1563
+ );
1564
+ }
1565
+ );
1566
+ return () => {
1567
+ unsubscribe();
1568
+ };
1569
+ }
1570
+
1571
+ // src/runtime/installations.ts
1572
+ var import_installations = require("@react-native-firebase/installations");
1573
+ async function getFirebaseInstallationId(platform) {
1574
+ try {
1575
+ const installationsInstance = (0, import_installations.getInstallations)();
1576
+ const installationId = await (0, import_installations.getId)(installationsInstance);
1577
+ return getRequiredNonEmptyStringFromSource(
1578
+ installationId,
1579
+ `${platform} Firebase installation id from Firebase Installations`
1580
+ );
1581
+ } catch (error) {
1582
+ const reason = getErrorMessage(error);
1583
+ const platformSpecificSetupHint = platform === "ios" ? " On iOS, ensure `expo.ios.googleServicesFile` is configured and the native app has been rebuilt." : " On Android, ensure `expo.android.googleServicesFile` is configured and the native app has been rebuilt.";
1584
+ failWithBubblesError(
1585
+ `Failed to get a ${platform} Firebase installation id from Firebase Installations. Ensure "@react-native-firebase/app" and "@react-native-firebase/installations" are installed and Firebase is configured for this platform.${platformSpecificSetupHint} Original error: ${reason}`
1586
+ );
1587
+ }
1588
+ }
1589
+
1590
+ // src/runtime/transport.ts
1591
+ function observeBubblesForegroundRemoteMessages() {
1592
+ return observeBubblesForegroundMessages();
1593
+ }
1594
+ async function getBubblesDeviceRegistrationState(options) {
1595
+ return getDeviceRegistrationState(options);
1596
+ }
1597
+ async function getBubblesInstallationId(platform) {
1598
+ return getFirebaseInstallationId(platform);
1599
+ }
1600
+
1601
+ // src/runtime/sync-device.ts
1602
+ var BubblesNotificationsSyncError = class extends Error {
1603
+ constructor(message, snapshot, cause) {
1604
+ super(message);
1605
+ this.name = "BubblesNotificationsSyncError";
1606
+ this.snapshot = snapshot;
1607
+ this.cause = cause;
1608
+ }
1609
+ };
1610
+ function fail2(message) {
1611
+ throw new Error(`[@fishonfire/bubbles-expo] ${message}`);
1612
+ }
1613
+ function toError(error) {
1614
+ if (error instanceof Error) {
1615
+ return error;
1616
+ }
1617
+ return new Error(String(error));
1618
+ }
1619
+ function getRequiredNonEmptyString3(value, fieldName) {
1620
+ if (typeof value !== "string") {
1621
+ fail2(`"${fieldName}" must be a string.`);
1622
+ }
1623
+ const trimmedValue = value.trim();
1624
+ if (!trimmedValue) {
1625
+ fail2(`"${fieldName}" must not be empty.`);
1626
+ }
1627
+ return trimmedValue;
1628
+ }
1629
+ function buildRuntimeSnapshot(deviceId, registrationState) {
1630
+ return {
1631
+ deviceId,
1632
+ pushToken: registrationState.token,
1633
+ tokenType: registrationState.tokenType,
1634
+ permissionStatus: registrationState.permissionStatus,
1635
+ notificationsEnabled: registrationState.notificationsEnabled
1636
+ };
1637
+ }
1638
+ async function syncDeviceRegistrationState(options) {
1639
+ const apiBaseUrl = getRequiredNonEmptyString3(options.apiBaseUrl, "apiBaseUrl");
1640
+ const appKey = getRequiredNonEmptyString3(options.appKey, "appKey");
1641
+ storeApiBaseUrl(apiBaseUrl);
1642
+ storeAppKey(appKey);
1643
+ const snapshot = buildRuntimeSnapshot(
1644
+ options.deviceId,
1645
+ options.registrationState
1646
+ );
1647
+ try {
1648
+ const installationId = options.registrationState.notificationsEnabled ? await getBubblesInstallationId(options.registrationState.platform) : null;
1649
+ const syncResult = await syncBubblesDevice({
1650
+ apiBaseUrl,
1651
+ appId: options.appId,
1652
+ appKey,
1653
+ userId: options.userId,
1654
+ aliasing: options.aliasing,
1655
+ appVersion: options.appVersion,
1656
+ deviceId: options.deviceId,
1657
+ platform: options.registrationState.platform,
1658
+ pushToken: options.registrationState.token,
1659
+ fid: installationId,
1660
+ notificationsEnabled: options.registrationState.notificationsEnabled
1661
+ });
1662
+ const nextDeviceId = syncResult.deviceId ?? options.deviceId;
1663
+ storeDeviceId(nextDeviceId);
1664
+ return {
1665
+ ...snapshot,
1666
+ action: syncResult.action,
1667
+ deviceId: nextDeviceId
1668
+ };
1669
+ } catch (error) {
1670
+ const normalizedError = toError(error);
1671
+ throw new BubblesNotificationsSyncError(
1672
+ normalizedError.message,
1673
+ snapshot,
1674
+ error
1675
+ );
1676
+ }
1677
+ }
1678
+ async function syncExistingBubblesDevice(options) {
1679
+ const registrationState = await getBubblesDeviceRegistrationState({
1680
+ requestPermissions: false
1681
+ });
1682
+ return syncDeviceRegistrationState({
1683
+ ...options,
1684
+ deviceId: options.deviceId,
1685
+ registrationState
1686
+ });
1687
+ }
1688
+
1689
+ // src/runtime/register-device.ts
1690
+ var DEFAULT_PERMISSION_REQUEST_OPTIONS = {
1691
+ ios: {
1692
+ allowAlert: true,
1693
+ allowBadge: true,
1694
+ allowSound: true
1695
+ }
1696
+ };
1697
+ function buildRegistrationOptions(options) {
1698
+ if (options?.requestPermissions === false) {
1699
+ return {
1700
+ requestPermissions: false,
1701
+ permissionRequestOptions: options.permissionRequestOptions
1702
+ };
1703
+ }
1704
+ return {
1705
+ requestPermissions: options?.requestPermissions,
1706
+ permissionRequestOptions: options?.permissionRequestOptions ?? DEFAULT_PERMISSION_REQUEST_OPTIONS
1707
+ };
1708
+ }
1709
+ async function registerBubblesDevice(options) {
1710
+ const registrationState = await getBubblesDeviceRegistrationState(
1711
+ buildRegistrationOptions(options.registrationOptions)
1712
+ );
1713
+ return syncDeviceRegistrationState({
1714
+ ...options,
1715
+ deviceId: options.deviceId,
1716
+ registrationState
1717
+ });
1718
+ }
1719
+
1720
+ // src/runtime/provider-operation-queue.ts
1721
+ var import_react2 = require("react");
1722
+
1723
+ // src/runtime/provider-state.ts
1724
+ function createInitialRuntimeState(deviceId, error) {
1725
+ return {
1726
+ deviceId,
1727
+ pushToken: null,
1728
+ tokenType: null,
1729
+ permissionStatus: "unknown",
1730
+ notificationsEnabled: false,
1731
+ isSyncing: false,
1732
+ error
1733
+ };
1734
+ }
1735
+ function normalizeProviderError(error) {
1736
+ if (error instanceof Error) {
1737
+ return error;
1738
+ }
1739
+ return new Error(String(error));
1740
+ }
1741
+ function normalizeUserId(value) {
1742
+ if (value === null) {
1743
+ return null;
1744
+ }
1745
+ const trimmedValue = value.trim();
1746
+ return trimmedValue.length > 0 ? trimmedValue : null;
1747
+ }
1748
+ function getAutomaticSyncSignature(input) {
1749
+ return JSON.stringify({
1750
+ appId: input.appId,
1751
+ appKey: input.appKey,
1752
+ apiBaseUrl: input.apiBaseUrl,
1753
+ userId: input.userId,
1754
+ aliasing: input.aliasing ?? [],
1755
+ appVersion: input.appVersion ?? null,
1756
+ deviceId: input.deviceId
1757
+ });
1758
+ }
1759
+ function getRuntimeStateUpdate(snapshot) {
1760
+ return {
1761
+ deviceId: snapshot.deviceId,
1762
+ pushToken: snapshot.pushToken,
1763
+ tokenType: snapshot.tokenType,
1764
+ permissionStatus: snapshot.permissionStatus,
1765
+ notificationsEnabled: snapshot.notificationsEnabled
1766
+ };
1767
+ }
1768
+ function getInitialRuntimeState() {
1769
+ try {
1770
+ return createInitialRuntimeState(readStoredDeviceId(), null);
1771
+ } catch (error) {
1772
+ return createInitialRuntimeState(
1773
+ null,
1774
+ normalizeProviderError(error)
1775
+ );
1776
+ }
1777
+ }
1778
+
1779
+ // src/runtime/provider-operation-queue.ts
1780
+ function useBubblesRuntimeOperationQueue(setState) {
1781
+ const isMountedRef = (0, import_react2.useRef)(true);
1782
+ const pendingOperationsRef = (0, import_react2.useRef)(0);
1783
+ const operationQueueRef = (0, import_react2.useRef)(Promise.resolve());
1784
+ const enqueueOperation = (0, import_react2.useCallback)((operation) => {
1785
+ pendingOperationsRef.current += 1;
1786
+ setState((currentState) => ({
1787
+ ...currentState,
1788
+ isSyncing: true,
1789
+ error: null
1790
+ }));
1791
+ const queuedOperation = operationQueueRef.current.catch(() => void 0).then(operation);
1792
+ operationQueueRef.current = queuedOperation.then(
1793
+ () => void 0,
1794
+ () => void 0
1795
+ );
1796
+ return queuedOperation.then(
1797
+ (result) => {
1798
+ pendingOperationsRef.current -= 1;
1799
+ if (!isMountedRef.current) {
1800
+ return;
1801
+ }
1802
+ setState((currentState) => ({
1803
+ ...currentState,
1804
+ ...result ?? {},
1805
+ isSyncing: pendingOperationsRef.current > 0,
1806
+ error: null
1807
+ }));
1808
+ },
1809
+ (error) => {
1810
+ pendingOperationsRef.current -= 1;
1811
+ const normalizedError = normalizeProviderError(error);
1812
+ const snapshot = error instanceof BubblesNotificationsSyncError ? error.snapshot : void 0;
1813
+ if (isMountedRef.current) {
1814
+ setState((currentState) => ({
1815
+ ...currentState,
1816
+ ...snapshot ? getRuntimeStateUpdate(snapshot) : {},
1817
+ isSyncing: pendingOperationsRef.current > 0,
1818
+ error: normalizedError
1819
+ }));
1820
+ }
1821
+ throw normalizedError;
1822
+ }
1823
+ );
1824
+ }, [setState]);
1825
+ return {
1826
+ enqueueOperation,
1827
+ isMountedRef
1828
+ };
1829
+ }
1830
+
1831
+ // src/runtime/BubblesNotificationsProvider.tsx
1832
+ var import_jsx_runtime = require("react/jsx-runtime");
1833
+ function BubblesNotificationsProvider(props) {
1834
+ const {
1835
+ appId,
1836
+ appKey,
1837
+ apiBaseUrl,
1838
+ ready,
1839
+ userId,
1840
+ aliasing,
1841
+ appVersion,
1842
+ onLogout,
1843
+ foregroundPresentation,
1844
+ onNotificationResponse,
1845
+ children
1846
+ } = props;
1847
+ const normalizedUserId = normalizeUserId(userId);
1848
+ const [state, setState] = (0, import_react3.useState)(getInitialRuntimeState);
1849
+ const { enqueueOperation, isMountedRef } = useBubblesRuntimeOperationQueue(setState);
1850
+ const lastAutomaticSyncSignatureRef = (0, import_react3.useRef)(null);
1851
+ const previousUserIdRef = (0, import_react3.useRef)(normalizedUserId);
1852
+ const buildAutomaticSyncSignature = (0, import_react3.useCallback)((deviceId) => {
1853
+ return getAutomaticSyncSignature({
1854
+ appId,
1855
+ appKey,
1856
+ apiBaseUrl,
1857
+ userId: normalizedUserId,
1858
+ aliasing,
1859
+ appVersion,
1860
+ deviceId
1861
+ });
1862
+ }, [
1863
+ appId,
1864
+ appKey,
1865
+ apiBaseUrl,
1866
+ normalizedUserId,
1867
+ aliasing,
1868
+ appVersion
1869
+ ]);
1870
+ (0, import_react3.useEffect)(() => {
1871
+ return () => {
1872
+ isMountedRef.current = false;
1873
+ clearBubblesForegroundPresentation();
1874
+ };
1875
+ }, [isMountedRef]);
1876
+ (0, import_react3.useEffect)(() => {
1877
+ let isCancelled = false;
1878
+ void (async () => {
1879
+ try {
1880
+ storeApiBaseUrl(getRequiredNonEmptyString(apiBaseUrl, "apiBaseUrl"));
1881
+ storeAppKey(getRequiredNonEmptyString(appKey, "appKey"));
1882
+ await prepareBubblesNotificationPresentation();
1883
+ } catch (error) {
1884
+ if (!isCancelled && isMountedRef.current) {
1885
+ setState((currentState) => ({
1886
+ ...currentState,
1887
+ error: normalizeProviderError(error)
1888
+ }));
1889
+ }
1890
+ }
1891
+ })();
1892
+ return () => {
1893
+ isCancelled = true;
1894
+ };
1895
+ }, [apiBaseUrl, appKey, isMountedRef]);
1896
+ (0, import_react3.useEffect)(() => {
1897
+ applyBubblesForegroundPresentation(foregroundPresentation);
1898
+ }, [foregroundPresentation]);
1899
+ (0, import_react3.useEffect)(() => {
1900
+ return observeBubblesForegroundRemoteMessages();
1901
+ }, []);
1902
+ (0, import_react3.useEffect)(() => {
1903
+ return observeBubblesNotificationOpenEvents({
1904
+ onNotificationResponse
1905
+ });
1906
+ }, [onNotificationResponse]);
1907
+ (0, import_react3.useEffect)(() => {
1908
+ const previousUserId = previousUserIdRef.current;
1909
+ if (previousUserId !== null && normalizedUserId === null) {
1910
+ lastAutomaticSyncSignatureRef.current = null;
1911
+ if (onLogout) {
1912
+ void enqueueOperation(async () => {
1913
+ await onLogout({
1914
+ previousUserId,
1915
+ deviceId: state.deviceId
1916
+ });
1917
+ });
1918
+ }
1919
+ }
1920
+ previousUserIdRef.current = normalizedUserId;
1921
+ }, [enqueueOperation, normalizedUserId, onLogout, state.deviceId]);
1922
+ (0, import_react3.useEffect)(() => {
1923
+ if (!ready || normalizedUserId === null || state.deviceId === null) {
1924
+ lastAutomaticSyncSignatureRef.current = null;
1925
+ return;
1926
+ }
1927
+ const syncSignature = buildAutomaticSyncSignature(state.deviceId);
1928
+ if (lastAutomaticSyncSignatureRef.current === syncSignature) {
1929
+ return;
1930
+ }
1931
+ lastAutomaticSyncSignatureRef.current = syncSignature;
1932
+ const storedDeviceId = state.deviceId;
1933
+ void enqueueOperation(async () => {
1934
+ const result = await syncExistingBubblesDevice({
1935
+ appId,
1936
+ appKey,
1937
+ apiBaseUrl,
1938
+ userId: normalizedUserId,
1939
+ aliasing,
1940
+ appVersion,
1941
+ deviceId: storedDeviceId
1942
+ });
1943
+ return getRuntimeStateUpdate(result);
1944
+ });
1945
+ }, [
1946
+ ready,
1947
+ normalizedUserId,
1948
+ state.deviceId,
1949
+ appId,
1950
+ appKey,
1951
+ apiBaseUrl,
1952
+ aliasing,
1953
+ appVersion,
1954
+ buildAutomaticSyncSignature,
1955
+ enqueueOperation
1956
+ ]);
1957
+ function registerDevice(options) {
1958
+ return enqueueOperation(async () => {
1959
+ if (!ready) {
1960
+ failWithBubblesError(
1961
+ '"registerDevice()" requires "ready" to be true before the device can be synced.'
1962
+ );
1963
+ }
1964
+ if (normalizedUserId === null) {
1965
+ failWithBubblesError(
1966
+ '"registerDevice()" requires a non-null "userId" before the device can be synced.'
1967
+ );
1968
+ }
1969
+ const result = await registerBubblesDevice({
1970
+ appId,
1971
+ appKey,
1972
+ apiBaseUrl,
1973
+ userId: normalizedUserId,
1974
+ aliasing,
1975
+ appVersion,
1976
+ deviceId: state.deviceId,
1977
+ registrationOptions: options
1978
+ });
1979
+ if (result.deviceId !== null) {
1980
+ lastAutomaticSyncSignatureRef.current = buildAutomaticSyncSignature(
1981
+ result.deviceId
1982
+ );
1983
+ }
1984
+ return getRuntimeStateUpdate(result);
1985
+ });
1986
+ }
1987
+ const contextValue = {
1988
+ registerDevice,
1989
+ deviceId: state.deviceId,
1990
+ pushToken: state.pushToken,
1991
+ tokenType: state.tokenType,
1992
+ permissionStatus: state.permissionStatus,
1993
+ notificationsEnabled: state.notificationsEnabled,
1994
+ isSyncing: state.isSyncing,
1995
+ error: state.error
1996
+ };
1997
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BubblesNotificationsContext.Provider, { value: contextValue, children });
1998
+ }
1999
+
2000
+ // src/runtime/useBubblesNotifications.ts
2001
+ var import_react4 = require("react");
2002
+ function fail3(message) {
2003
+ throw new Error(`[@fishonfire/bubbles-expo] ${message}`);
2004
+ }
2005
+ function useBubblesNotifications() {
2006
+ const context = (0, import_react4.useContext)(BubblesNotificationsContext);
2007
+ if (context === null) {
2008
+ fail3(
2009
+ '"useBubblesNotifications()" must be used inside "BubblesNotificationsProvider".'
2010
+ );
2011
+ }
2012
+ return context;
2013
+ }
2014
+ // Annotate the CommonJS export names for ESM import in node:
2015
+ 0 && (module.exports = {
2016
+ ANDROID_CHANNEL_IMPORTANCE_VALUES,
2017
+ BUBBLES_DELIVERY_STATUSES,
2018
+ BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY,
2019
+ BubblesAppAuthenticationError,
2020
+ BubblesNotificationsProvider,
2021
+ buildBubblesDeliveryStatusPayload,
2022
+ buildCreateDevicePayload,
2023
+ buildStoredDeliveryStatusEventKey,
2024
+ buildUpdateDevicePayload,
2025
+ createBubblesDevice,
2026
+ createBubblesDeviceClient,
2027
+ createStoredDeliveryStatusEvent,
2028
+ describeNotificationPermissionStatus,
2029
+ enqueueStoredDeliveryStatusEvent,
2030
+ ensureDefaultNotificationChannel,
2031
+ ensureNotificationPermissions,
2032
+ extractDeviceIdFromDeviceResponse,
2033
+ flushStoredBubblesDeliveryStatuses,
2034
+ getAndroidNotificationChannelImportance,
2035
+ getBubblesNotificationsRuntimeConfig,
2036
+ getDefaultNotificationChannelConfig,
2037
+ getDeviceRegistrationState,
2038
+ getDeviceToken,
2039
+ getFCMToken,
2040
+ getNotificationPermissions,
2041
+ isNotificationPermissionGranted,
2042
+ markStoredDeliveryStatusEventPosted,
2043
+ patchStoredDeviceState,
2044
+ postBubblesDeliveryStatus,
2045
+ postStoredBubblesDeliveryStatus,
2046
+ readStoredApiBaseUrl,
2047
+ readStoredAppKey,
2048
+ readStoredDeliveryStatusLedger,
2049
+ readStoredDeviceId,
2050
+ readStoredDeviceState,
2051
+ registerBubblesBackgroundHandlers,
2052
+ storeApiBaseUrl,
2053
+ storeAppKey,
2054
+ storeDeliveryStatusLedger,
2055
+ storeDeviceId,
2056
+ storeDeviceState,
2057
+ syncBubblesDevice,
2058
+ updateBubblesDevice,
2059
+ useBubblesNotifications
2060
+ });