@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.
@@ -0,0 +1,1308 @@
1
+ // src/config/runtime-config.ts
2
+ import Constants from "expo-constants";
3
+
4
+ // plugin/src/shared-config.ts
5
+ var BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY = "bubblesNotificationsExpo";
6
+ var ANDROID_CHANNEL_IMPORTANCE_VALUES = [
7
+ "min",
8
+ "low",
9
+ "default",
10
+ "high",
11
+ "max"
12
+ ];
13
+
14
+ // src/internal/errors.ts
15
+ function createBubblesError(message) {
16
+ return new Error(`[@fishonfire/bubbles-expo] ${message}`);
17
+ }
18
+ function failWithBubblesError(message) {
19
+ throw createBubblesError(message);
20
+ }
21
+ function getErrorMessage(error) {
22
+ return error instanceof Error ? error.message : String(error);
23
+ }
24
+
25
+ // src/internal/validation.ts
26
+ function isPlainObject(value) {
27
+ return value !== null && typeof value === "object" && !Array.isArray(value);
28
+ }
29
+ function getRequiredNonEmptyString(value, fieldName) {
30
+ if (typeof value !== "string") {
31
+ failWithBubblesError(`"${fieldName}" must be a string.`);
32
+ }
33
+ const trimmedValue = value.trim();
34
+ if (!trimmedValue) {
35
+ failWithBubblesError(`"${fieldName}" must not be empty.`);
36
+ }
37
+ return trimmedValue;
38
+ }
39
+ function getRequiredNonEmptyStringFromSource(value, sourceDescription) {
40
+ if (typeof value !== "string") {
41
+ failWithBubblesError(`${sourceDescription} must be a string.`);
42
+ }
43
+ const trimmedValue = value.trim();
44
+ if (!trimmedValue) {
45
+ failWithBubblesError(`${sourceDescription} must not be empty.`);
46
+ }
47
+ return trimmedValue;
48
+ }
49
+ function normalizeIdentifier(value, fieldName) {
50
+ if (typeof value === "number") {
51
+ if (!Number.isFinite(value)) {
52
+ failWithBubblesError(
53
+ `"${fieldName}" must be a finite number when it is numeric.`
54
+ );
55
+ }
56
+ return String(value);
57
+ }
58
+ if (typeof value !== "string") {
59
+ failWithBubblesError(`"${fieldName}" must be a string or number.`);
60
+ }
61
+ const trimmedValue = value.trim();
62
+ if (!trimmedValue) {
63
+ failWithBubblesError(`"${fieldName}" must not be empty.`);
64
+ }
65
+ return trimmedValue;
66
+ }
67
+
68
+ // src/config/runtime-config.ts
69
+ var BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY2 = BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY;
70
+ var ANDROID_CHANNEL_IMPORTANCE_VALUES2 = ANDROID_CHANNEL_IMPORTANCE_VALUES;
71
+ function getAndroidChannelImportance(value, propertyPath) {
72
+ if (typeof value !== "string") {
73
+ failWithBubblesError(
74
+ `"${propertyPath}" must be one of: ${ANDROID_CHANNEL_IMPORTANCE_VALUES2.join(
75
+ ", "
76
+ )}.`
77
+ );
78
+ }
79
+ if (!ANDROID_CHANNEL_IMPORTANCE_VALUES2.includes(
80
+ value
81
+ )) {
82
+ failWithBubblesError(
83
+ `"${propertyPath}" must be one of: ${ANDROID_CHANNEL_IMPORTANCE_VALUES2.join(
84
+ ", "
85
+ )}.`
86
+ );
87
+ }
88
+ return value;
89
+ }
90
+ function getBubblesNotificationsRuntimeConfig() {
91
+ const expoConfig = Constants.expoConfig;
92
+ if (!expoConfig) {
93
+ failWithBubblesError(
94
+ "Expo runtime config is unavailable. Ensure the app is running with Expo config support."
95
+ );
96
+ }
97
+ if (!isPlainObject(expoConfig.extra)) {
98
+ failWithBubblesError(
99
+ `Missing "expo.extra.${BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY2}". Configure the "@fishonfire/bubbles-expo" plugin in app.json.`
100
+ );
101
+ }
102
+ const runtimeConfig = expoConfig.extra[BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY2];
103
+ if (!isPlainObject(runtimeConfig)) {
104
+ failWithBubblesError(
105
+ `Missing "expo.extra.${BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY2}". Configure the "@fishonfire/bubbles-expo" plugin in app.json.`
106
+ );
107
+ }
108
+ return {
109
+ defaultChannelId: getRequiredNonEmptyString(
110
+ runtimeConfig.defaultChannelId,
111
+ `expo.extra.${BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY2}.defaultChannelId`
112
+ ),
113
+ defaultChannelName: getRequiredNonEmptyString(
114
+ runtimeConfig.defaultChannelName,
115
+ `expo.extra.${BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY2}.defaultChannelName`
116
+ ),
117
+ androidChannelImportance: getAndroidChannelImportance(
118
+ runtimeConfig.androidChannelImportance,
119
+ `expo.extra.${BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY2}.androidChannelImportance`
120
+ )
121
+ };
122
+ }
123
+
124
+ // src/storage/device-state.ts
125
+ import { File, Paths } from "expo-file-system";
126
+ var STORED_DEVICE_STATE_FILE_NAME = "bubbles-notifications-expo-state.json";
127
+ var LEGACY_DEVICE_ID_FILE_NAME = "device-id.txt";
128
+ var STORED_DEVICE_STATE_VERSION = 1;
129
+ function getOptionalNonEmptyString(value, fieldName) {
130
+ if (value == null) {
131
+ return null;
132
+ }
133
+ if (typeof value !== "string") {
134
+ failWithBubblesError(`Stored "${fieldName}" must be a string when present.`);
135
+ }
136
+ const trimmedValue = value.trim();
137
+ return trimmedValue.length > 0 ? trimmedValue : null;
138
+ }
139
+ function getStoredDeviceStateFile() {
140
+ return new File(Paths.document, STORED_DEVICE_STATE_FILE_NAME);
141
+ }
142
+ function getLegacyDeviceIdFile() {
143
+ return new File(Paths.document, LEGACY_DEVICE_ID_FILE_NAME);
144
+ }
145
+ function readLegacyStoredDeviceId() {
146
+ const legacyDeviceIdFile = getLegacyDeviceIdFile();
147
+ if (!legacyDeviceIdFile.exists) {
148
+ return null;
149
+ }
150
+ const storedDeviceId = legacyDeviceIdFile.textSync().trim();
151
+ return storedDeviceId.length > 0 ? storedDeviceId : null;
152
+ }
153
+ function normalizeStoredDeviceState(value, sourceDescription) {
154
+ if (!isPlainObject(value)) {
155
+ failWithBubblesError(
156
+ `Stored device state in ${sourceDescription} must be a JSON object.`
157
+ );
158
+ }
159
+ if (value.version !== STORED_DEVICE_STATE_VERSION) {
160
+ failWithBubblesError(
161
+ `Stored device state in ${sourceDescription} uses unsupported schema version "${String(
162
+ value.version
163
+ )}".`
164
+ );
165
+ }
166
+ return {
167
+ deviceId: getOptionalNonEmptyString(value.deviceId, "deviceId"),
168
+ apiBaseUrl: getOptionalNonEmptyString(value.apiBaseUrl, "apiBaseUrl"),
169
+ appKey: getOptionalNonEmptyString(value.appKey, "appKey")
170
+ };
171
+ }
172
+ function normalizeStoredDeviceStateInput(value) {
173
+ if (!isPlainObject(value)) {
174
+ failWithBubblesError("Stored device state must be an object.");
175
+ }
176
+ return {
177
+ deviceId: getOptionalNonEmptyString(value.deviceId, "deviceId"),
178
+ apiBaseUrl: getOptionalNonEmptyString(value.apiBaseUrl, "apiBaseUrl"),
179
+ appKey: getOptionalNonEmptyString(value.appKey, "appKey")
180
+ };
181
+ }
182
+ function normalizeStoredDeviceStateUpdate(value) {
183
+ if (!isPlainObject(value)) {
184
+ failWithBubblesError("Stored device state update must be an object.");
185
+ }
186
+ const nextState = {};
187
+ if ("deviceId" in value) {
188
+ nextState.deviceId = getOptionalNonEmptyString(value.deviceId, "deviceId");
189
+ }
190
+ if ("apiBaseUrl" in value) {
191
+ nextState.apiBaseUrl = getOptionalNonEmptyString(
192
+ value.apiBaseUrl,
193
+ "apiBaseUrl"
194
+ );
195
+ }
196
+ if ("appKey" in value) {
197
+ nextState.appKey = getOptionalNonEmptyString(value.appKey, "appKey");
198
+ }
199
+ return nextState;
200
+ }
201
+ function serializeStoredDeviceState(state) {
202
+ const serializedState = {
203
+ version: STORED_DEVICE_STATE_VERSION
204
+ };
205
+ if (state.deviceId) {
206
+ serializedState.deviceId = state.deviceId;
207
+ }
208
+ if (state.apiBaseUrl) {
209
+ serializedState.apiBaseUrl = state.apiBaseUrl;
210
+ }
211
+ if (state.appKey) {
212
+ serializedState.appKey = state.appKey;
213
+ }
214
+ return serializedState;
215
+ }
216
+ function readStoredDeviceStateFileText() {
217
+ const storedDeviceStateFile = getStoredDeviceStateFile();
218
+ if (!storedDeviceStateFile.exists) {
219
+ return null;
220
+ }
221
+ const contents = storedDeviceStateFile.textSync().trim();
222
+ return contents.length > 0 ? contents : null;
223
+ }
224
+ function ensureStoredDeviceStateFile() {
225
+ const storedDeviceStateFile = getStoredDeviceStateFile();
226
+ if (!storedDeviceStateFile.exists) {
227
+ storedDeviceStateFile.create({ intermediates: true });
228
+ }
229
+ return storedDeviceStateFile;
230
+ }
231
+ function withLegacyDeviceIdFallback(state) {
232
+ if (state.deviceId) {
233
+ return state;
234
+ }
235
+ const legacyDeviceId = readLegacyStoredDeviceId();
236
+ if (!legacyDeviceId) {
237
+ return state;
238
+ }
239
+ return {
240
+ ...state,
241
+ deviceId: legacyDeviceId
242
+ };
243
+ }
244
+ function readStoredDeviceState() {
245
+ const storedDeviceStateFile = getStoredDeviceStateFile();
246
+ const rawState = readStoredDeviceStateFileText();
247
+ if (rawState === null) {
248
+ return withLegacyDeviceIdFallback({
249
+ deviceId: null,
250
+ apiBaseUrl: null,
251
+ appKey: null
252
+ });
253
+ }
254
+ let parsedState;
255
+ try {
256
+ parsedState = JSON.parse(rawState);
257
+ } catch {
258
+ failWithBubblesError(
259
+ `Stored device state at "${storedDeviceStateFile.uri}" is not valid JSON.`
260
+ );
261
+ }
262
+ return withLegacyDeviceIdFallback(
263
+ normalizeStoredDeviceState(parsedState, `"${storedDeviceStateFile.uri}"`)
264
+ );
265
+ }
266
+ function storeDeviceState(state) {
267
+ const normalizedState = normalizeStoredDeviceStateInput(state);
268
+ const storedDeviceStateFile = ensureStoredDeviceStateFile();
269
+ storedDeviceStateFile.write(
270
+ JSON.stringify(serializeStoredDeviceState(normalizedState), null, 2)
271
+ );
272
+ return normalizedState;
273
+ }
274
+ function patchStoredDeviceState(update) {
275
+ const normalizedUpdate = normalizeStoredDeviceStateUpdate(update);
276
+ return storeDeviceState({
277
+ ...readStoredDeviceState(),
278
+ ...normalizedUpdate
279
+ });
280
+ }
281
+ function readStoredDeviceId() {
282
+ return readStoredDeviceState().deviceId;
283
+ }
284
+ function readStoredApiBaseUrl() {
285
+ return readStoredDeviceState().apiBaseUrl;
286
+ }
287
+ function readStoredAppKey() {
288
+ return readStoredDeviceState().appKey;
289
+ }
290
+ function storeDeviceId(deviceId) {
291
+ return patchStoredDeviceState({ deviceId });
292
+ }
293
+ function storeApiBaseUrl(apiBaseUrl) {
294
+ return patchStoredDeviceState({ apiBaseUrl });
295
+ }
296
+ function storeAppKey(appKey) {
297
+ return patchStoredDeviceState({ appKey });
298
+ }
299
+
300
+ // src/api/device-payload.ts
301
+ import {
302
+ getLocaleAndTimeZone
303
+ } from "@fishonfire/bubbles-js";
304
+ function normalizeOptionalString(value, fieldName) {
305
+ if (value == null) {
306
+ return null;
307
+ }
308
+ if (typeof value !== "string") {
309
+ failWithBubblesError(`"${fieldName}" must be a string when present.`);
310
+ }
311
+ const trimmedValue = value.trim();
312
+ return trimmedValue.length > 0 ? trimmedValue : null;
313
+ }
314
+ function normalizeRequiredNullableString(value, fieldName) {
315
+ if (value === null) {
316
+ return null;
317
+ }
318
+ if (value === void 0) {
319
+ failWithBubblesError(
320
+ `"${fieldName}" is required. Pass a push token string or null.`
321
+ );
322
+ }
323
+ return getRequiredNonEmptyString(value, fieldName);
324
+ }
325
+ function normalizeAppId(value) {
326
+ if (typeof value === "number") {
327
+ if (!Number.isFinite(value)) {
328
+ failWithBubblesError('"appId" must be a finite number.');
329
+ }
330
+ return value;
331
+ }
332
+ if (typeof value === "string") {
333
+ return getRequiredNonEmptyString(value, "appId");
334
+ }
335
+ failWithBubblesError('"appId" must be a string or number.');
336
+ }
337
+ function normalizePlatform(value) {
338
+ if (value === "android" || value === "ios") {
339
+ return value;
340
+ }
341
+ failWithBubblesError('"platform" must be either "android" or "ios".');
342
+ }
343
+ function normalizeAliasing(value) {
344
+ if (value == null) {
345
+ return [];
346
+ }
347
+ if (!Array.isArray(value)) {
348
+ failWithBubblesError('"aliasing" must be an array of strings when present.');
349
+ }
350
+ return value.map(
351
+ (entry, index) => normalizeOptionalString(entry, `aliasing[${index}]`)
352
+ ).filter((entry) => entry !== null);
353
+ }
354
+ function normalizeNotificationsEnabled(value) {
355
+ if (typeof value !== "boolean") {
356
+ failWithBubblesError('"notificationsEnabled" must be a boolean.');
357
+ }
358
+ return value;
359
+ }
360
+ function resolveLocaleAndTimeZone(input) {
361
+ const resolved = input ?? getLocaleAndTimeZone();
362
+ return {
363
+ locale: getRequiredNonEmptyString(resolved.locale, "locale"),
364
+ timeZone: normalizeOptionalString(resolved.timeZone, "timeZone")
365
+ };
366
+ }
367
+ function buildBaseDevicePayload(options) {
368
+ const { locale, timeZone } = resolveLocaleAndTimeZone(
369
+ options.localeAndTimeZone
370
+ );
371
+ const payload = {
372
+ user_id: getRequiredNonEmptyString(options.userId, "userId"),
373
+ aliasing: normalizeAliasing(options.aliasing),
374
+ platform: normalizePlatform(options.platform),
375
+ push_token: normalizeRequiredNullableString(options.pushToken, "pushToken"),
376
+ locale,
377
+ notifications_enabled: normalizeNotificationsEnabled(
378
+ options.notificationsEnabled
379
+ )
380
+ };
381
+ if ("fid" in options) {
382
+ payload.fid = normalizeOptionalString(options.fid, "fid");
383
+ }
384
+ const appVersion = normalizeOptionalString(options.appVersion, "appVersion");
385
+ if (appVersion) {
386
+ payload.app_version = appVersion;
387
+ }
388
+ if (timeZone) {
389
+ payload.timezone = timeZone;
390
+ }
391
+ return payload;
392
+ }
393
+ function buildCreateDevicePayload(options) {
394
+ return {
395
+ app_id: normalizeAppId(options.appId),
396
+ ...buildBaseDevicePayload(options)
397
+ };
398
+ }
399
+ function buildUpdateDevicePayload(options) {
400
+ return buildBaseDevicePayload(options);
401
+ }
402
+
403
+ // src/api/device-client.ts
404
+ import {
405
+ DeviceClient
406
+ } from "@fishonfire/bubbles-js";
407
+ var APP_KEY_HEADER_NAME = "X-App-Key";
408
+ var INVALID_APP_KEY_ERROR = "invalid app key for app";
409
+ var BubblesAppAuthenticationError = class extends Error {
410
+ constructor(cause) {
411
+ super("Bubbles app configuration/authentication failed: invalid app key for app.");
412
+ this.name = "BubblesAppAuthenticationError";
413
+ this.cause = cause;
414
+ }
415
+ };
416
+ function resolveDeviceClient(options) {
417
+ if (options.client) {
418
+ return options.client;
419
+ }
420
+ return createBubblesDeviceClient({
421
+ ...options.clientOptions,
422
+ appKey: options.appKey,
423
+ baseUrl: getRequiredNonEmptyString(options.apiBaseUrl, "apiBaseUrl")
424
+ });
425
+ }
426
+ function createBubblesDeviceClient(options) {
427
+ const { appKey: rawAppKey, ...clientOptions } = options;
428
+ const appKey = rawAppKey ? getRequiredNonEmptyString(rawAppKey, "appKey") : null;
429
+ return new DeviceClient({
430
+ ...clientOptions,
431
+ baseUrl: getRequiredNonEmptyString(clientOptions.baseUrl, "apiBaseUrl"),
432
+ defaultHeaders: {
433
+ ...clientOptions.defaultHeaders,
434
+ ...appKey ? { [APP_KEY_HEADER_NAME]: appKey } : {}
435
+ }
436
+ });
437
+ }
438
+ function withAppKeyHeader(appKey, requestOptions) {
439
+ return {
440
+ ...requestOptions,
441
+ headers: {
442
+ ...requestOptions?.headers,
443
+ [APP_KEY_HEADER_NAME]: getRequiredNonEmptyString(appKey, "appKey")
444
+ }
445
+ };
446
+ }
447
+ function isInvalidAppKeyResponse(error) {
448
+ if (!error || typeof error !== "object") {
449
+ return false;
450
+ }
451
+ const status = Reflect.get(error, "status");
452
+ const body = Reflect.get(error, "body");
453
+ return status === 401 && !!body && typeof body === "object" && !Array.isArray(body) && Reflect.get(body, "error") === INVALID_APP_KEY_ERROR;
454
+ }
455
+ function throwAppAuthenticationError(error) {
456
+ if (isInvalidAppKeyResponse(error)) {
457
+ throw new BubblesAppAuthenticationError(error);
458
+ }
459
+ throw error;
460
+ }
461
+ function extractDeviceIdFromDeviceResponse(value) {
462
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
463
+ return null;
464
+ }
465
+ const id = Reflect.get(value, "id");
466
+ if (typeof id === "number") {
467
+ return Number.isFinite(id) ? String(id) : null;
468
+ }
469
+ if (typeof id === "string") {
470
+ const trimmedId = id.trim();
471
+ return trimmedId.length > 0 ? trimmedId : null;
472
+ }
473
+ return null;
474
+ }
475
+ async function createBubblesDevice(options) {
476
+ const client = resolveDeviceClient(options);
477
+ const payload = buildCreateDevicePayload(options);
478
+ try {
479
+ return await client.createDevice(
480
+ payload,
481
+ withAppKeyHeader(options.appKey, options.requestOptions)
482
+ );
483
+ } catch (error) {
484
+ throwAppAuthenticationError(error);
485
+ }
486
+ }
487
+ async function updateBubblesDevice(options) {
488
+ const client = resolveDeviceClient(options);
489
+ const payload = buildUpdateDevicePayload(options);
490
+ try {
491
+ return await client.updateDevice(
492
+ normalizeIdentifier(options.deviceId, "deviceId"),
493
+ payload,
494
+ withAppKeyHeader(options.appKey, options.requestOptions)
495
+ );
496
+ } catch (error) {
497
+ throwAppAuthenticationError(error);
498
+ }
499
+ }
500
+ async function syncBubblesDevice(options) {
501
+ const client = resolveDeviceClient(options);
502
+ if (options.deviceId !== null) {
503
+ const deviceId = normalizeIdentifier(options.deviceId, "deviceId");
504
+ let response2;
505
+ try {
506
+ response2 = await client.updateDevice(
507
+ deviceId,
508
+ buildUpdateDevicePayload(options),
509
+ withAppKeyHeader(options.appKey, options.updateRequestOptions)
510
+ );
511
+ } catch (error) {
512
+ throwAppAuthenticationError(error);
513
+ }
514
+ return {
515
+ action: "updated",
516
+ deviceId,
517
+ response: response2
518
+ };
519
+ }
520
+ let response;
521
+ try {
522
+ response = await client.createDevice(
523
+ buildCreateDevicePayload(options),
524
+ withAppKeyHeader(options.appKey, options.createRequestOptions)
525
+ );
526
+ } catch (error) {
527
+ throwAppAuthenticationError(error);
528
+ }
529
+ return {
530
+ action: "created",
531
+ deviceId: extractDeviceIdFromDeviceResponse(response),
532
+ response
533
+ };
534
+ }
535
+
536
+ // src/api/delivery-status.ts
537
+ var BUBBLES_DELIVERY_STATUSES = {
538
+ notificationReceived: "received",
539
+ notificationShown: "shown",
540
+ notificationClicked: "clicked",
541
+ notificationsDisabled: "disabled"
542
+ };
543
+ function normalizeError(value) {
544
+ if (value == null) {
545
+ return void 0;
546
+ }
547
+ if (typeof value !== "string") {
548
+ failWithBubblesError('"error" must be a string when present.');
549
+ }
550
+ const trimmedValue = value.trim();
551
+ if (!trimmedValue) {
552
+ failWithBubblesError('"error" must not be empty when present.');
553
+ }
554
+ return trimmedValue;
555
+ }
556
+ function normalizeStatus(value) {
557
+ if (value === void 0) {
558
+ return void 0;
559
+ }
560
+ const supportedValues = Object.values(BUBBLES_DELIVERY_STATUSES);
561
+ if (typeof value !== "string" || !supportedValues.includes(value)) {
562
+ failWithBubblesError(
563
+ `"status" must be one of: ${supportedValues.join(", ")}.`
564
+ );
565
+ }
566
+ return value;
567
+ }
568
+ function resolveDeviceClient2(options) {
569
+ if (options.client) {
570
+ return options.client;
571
+ }
572
+ return createBubblesDeviceClient({
573
+ ...options.clientOptions,
574
+ appKey: getRequiredNonEmptyString(options.appKey, "appKey"),
575
+ baseUrl: getRequiredNonEmptyString(options.apiBaseUrl, "apiBaseUrl")
576
+ });
577
+ }
578
+ function withAppKeyHeader2(appKey, requestOptions) {
579
+ return {
580
+ ...requestOptions,
581
+ headers: {
582
+ ...requestOptions?.headers,
583
+ "X-App-Key": getRequiredNonEmptyString(appKey, "appKey")
584
+ }
585
+ };
586
+ }
587
+ function isInvalidAppKeyResponse2(error) {
588
+ if (!error || typeof error !== "object") {
589
+ return false;
590
+ }
591
+ const status = Reflect.get(error, "status");
592
+ const body = Reflect.get(error, "body");
593
+ return status === 401 && !!body && typeof body === "object" && !Array.isArray(body) && Reflect.get(body, "error") === "invalid app key for app";
594
+ }
595
+ function buildBubblesDeliveryStatusPayload(options) {
596
+ const status = normalizeStatus(options.status);
597
+ const error = normalizeError(options.error);
598
+ if (!status && !error) {
599
+ failWithBubblesError(
600
+ 'A delivery status payload must include "status" and/or "error".'
601
+ );
602
+ }
603
+ return {
604
+ ...status ? { status } : {},
605
+ ...error ? { error } : {}
606
+ };
607
+ }
608
+ async function postBubblesDeliveryStatus(options) {
609
+ const client = resolveDeviceClient2(options);
610
+ try {
611
+ await client.postDeliveryStatus(
612
+ normalizeIdentifier(options.deviceId, "deviceId"),
613
+ normalizeIdentifier(options.notificationId, "notificationId"),
614
+ buildBubblesDeliveryStatusPayload(options),
615
+ withAppKeyHeader2(options.appKey, options.requestOptions)
616
+ );
617
+ } catch (error) {
618
+ if (isInvalidAppKeyResponse2(error)) {
619
+ throw new BubblesAppAuthenticationError(error);
620
+ }
621
+ throw error;
622
+ }
623
+ }
624
+
625
+ // src/storage/delivery-status-ledger.ts
626
+ import { File as File2, Paths as Paths2 } from "expo-file-system";
627
+ var DELIVERY_STATUS_LEDGER_FILE_NAME = "bubbles-notifications-expo-delivery-status-ledger.json";
628
+ var DELIVERY_STATUS_LEDGER_VERSION = 1;
629
+ var MAX_POSTED_EVENT_KEYS = 200;
630
+ function getDeliveryStatusLedgerFile() {
631
+ return new File2(Paths2.document, DELIVERY_STATUS_LEDGER_FILE_NAME);
632
+ }
633
+ function getOptionalNonEmptyString2(value) {
634
+ if (value == null) {
635
+ return void 0;
636
+ }
637
+ if (typeof value !== "string") {
638
+ return void 0;
639
+ }
640
+ const trimmedValue = value.trim();
641
+ return trimmedValue.length > 0 ? trimmedValue : void 0;
642
+ }
643
+ function requireString(value, sourceDescription) {
644
+ if (typeof value !== "string") {
645
+ failWithBubblesError(`${sourceDescription} must be a string.`);
646
+ }
647
+ const trimmedValue = value.trim();
648
+ if (!trimmedValue) {
649
+ failWithBubblesError(`${sourceDescription} must not be empty.`);
650
+ }
651
+ return trimmedValue;
652
+ }
653
+ function buildStoredDeliveryStatusEventKey(options) {
654
+ return JSON.stringify([
655
+ options.notificationId,
656
+ options.status ?? null,
657
+ options.error ?? null,
658
+ options.actionId ?? null
659
+ ]);
660
+ }
661
+ function createStoredDeliveryStatusEvent(options) {
662
+ const error = getOptionalNonEmptyString2(options.error);
663
+ const actionId = getOptionalNonEmptyString2(options.actionId);
664
+ return {
665
+ key: buildStoredDeliveryStatusEventKey({
666
+ notificationId: options.notificationId,
667
+ status: options.status,
668
+ error,
669
+ actionId
670
+ }),
671
+ notificationId: options.notificationId,
672
+ ...options.status ? { status: options.status } : {},
673
+ ...error ? { error } : {},
674
+ ...actionId ? { actionId } : {},
675
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
676
+ };
677
+ }
678
+ function normalizeStoredDeliveryStatusEvent(value, sourceDescription) {
679
+ if (!isPlainObject(value)) {
680
+ failWithBubblesError(`${sourceDescription} must be a JSON object.`);
681
+ }
682
+ const status = getOptionalNonEmptyString2(value.status);
683
+ const error = getOptionalNonEmptyString2(value.error);
684
+ const actionId = getOptionalNonEmptyString2(value.actionId);
685
+ return {
686
+ key: requireString(value.key, `${sourceDescription}.key`),
687
+ notificationId: requireString(
688
+ value.notificationId,
689
+ `${sourceDescription}.notificationId`
690
+ ),
691
+ ...status ? { status } : {},
692
+ ...error ? { error } : {},
693
+ ...actionId ? { actionId } : {},
694
+ createdAt: requireString(value.createdAt, `${sourceDescription}.createdAt`)
695
+ };
696
+ }
697
+ function normalizeStringArray(value) {
698
+ if (!Array.isArray(value)) {
699
+ return [];
700
+ }
701
+ return value.map((item) => getOptionalNonEmptyString2(item)).filter((item) => item !== void 0);
702
+ }
703
+ function normalizeDeliveryStatusLedger(value, sourceDescription) {
704
+ if (!isPlainObject(value)) {
705
+ failWithBubblesError(
706
+ `Stored delivery status ledger in ${sourceDescription} must be a JSON object.`
707
+ );
708
+ }
709
+ if (value.version !== DELIVERY_STATUS_LEDGER_VERSION) {
710
+ failWithBubblesError(
711
+ `Stored delivery status ledger in ${sourceDescription} uses unsupported schema version "${String(
712
+ value.version
713
+ )}".`
714
+ );
715
+ }
716
+ return {
717
+ pendingEvents: Array.isArray(value.pendingEvents) ? value.pendingEvents.map(
718
+ (event, index) => normalizeStoredDeliveryStatusEvent(
719
+ event,
720
+ `${sourceDescription}.pendingEvents[${index}]`
721
+ )
722
+ ) : [],
723
+ postedEventKeys: normalizeStringArray(value.postedEventKeys).slice(
724
+ -MAX_POSTED_EVENT_KEYS
725
+ )
726
+ };
727
+ }
728
+ function serializeDeliveryStatusLedger(ledger) {
729
+ return {
730
+ version: DELIVERY_STATUS_LEDGER_VERSION,
731
+ pendingEvents: ledger.pendingEvents,
732
+ postedEventKeys: ledger.postedEventKeys.slice(-MAX_POSTED_EVENT_KEYS)
733
+ };
734
+ }
735
+ function readDeliveryStatusLedgerFileText() {
736
+ const ledgerFile = getDeliveryStatusLedgerFile();
737
+ if (!ledgerFile.exists) {
738
+ return null;
739
+ }
740
+ const contents = ledgerFile.textSync().trim();
741
+ return contents.length > 0 ? contents : null;
742
+ }
743
+ function ensureDeliveryStatusLedgerFile() {
744
+ const ledgerFile = getDeliveryStatusLedgerFile();
745
+ if (!ledgerFile.exists) {
746
+ ledgerFile.create({ intermediates: true });
747
+ }
748
+ return ledgerFile;
749
+ }
750
+ function readStoredDeliveryStatusLedger() {
751
+ const ledgerFile = getDeliveryStatusLedgerFile();
752
+ const rawLedger = readDeliveryStatusLedgerFileText();
753
+ if (rawLedger === null) {
754
+ return {
755
+ pendingEvents: [],
756
+ postedEventKeys: []
757
+ };
758
+ }
759
+ let parsedLedger;
760
+ try {
761
+ parsedLedger = JSON.parse(rawLedger);
762
+ } catch {
763
+ failWithBubblesError(
764
+ `Stored delivery status ledger at "${ledgerFile.uri}" is not valid JSON.`
765
+ );
766
+ }
767
+ return normalizeDeliveryStatusLedger(
768
+ parsedLedger,
769
+ `"${ledgerFile.uri}"`
770
+ );
771
+ }
772
+ function storeDeliveryStatusLedger(ledger) {
773
+ const normalizedLedger = {
774
+ pendingEvents: ledger.pendingEvents,
775
+ postedEventKeys: ledger.postedEventKeys.slice(-MAX_POSTED_EVENT_KEYS)
776
+ };
777
+ const ledgerFile = ensureDeliveryStatusLedgerFile();
778
+ ledgerFile.write(
779
+ JSON.stringify(serializeDeliveryStatusLedger(normalizedLedger), null, 2)
780
+ );
781
+ return normalizedLedger;
782
+ }
783
+ function enqueueStoredDeliveryStatusEvent(event) {
784
+ const ledger = readStoredDeliveryStatusLedger();
785
+ if (ledger.postedEventKeys.includes(event.key) || ledger.pendingEvents.some((pendingEvent) => pendingEvent.key === event.key)) {
786
+ return ledger;
787
+ }
788
+ return storeDeliveryStatusLedger({
789
+ ...ledger,
790
+ pendingEvents: [...ledger.pendingEvents, event]
791
+ });
792
+ }
793
+ function markStoredDeliveryStatusEventPosted(event) {
794
+ const ledger = readStoredDeliveryStatusLedger();
795
+ const postedEventKeys = ledger.postedEventKeys.includes(event.key) ? ledger.postedEventKeys : [...ledger.postedEventKeys, event.key];
796
+ return storeDeliveryStatusLedger({
797
+ pendingEvents: ledger.pendingEvents.filter(
798
+ (pendingEvent) => pendingEvent.key !== event.key
799
+ ),
800
+ postedEventKeys
801
+ });
802
+ }
803
+
804
+ // src/notifications/delivery-status.ts
805
+ function warnMissingApiBaseUrl(source) {
806
+ console.warn(
807
+ `[@fishonfire/bubbles-expo] Skipping ${source} delivery-status update because "apiBaseUrl" has not been persisted yet.`
808
+ );
809
+ }
810
+ function warnMissingAppKey(source) {
811
+ console.warn(
812
+ `[@fishonfire/bubbles-expo] Skipping ${source} delivery-status update because "appKey" has not been persisted yet.`
813
+ );
814
+ }
815
+ async function postStoredBubblesDeliveryStatus(options) {
816
+ const storedDeviceState = options.storedDeviceState ?? readStoredDeviceState();
817
+ const { deviceId, apiBaseUrl, appKey } = storedDeviceState;
818
+ if (!options.notificationId) {
819
+ return storedDeviceState;
820
+ }
821
+ const event = createStoredDeliveryStatusEvent({
822
+ notificationId: options.notificationId,
823
+ status: options.status,
824
+ error: options.error,
825
+ actionId: options.actionId
826
+ });
827
+ enqueueStoredDeliveryStatusEvent(event);
828
+ if (!deviceId) {
829
+ return storedDeviceState;
830
+ }
831
+ if (!apiBaseUrl) {
832
+ warnMissingApiBaseUrl(options.source);
833
+ return storedDeviceState;
834
+ }
835
+ if (!appKey) {
836
+ warnMissingAppKey(options.source);
837
+ return storedDeviceState;
838
+ }
839
+ await flushStoredBubblesDeliveryStatuses({
840
+ apiBaseUrl,
841
+ appKey,
842
+ deviceId
843
+ });
844
+ return storedDeviceState;
845
+ }
846
+ async function flushStoredBubblesDeliveryStatuses(storedDeviceState = readStoredDeviceState()) {
847
+ const { deviceId, apiBaseUrl, appKey } = storedDeviceState;
848
+ if (!deviceId || !apiBaseUrl || !appKey) {
849
+ return storedDeviceState;
850
+ }
851
+ const { pendingEvents } = readStoredDeliveryStatusLedger();
852
+ for (const event of pendingEvents) {
853
+ await postStoredDeliveryStatusEvent(apiBaseUrl, appKey, deviceId, event);
854
+ markStoredDeliveryStatusEventPosted(event);
855
+ }
856
+ return storedDeviceState;
857
+ }
858
+ async function postStoredDeliveryStatusEvent(apiBaseUrl, appKey, deviceId, event) {
859
+ await postBubblesDeliveryStatus({
860
+ apiBaseUrl,
861
+ appKey,
862
+ deviceId,
863
+ notificationId: event.notificationId,
864
+ status: event.status,
865
+ error: event.error
866
+ });
867
+ }
868
+
869
+ // src/runtime/channels.ts
870
+ import * as Notifications from "expo-notifications";
871
+ import { Platform } from "react-native";
872
+ var ANDROID_CHANNEL_IMPORTANCE_MAP = {
873
+ min: Notifications.AndroidImportance.MIN,
874
+ low: Notifications.AndroidImportance.LOW,
875
+ default: Notifications.AndroidImportance.DEFAULT,
876
+ high: Notifications.AndroidImportance.HIGH,
877
+ max: Notifications.AndroidImportance.MAX
878
+ };
879
+ function fail(message) {
880
+ throw new Error(`[@fishonfire/bubbles-expo] ${message}`);
881
+ }
882
+ function getRequiredNonEmptyString2(value, fieldName) {
883
+ if (typeof value !== "string") {
884
+ fail(`"${fieldName}" must be a string.`);
885
+ }
886
+ const trimmedValue = value.trim();
887
+ if (!trimmedValue) {
888
+ fail(`"${fieldName}" must not be empty.`);
889
+ }
890
+ return trimmedValue;
891
+ }
892
+ function getAndroidNotificationChannelImportance(value) {
893
+ return ANDROID_CHANNEL_IMPORTANCE_MAP[value];
894
+ }
895
+ function getDefaultNotificationChannelConfig(options) {
896
+ const runtimeConfig = getBubblesNotificationsRuntimeConfig();
897
+ return {
898
+ channelId: getRequiredNonEmptyString2(
899
+ options?.channelId ?? runtimeConfig.defaultChannelId,
900
+ "channelId"
901
+ ),
902
+ channelName: getRequiredNonEmptyString2(
903
+ options?.channelName ?? runtimeConfig.defaultChannelName,
904
+ "channelName"
905
+ ),
906
+ importance: options?.importance ?? runtimeConfig.androidChannelImportance
907
+ };
908
+ }
909
+ async function ensureDefaultNotificationChannel(options) {
910
+ if (Platform.OS !== "android") {
911
+ return null;
912
+ }
913
+ const channelConfig = getDefaultNotificationChannelConfig(options);
914
+ return Notifications.setNotificationChannelAsync(channelConfig.channelId, {
915
+ name: channelConfig.channelName,
916
+ importance: getAndroidNotificationChannelImportance(channelConfig.importance)
917
+ });
918
+ }
919
+
920
+ // src/runtime/permissions.ts
921
+ import * as Notifications2 from "expo-notifications";
922
+ import { Platform as Platform2 } from "react-native";
923
+
924
+ // src/internal/platform.ts
925
+ function isSupportedPlatform(value) {
926
+ return value === "android" || value === "ios";
927
+ }
928
+ function assertSupportedPlatform(value) {
929
+ if (isSupportedPlatform(value)) {
930
+ return;
931
+ }
932
+ failWithBubblesError(
933
+ `Unsupported platform: ${value}. This package only supports iOS and Android.`
934
+ );
935
+ }
936
+ function getSupportedPlatform(value) {
937
+ assertSupportedPlatform(value);
938
+ return value;
939
+ }
940
+
941
+ // src/runtime/permissions.ts
942
+ var IOS_GRANTED_AUTHORIZATION_STATUSES = /* @__PURE__ */ new Set([
943
+ Notifications2.IosAuthorizationStatus.AUTHORIZED,
944
+ Notifications2.IosAuthorizationStatus.PROVISIONAL,
945
+ Notifications2.IosAuthorizationStatus.EPHEMERAL
946
+ ]);
947
+ function isNotificationPermissionGranted(permissions) {
948
+ if (Platform2.OS === "ios" && permissions.ios?.status != null) {
949
+ return IOS_GRANTED_AUTHORIZATION_STATUSES.has(permissions.ios.status);
950
+ }
951
+ return permissions.granted;
952
+ }
953
+ function describeNotificationPermissionStatus(permissions) {
954
+ if (Platform2.OS === "ios" && permissions.ios?.status != null) {
955
+ return Notifications2.IosAuthorizationStatus[permissions.ios.status].toLowerCase();
956
+ }
957
+ return permissions.status;
958
+ }
959
+ async function getNotificationPermissions(options) {
960
+ assertSupportedPlatform(Platform2.OS);
961
+ await ensureDefaultNotificationChannel();
962
+ const existingPermissions = await Notifications2.getPermissionsAsync();
963
+ if (isNotificationPermissionGranted(existingPermissions) || options?.requestPermissions === false) {
964
+ return existingPermissions;
965
+ }
966
+ return Notifications2.requestPermissionsAsync(options?.permissionRequestOptions);
967
+ }
968
+ async function ensureNotificationPermissions(options) {
969
+ const permissions = await getNotificationPermissions(options);
970
+ if (!isNotificationPermissionGranted(permissions)) {
971
+ failWithBubblesError("Push notification permission was not granted.");
972
+ }
973
+ return permissions;
974
+ }
975
+
976
+ // src/background/firebase-message-handler.ts
977
+ import {
978
+ getMessaging,
979
+ setBackgroundMessageHandler
980
+ } from "@react-native-firebase/messaging";
981
+ import * as Notifications4 from "expo-notifications";
982
+ import { Platform as Platform3 } from "react-native";
983
+
984
+ // src/notifications/local-notification.ts
985
+ import * as Notifications3 from "expo-notifications";
986
+ var DEFAULT_LOCAL_NOTIFICATION_TITLE = "Notification received";
987
+ var DEFAULT_LOCAL_NOTIFICATION_BODY = "Open the app to view this update.";
988
+ var DUPLICATE_NOTIFICATION_WINDOW_MS = 5e3;
989
+ var recentlyScheduledNotificationKeys = /* @__PURE__ */ new Map();
990
+ function getOptionalNonEmptyString3(value) {
991
+ if (typeof value !== "string") {
992
+ return null;
993
+ }
994
+ const trimmedValue = value.trim();
995
+ return trimmedValue.length > 0 ? trimmedValue : null;
996
+ }
997
+ function buildDuplicateNotificationKey(payload) {
998
+ if (payload.notificationId) {
999
+ return `notification:${payload.notificationId}`;
1000
+ }
1001
+ const dataKeys = Object.keys(payload.data);
1002
+ if (dataKeys.length === 0) {
1003
+ return null;
1004
+ }
1005
+ try {
1006
+ return `data:${JSON.stringify(payload.data)}`;
1007
+ } catch {
1008
+ return null;
1009
+ }
1010
+ }
1011
+ function rememberScheduledNotificationKey(duplicateKey) {
1012
+ const now = Date.now();
1013
+ for (const [key, timestamp] of recentlyScheduledNotificationKeys) {
1014
+ if (now - timestamp > DUPLICATE_NOTIFICATION_WINDOW_MS) {
1015
+ recentlyScheduledNotificationKeys.delete(key);
1016
+ }
1017
+ }
1018
+ const previousTimestamp = recentlyScheduledNotificationKeys.get(duplicateKey);
1019
+ if (previousTimestamp !== void 0 && now - previousTimestamp <= DUPLICATE_NOTIFICATION_WINDOW_MS) {
1020
+ return false;
1021
+ }
1022
+ recentlyScheduledNotificationKeys.set(duplicateKey, now);
1023
+ return true;
1024
+ }
1025
+ async function postLocalNotificationShownDeliveryStatus(options) {
1026
+ try {
1027
+ await postStoredBubblesDeliveryStatus({
1028
+ source: options.source,
1029
+ notificationId: options.payload.notificationId,
1030
+ status: BUBBLES_DELIVERY_STATUSES.notificationShown
1031
+ });
1032
+ } catch (error) {
1033
+ console.error(
1034
+ `[@fishonfire/bubbles-expo] Failed to post ${options.source} shown delivery status.`,
1035
+ error
1036
+ );
1037
+ }
1038
+ }
1039
+ function buildBubblesLocalNotificationRequest(payload, options) {
1040
+ return {
1041
+ content: {
1042
+ title: payload.messageTitle ?? getOptionalNonEmptyString3(options?.title) ?? DEFAULT_LOCAL_NOTIFICATION_TITLE,
1043
+ body: payload.body ?? getOptionalNonEmptyString3(options?.body) ?? DEFAULT_LOCAL_NOTIFICATION_BODY,
1044
+ data: payload.data
1045
+ },
1046
+ // Force app to trigger it 1 second from now instead of null.
1047
+ // This prevents issues during background handling.
1048
+ trigger: {
1049
+ type: Notifications3.SchedulableTriggerInputTypes.TIME_INTERVAL,
1050
+ seconds: 1
1051
+ }
1052
+ };
1053
+ }
1054
+ async function scheduleBubblesLocalNotification(options) {
1055
+ if (!options.skipPermissionCheck) {
1056
+ const permissions = await Notifications3.getPermissionsAsync();
1057
+ const notificationsEnabled = isNotificationPermissionGranted(permissions);
1058
+ if (!notificationsEnabled) {
1059
+ return null;
1060
+ }
1061
+ }
1062
+ const duplicateKey = buildDuplicateNotificationKey(options.payload);
1063
+ if (duplicateKey !== null && !rememberScheduledNotificationKey(duplicateKey)) {
1064
+ return null;
1065
+ }
1066
+ const notificationRequest = buildBubblesLocalNotificationRequest(
1067
+ options.payload,
1068
+ options
1069
+ );
1070
+ const scheduledNotificationId = await Notifications3.scheduleNotificationAsync(
1071
+ notificationRequest
1072
+ );
1073
+ await postLocalNotificationShownDeliveryStatus(options);
1074
+ return scheduledNotificationId;
1075
+ }
1076
+
1077
+ // src/notifications/payload.ts
1078
+ function isPlainObject2(value) {
1079
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1080
+ }
1081
+ function getOptionalNonEmptyString4(value) {
1082
+ if (typeof value !== "string") {
1083
+ return null;
1084
+ }
1085
+ const trimmedValue = value.trim();
1086
+ return trimmedValue.length > 0 ? trimmedValue : null;
1087
+ }
1088
+ function getOptionalIdentifier(value) {
1089
+ if (typeof value === "number") {
1090
+ return Number.isFinite(value) ? String(value) : null;
1091
+ }
1092
+ return getOptionalNonEmptyString4(value);
1093
+ }
1094
+ function getBubblesNotificationData(value) {
1095
+ return isPlainObject2(value) ? value : {};
1096
+ }
1097
+ function getBubblesNotificationDataFromNotification(notification) {
1098
+ return getBubblesNotificationData(notification.request.content.data);
1099
+ }
1100
+ function normalizeBubblesNotificationPayload(value) {
1101
+ const data = getBubblesNotificationData(value);
1102
+ return {
1103
+ data,
1104
+ notificationId: getOptionalIdentifier(data.notification_id),
1105
+ messageTitle: getOptionalNonEmptyString4(data.message_title),
1106
+ body: getOptionalNonEmptyString4(data.body),
1107
+ source: getOptionalNonEmptyString4(data.source),
1108
+ messageId: getOptionalIdentifier(data.message_id),
1109
+ campaignId: getOptionalIdentifier(data.campaign_id),
1110
+ url: getOptionalNonEmptyString4(data.url)
1111
+ };
1112
+ }
1113
+
1114
+ // src/notifications/remote-message.ts
1115
+ function getOptionalNonEmptyString5(value) {
1116
+ if (typeof value !== "string") {
1117
+ return null;
1118
+ }
1119
+ const trimmedValue = value.trim();
1120
+ return trimmedValue.length > 0 ? trimmedValue : null;
1121
+ }
1122
+ function getBubblesNotificationPayloadFromRemoteMessage(remoteMessage) {
1123
+ const payload = normalizeBubblesNotificationPayload(
1124
+ getBubblesNotificationData(remoteMessage.data)
1125
+ );
1126
+ return {
1127
+ ...payload,
1128
+ messageTitle: payload.messageTitle ?? getOptionalNonEmptyString5(remoteMessage.notification?.title),
1129
+ body: payload.body ?? getOptionalNonEmptyString5(remoteMessage.notification?.body)
1130
+ };
1131
+ }
1132
+ function hasFirebaseDisplayNotification(remoteMessage) {
1133
+ return getOptionalNonEmptyString5(remoteMessage.notification?.title) !== null || getOptionalNonEmptyString5(remoteMessage.notification?.body) !== null;
1134
+ }
1135
+
1136
+ // src/background/firebase-message-handler.ts
1137
+ var BACKGROUND_NOTIFICATION_SOURCE = "Firebase background message";
1138
+ var DEFAULT_BACKGROUND_NOTIFICATION_TITLE = "Background notification received";
1139
+ var DEFAULT_BACKGROUND_NOTIFICATION_BODY = "Open the app to view this update.";
1140
+ function supportsFirebaseBackgroundMessages() {
1141
+ return Platform3.OS === "android" || Platform3.OS === "ios";
1142
+ }
1143
+ function supportsBackgroundLocalNotificationPresentation() {
1144
+ return Platform3.OS === "android";
1145
+ }
1146
+ function logBackgroundStatusError(description, error) {
1147
+ console.error(
1148
+ `[@fishonfire/bubbles-expo] Failed to post ${BACKGROUND_NOTIFICATION_SOURCE} ${description} delivery status.`,
1149
+ error
1150
+ );
1151
+ }
1152
+ function getBackgroundNotificationErrorMessage(error) {
1153
+ if (!(error instanceof Error)) {
1154
+ return "Failed to show notification";
1155
+ }
1156
+ return `Failed to show notification: ${getErrorMessage(error)}`;
1157
+ }
1158
+ async function postBackgroundDeliveryStatus(notificationId, status) {
1159
+ try {
1160
+ await postStoredBubblesDeliveryStatus({
1161
+ source: BACKGROUND_NOTIFICATION_SOURCE,
1162
+ notificationId,
1163
+ status
1164
+ });
1165
+ } catch (error) {
1166
+ logBackgroundStatusError(status, error);
1167
+ }
1168
+ }
1169
+ async function postBackgroundDeliveryError(notificationId, errorMessage) {
1170
+ try {
1171
+ await postStoredBubblesDeliveryStatus({
1172
+ source: BACKGROUND_NOTIFICATION_SOURCE,
1173
+ notificationId,
1174
+ error: errorMessage
1175
+ });
1176
+ } catch (error) {
1177
+ logBackgroundStatusError("error", error);
1178
+ }
1179
+ }
1180
+ function postBackgroundNotificationReceived(notificationId) {
1181
+ return postBackgroundDeliveryStatus(
1182
+ notificationId,
1183
+ BUBBLES_DELIVERY_STATUSES.notificationReceived
1184
+ );
1185
+ }
1186
+ function postBackgroundNotificationsDisabled(notificationId) {
1187
+ return postBackgroundDeliveryStatus(
1188
+ notificationId,
1189
+ BUBBLES_DELIVERY_STATUSES.notificationsDisabled
1190
+ );
1191
+ }
1192
+ var hasRegisteredFirebaseBackgroundMessageHandler = false;
1193
+ var handleBubblesFirebaseBackgroundMessage = async (remoteMessage) => {
1194
+ const payload = getBubblesNotificationPayloadFromRemoteMessage(remoteMessage);
1195
+ const permissions = await Notifications4.getPermissionsAsync();
1196
+ const notificationsEnabled = isNotificationPermissionGranted(permissions);
1197
+ if (!notificationsEnabled) {
1198
+ await postBackgroundNotificationsDisabled(payload.notificationId);
1199
+ return;
1200
+ }
1201
+ await postBackgroundNotificationReceived(payload.notificationId);
1202
+ if (hasFirebaseDisplayNotification(remoteMessage)) {
1203
+ return;
1204
+ }
1205
+ if (!supportsBackgroundLocalNotificationPresentation()) {
1206
+ return;
1207
+ }
1208
+ try {
1209
+ const scheduledNotificationId = await scheduleBubblesLocalNotification({
1210
+ source: BACKGROUND_NOTIFICATION_SOURCE,
1211
+ payload,
1212
+ title: DEFAULT_BACKGROUND_NOTIFICATION_TITLE,
1213
+ body: DEFAULT_BACKGROUND_NOTIFICATION_BODY,
1214
+ skipPermissionCheck: true
1215
+ });
1216
+ if (scheduledNotificationId === null) {
1217
+ return;
1218
+ }
1219
+ } catch (error) {
1220
+ await postBackgroundDeliveryError(
1221
+ payload.notificationId,
1222
+ getBackgroundNotificationErrorMessage(error)
1223
+ );
1224
+ throw error;
1225
+ }
1226
+ };
1227
+ function registerBubblesFirebaseBackgroundMessageHandler() {
1228
+ if (hasRegisteredFirebaseBackgroundMessageHandler) {
1229
+ return;
1230
+ }
1231
+ if (!supportsFirebaseBackgroundMessages()) {
1232
+ return;
1233
+ }
1234
+ try {
1235
+ setBackgroundMessageHandler(
1236
+ getMessaging(),
1237
+ handleBubblesFirebaseBackgroundMessage
1238
+ );
1239
+ hasRegisteredFirebaseBackgroundMessageHandler = true;
1240
+ } catch (error) {
1241
+ console.error(
1242
+ "[@fishonfire/bubbles-expo] Failed to register Firebase background message handler.",
1243
+ error
1244
+ );
1245
+ }
1246
+ }
1247
+
1248
+ // src/background/setup.ts
1249
+ var hasRegisteredBubblesBackgroundHandlers = false;
1250
+ function registerBubblesBackgroundHandlers() {
1251
+ if (hasRegisteredBubblesBackgroundHandlers) {
1252
+ return;
1253
+ }
1254
+ registerBubblesFirebaseBackgroundMessageHandler();
1255
+ hasRegisteredBubblesBackgroundHandlers = true;
1256
+ }
1257
+
1258
+ export {
1259
+ failWithBubblesError,
1260
+ getErrorMessage,
1261
+ getRequiredNonEmptyString,
1262
+ getRequiredNonEmptyStringFromSource,
1263
+ BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY2 as BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY,
1264
+ ANDROID_CHANNEL_IMPORTANCE_VALUES2 as ANDROID_CHANNEL_IMPORTANCE_VALUES,
1265
+ getBubblesNotificationsRuntimeConfig,
1266
+ readStoredDeviceState,
1267
+ storeDeviceState,
1268
+ patchStoredDeviceState,
1269
+ readStoredDeviceId,
1270
+ readStoredApiBaseUrl,
1271
+ readStoredAppKey,
1272
+ storeDeviceId,
1273
+ storeApiBaseUrl,
1274
+ storeAppKey,
1275
+ buildCreateDevicePayload,
1276
+ buildUpdateDevicePayload,
1277
+ BubblesAppAuthenticationError,
1278
+ createBubblesDeviceClient,
1279
+ extractDeviceIdFromDeviceResponse,
1280
+ createBubblesDevice,
1281
+ updateBubblesDevice,
1282
+ syncBubblesDevice,
1283
+ BUBBLES_DELIVERY_STATUSES,
1284
+ buildBubblesDeliveryStatusPayload,
1285
+ postBubblesDeliveryStatus,
1286
+ buildStoredDeliveryStatusEventKey,
1287
+ createStoredDeliveryStatusEvent,
1288
+ readStoredDeliveryStatusLedger,
1289
+ storeDeliveryStatusLedger,
1290
+ enqueueStoredDeliveryStatusEvent,
1291
+ markStoredDeliveryStatusEventPosted,
1292
+ postStoredBubblesDeliveryStatus,
1293
+ flushStoredBubblesDeliveryStatuses,
1294
+ getAndroidNotificationChannelImportance,
1295
+ getDefaultNotificationChannelConfig,
1296
+ ensureDefaultNotificationChannel,
1297
+ getSupportedPlatform,
1298
+ isNotificationPermissionGranted,
1299
+ describeNotificationPermissionStatus,
1300
+ getNotificationPermissions,
1301
+ ensureNotificationPermissions,
1302
+ scheduleBubblesLocalNotification,
1303
+ getBubblesNotificationDataFromNotification,
1304
+ normalizeBubblesNotificationPayload,
1305
+ getBubblesNotificationPayloadFromRemoteMessage,
1306
+ hasFirebaseDisplayNotification,
1307
+ registerBubblesBackgroundHandlers
1308
+ };