@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/LICENSE +674 -0
- package/README.md +180 -0
- package/app.plugin.js +1 -0
- package/dist/chunk-6NPXFGQX.mjs +1308 -0
- package/dist/index.d.mts +306 -0
- package/dist/index.d.ts +306 -0
- package/dist/index.js +2060 -0
- package/dist/index.mjs +789 -0
- package/dist/register-task.d.mts +2 -0
- package/dist/register-task.d.ts +2 -0
- package/dist/register-task.js +818 -0
- package/dist/register-task.mjs +6 -0
- package/package.json +94 -0
- package/plugin/build/config.d.ts +18 -0
- package/plugin/build/config.js +69 -0
- package/plugin/build/index.d.ts +4 -0
- package/plugin/build/index.js +19 -0
- package/plugin/build/shared-config.d.ts +3 -0
- package/plugin/build/shared-config.js +11 -0
- package/plugin/build/with-expo-notifications.d.ts +3 -0
- package/plugin/build/with-expo-notifications.js +19 -0
- package/plugin/build/with-firebase-messaging-manifest.d.ts +4 -0
- package/plugin/build/with-firebase-messaging-manifest.js +35 -0
- package/plugin/build/with-rnfirebase-disable-spm.d.ts +3 -0
- package/plugin/build/with-rnfirebase-disable-spm.js +14 -0
- package/plugin/build/with-runtime-defaults.d.ts +3 -0
- package/plugin/build/with-runtime-defaults.js +33 -0
|
@@ -0,0 +1,818 @@
|
|
|
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 __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
25
|
+
|
|
26
|
+
// src/register-task.ts
|
|
27
|
+
var register_task_exports = {};
|
|
28
|
+
module.exports = __toCommonJS(register_task_exports);
|
|
29
|
+
|
|
30
|
+
// src/background/firebase-message-handler.ts
|
|
31
|
+
var import_messaging = require("@react-native-firebase/messaging");
|
|
32
|
+
var Notifications3 = __toESM(require("expo-notifications"));
|
|
33
|
+
var import_react_native2 = require("react-native");
|
|
34
|
+
|
|
35
|
+
// src/api/device-client.ts
|
|
36
|
+
var import_bubbles_js = require("@fishonfire/bubbles-js");
|
|
37
|
+
|
|
38
|
+
// src/internal/errors.ts
|
|
39
|
+
function createBubblesError(message) {
|
|
40
|
+
return new Error(`[@fishonfire/bubbles-expo] ${message}`);
|
|
41
|
+
}
|
|
42
|
+
function failWithBubblesError(message) {
|
|
43
|
+
throw createBubblesError(message);
|
|
44
|
+
}
|
|
45
|
+
function getErrorMessage(error) {
|
|
46
|
+
return error instanceof Error ? error.message : String(error);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/internal/validation.ts
|
|
50
|
+
function isPlainObject(value) {
|
|
51
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
function getRequiredNonEmptyString(value, fieldName) {
|
|
54
|
+
if (typeof value !== "string") {
|
|
55
|
+
failWithBubblesError(`"${fieldName}" must be a string.`);
|
|
56
|
+
}
|
|
57
|
+
const trimmedValue = value.trim();
|
|
58
|
+
if (!trimmedValue) {
|
|
59
|
+
failWithBubblesError(`"${fieldName}" must not be empty.`);
|
|
60
|
+
}
|
|
61
|
+
return trimmedValue;
|
|
62
|
+
}
|
|
63
|
+
function normalizeIdentifier(value, fieldName) {
|
|
64
|
+
if (typeof value === "number") {
|
|
65
|
+
if (!Number.isFinite(value)) {
|
|
66
|
+
failWithBubblesError(
|
|
67
|
+
`"${fieldName}" must be a finite number when it is numeric.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return String(value);
|
|
71
|
+
}
|
|
72
|
+
if (typeof value !== "string") {
|
|
73
|
+
failWithBubblesError(`"${fieldName}" must be a string or number.`);
|
|
74
|
+
}
|
|
75
|
+
const trimmedValue = value.trim();
|
|
76
|
+
if (!trimmedValue) {
|
|
77
|
+
failWithBubblesError(`"${fieldName}" must not be empty.`);
|
|
78
|
+
}
|
|
79
|
+
return trimmedValue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/api/device-client.ts
|
|
83
|
+
var APP_KEY_HEADER_NAME = "X-App-Key";
|
|
84
|
+
var BubblesAppAuthenticationError = class extends Error {
|
|
85
|
+
constructor(cause) {
|
|
86
|
+
super("Bubbles app configuration/authentication failed: invalid app key for app.");
|
|
87
|
+
this.name = "BubblesAppAuthenticationError";
|
|
88
|
+
this.cause = cause;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
function createBubblesDeviceClient(options) {
|
|
92
|
+
const { appKey: rawAppKey, ...clientOptions } = options;
|
|
93
|
+
const appKey = rawAppKey ? getRequiredNonEmptyString(rawAppKey, "appKey") : null;
|
|
94
|
+
return new import_bubbles_js.DeviceClient({
|
|
95
|
+
...clientOptions,
|
|
96
|
+
baseUrl: getRequiredNonEmptyString(clientOptions.baseUrl, "apiBaseUrl"),
|
|
97
|
+
defaultHeaders: {
|
|
98
|
+
...clientOptions.defaultHeaders,
|
|
99
|
+
...appKey ? { [APP_KEY_HEADER_NAME]: appKey } : {}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/api/delivery-status.ts
|
|
105
|
+
var BUBBLES_DELIVERY_STATUSES = {
|
|
106
|
+
notificationReceived: "received",
|
|
107
|
+
notificationShown: "shown",
|
|
108
|
+
notificationClicked: "clicked",
|
|
109
|
+
notificationsDisabled: "disabled"
|
|
110
|
+
};
|
|
111
|
+
function normalizeError(value) {
|
|
112
|
+
if (value == null) {
|
|
113
|
+
return void 0;
|
|
114
|
+
}
|
|
115
|
+
if (typeof value !== "string") {
|
|
116
|
+
failWithBubblesError('"error" must be a string when present.');
|
|
117
|
+
}
|
|
118
|
+
const trimmedValue = value.trim();
|
|
119
|
+
if (!trimmedValue) {
|
|
120
|
+
failWithBubblesError('"error" must not be empty when present.');
|
|
121
|
+
}
|
|
122
|
+
return trimmedValue;
|
|
123
|
+
}
|
|
124
|
+
function normalizeStatus(value) {
|
|
125
|
+
if (value === void 0) {
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
const supportedValues = Object.values(BUBBLES_DELIVERY_STATUSES);
|
|
129
|
+
if (typeof value !== "string" || !supportedValues.includes(value)) {
|
|
130
|
+
failWithBubblesError(
|
|
131
|
+
`"status" must be one of: ${supportedValues.join(", ")}.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
function resolveDeviceClient(options) {
|
|
137
|
+
if (options.client) {
|
|
138
|
+
return options.client;
|
|
139
|
+
}
|
|
140
|
+
return createBubblesDeviceClient({
|
|
141
|
+
...options.clientOptions,
|
|
142
|
+
appKey: getRequiredNonEmptyString(options.appKey, "appKey"),
|
|
143
|
+
baseUrl: getRequiredNonEmptyString(options.apiBaseUrl, "apiBaseUrl")
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function withAppKeyHeader(appKey, requestOptions) {
|
|
147
|
+
return {
|
|
148
|
+
...requestOptions,
|
|
149
|
+
headers: {
|
|
150
|
+
...requestOptions?.headers,
|
|
151
|
+
"X-App-Key": getRequiredNonEmptyString(appKey, "appKey")
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function isInvalidAppKeyResponse(error) {
|
|
156
|
+
if (!error || typeof error !== "object") {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
const status = Reflect.get(error, "status");
|
|
160
|
+
const body = Reflect.get(error, "body");
|
|
161
|
+
return status === 401 && !!body && typeof body === "object" && !Array.isArray(body) && Reflect.get(body, "error") === "invalid app key for app";
|
|
162
|
+
}
|
|
163
|
+
function buildBubblesDeliveryStatusPayload(options) {
|
|
164
|
+
const status = normalizeStatus(options.status);
|
|
165
|
+
const error = normalizeError(options.error);
|
|
166
|
+
if (!status && !error) {
|
|
167
|
+
failWithBubblesError(
|
|
168
|
+
'A delivery status payload must include "status" and/or "error".'
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
...status ? { status } : {},
|
|
173
|
+
...error ? { error } : {}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async function postBubblesDeliveryStatus(options) {
|
|
177
|
+
const client = resolveDeviceClient(options);
|
|
178
|
+
try {
|
|
179
|
+
await client.postDeliveryStatus(
|
|
180
|
+
normalizeIdentifier(options.deviceId, "deviceId"),
|
|
181
|
+
normalizeIdentifier(options.notificationId, "notificationId"),
|
|
182
|
+
buildBubblesDeliveryStatusPayload(options),
|
|
183
|
+
withAppKeyHeader(options.appKey, options.requestOptions)
|
|
184
|
+
);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (isInvalidAppKeyResponse(error)) {
|
|
187
|
+
throw new BubblesAppAuthenticationError(error);
|
|
188
|
+
}
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/storage/device-state.ts
|
|
194
|
+
var import_expo_file_system = require("expo-file-system");
|
|
195
|
+
var STORED_DEVICE_STATE_FILE_NAME = "bubbles-notifications-expo-state.json";
|
|
196
|
+
var LEGACY_DEVICE_ID_FILE_NAME = "device-id.txt";
|
|
197
|
+
var STORED_DEVICE_STATE_VERSION = 1;
|
|
198
|
+
function getOptionalNonEmptyString(value, fieldName) {
|
|
199
|
+
if (value == null) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
if (typeof value !== "string") {
|
|
203
|
+
failWithBubblesError(`Stored "${fieldName}" must be a string when present.`);
|
|
204
|
+
}
|
|
205
|
+
const trimmedValue = value.trim();
|
|
206
|
+
return trimmedValue.length > 0 ? trimmedValue : null;
|
|
207
|
+
}
|
|
208
|
+
function getStoredDeviceStateFile() {
|
|
209
|
+
return new import_expo_file_system.File(import_expo_file_system.Paths.document, STORED_DEVICE_STATE_FILE_NAME);
|
|
210
|
+
}
|
|
211
|
+
function getLegacyDeviceIdFile() {
|
|
212
|
+
return new import_expo_file_system.File(import_expo_file_system.Paths.document, LEGACY_DEVICE_ID_FILE_NAME);
|
|
213
|
+
}
|
|
214
|
+
function readLegacyStoredDeviceId() {
|
|
215
|
+
const legacyDeviceIdFile = getLegacyDeviceIdFile();
|
|
216
|
+
if (!legacyDeviceIdFile.exists) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const storedDeviceId = legacyDeviceIdFile.textSync().trim();
|
|
220
|
+
return storedDeviceId.length > 0 ? storedDeviceId : null;
|
|
221
|
+
}
|
|
222
|
+
function normalizeStoredDeviceState(value, sourceDescription) {
|
|
223
|
+
if (!isPlainObject(value)) {
|
|
224
|
+
failWithBubblesError(
|
|
225
|
+
`Stored device state in ${sourceDescription} must be a JSON object.`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (value.version !== STORED_DEVICE_STATE_VERSION) {
|
|
229
|
+
failWithBubblesError(
|
|
230
|
+
`Stored device state in ${sourceDescription} uses unsupported schema version "${String(
|
|
231
|
+
value.version
|
|
232
|
+
)}".`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
deviceId: getOptionalNonEmptyString(value.deviceId, "deviceId"),
|
|
237
|
+
apiBaseUrl: getOptionalNonEmptyString(value.apiBaseUrl, "apiBaseUrl"),
|
|
238
|
+
appKey: getOptionalNonEmptyString(value.appKey, "appKey")
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function readStoredDeviceStateFileText() {
|
|
242
|
+
const storedDeviceStateFile = getStoredDeviceStateFile();
|
|
243
|
+
if (!storedDeviceStateFile.exists) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const contents = storedDeviceStateFile.textSync().trim();
|
|
247
|
+
return contents.length > 0 ? contents : null;
|
|
248
|
+
}
|
|
249
|
+
function withLegacyDeviceIdFallback(state) {
|
|
250
|
+
if (state.deviceId) {
|
|
251
|
+
return state;
|
|
252
|
+
}
|
|
253
|
+
const legacyDeviceId = readLegacyStoredDeviceId();
|
|
254
|
+
if (!legacyDeviceId) {
|
|
255
|
+
return state;
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
...state,
|
|
259
|
+
deviceId: legacyDeviceId
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function readStoredDeviceState() {
|
|
263
|
+
const storedDeviceStateFile = getStoredDeviceStateFile();
|
|
264
|
+
const rawState = readStoredDeviceStateFileText();
|
|
265
|
+
if (rawState === null) {
|
|
266
|
+
return withLegacyDeviceIdFallback({
|
|
267
|
+
deviceId: null,
|
|
268
|
+
apiBaseUrl: null,
|
|
269
|
+
appKey: null
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
let parsedState;
|
|
273
|
+
try {
|
|
274
|
+
parsedState = JSON.parse(rawState);
|
|
275
|
+
} catch {
|
|
276
|
+
failWithBubblesError(
|
|
277
|
+
`Stored device state at "${storedDeviceStateFile.uri}" is not valid JSON.`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return withLegacyDeviceIdFallback(
|
|
281
|
+
normalizeStoredDeviceState(parsedState, `"${storedDeviceStateFile.uri}"`)
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/storage/delivery-status-ledger.ts
|
|
286
|
+
var import_expo_file_system2 = require("expo-file-system");
|
|
287
|
+
var DELIVERY_STATUS_LEDGER_FILE_NAME = "bubbles-notifications-expo-delivery-status-ledger.json";
|
|
288
|
+
var DELIVERY_STATUS_LEDGER_VERSION = 1;
|
|
289
|
+
var MAX_POSTED_EVENT_KEYS = 200;
|
|
290
|
+
function getDeliveryStatusLedgerFile() {
|
|
291
|
+
return new import_expo_file_system2.File(import_expo_file_system2.Paths.document, DELIVERY_STATUS_LEDGER_FILE_NAME);
|
|
292
|
+
}
|
|
293
|
+
function getOptionalNonEmptyString2(value) {
|
|
294
|
+
if (value == null) {
|
|
295
|
+
return void 0;
|
|
296
|
+
}
|
|
297
|
+
if (typeof value !== "string") {
|
|
298
|
+
return void 0;
|
|
299
|
+
}
|
|
300
|
+
const trimmedValue = value.trim();
|
|
301
|
+
return trimmedValue.length > 0 ? trimmedValue : void 0;
|
|
302
|
+
}
|
|
303
|
+
function requireString(value, sourceDescription) {
|
|
304
|
+
if (typeof value !== "string") {
|
|
305
|
+
failWithBubblesError(`${sourceDescription} must be a string.`);
|
|
306
|
+
}
|
|
307
|
+
const trimmedValue = value.trim();
|
|
308
|
+
if (!trimmedValue) {
|
|
309
|
+
failWithBubblesError(`${sourceDescription} must not be empty.`);
|
|
310
|
+
}
|
|
311
|
+
return trimmedValue;
|
|
312
|
+
}
|
|
313
|
+
function buildStoredDeliveryStatusEventKey(options) {
|
|
314
|
+
return JSON.stringify([
|
|
315
|
+
options.notificationId,
|
|
316
|
+
options.status ?? null,
|
|
317
|
+
options.error ?? null,
|
|
318
|
+
options.actionId ?? null
|
|
319
|
+
]);
|
|
320
|
+
}
|
|
321
|
+
function createStoredDeliveryStatusEvent(options) {
|
|
322
|
+
const error = getOptionalNonEmptyString2(options.error);
|
|
323
|
+
const actionId = getOptionalNonEmptyString2(options.actionId);
|
|
324
|
+
return {
|
|
325
|
+
key: buildStoredDeliveryStatusEventKey({
|
|
326
|
+
notificationId: options.notificationId,
|
|
327
|
+
status: options.status,
|
|
328
|
+
error,
|
|
329
|
+
actionId
|
|
330
|
+
}),
|
|
331
|
+
notificationId: options.notificationId,
|
|
332
|
+
...options.status ? { status: options.status } : {},
|
|
333
|
+
...error ? { error } : {},
|
|
334
|
+
...actionId ? { actionId } : {},
|
|
335
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function normalizeStoredDeliveryStatusEvent(value, sourceDescription) {
|
|
339
|
+
if (!isPlainObject(value)) {
|
|
340
|
+
failWithBubblesError(`${sourceDescription} must be a JSON object.`);
|
|
341
|
+
}
|
|
342
|
+
const status = getOptionalNonEmptyString2(value.status);
|
|
343
|
+
const error = getOptionalNonEmptyString2(value.error);
|
|
344
|
+
const actionId = getOptionalNonEmptyString2(value.actionId);
|
|
345
|
+
return {
|
|
346
|
+
key: requireString(value.key, `${sourceDescription}.key`),
|
|
347
|
+
notificationId: requireString(
|
|
348
|
+
value.notificationId,
|
|
349
|
+
`${sourceDescription}.notificationId`
|
|
350
|
+
),
|
|
351
|
+
...status ? { status } : {},
|
|
352
|
+
...error ? { error } : {},
|
|
353
|
+
...actionId ? { actionId } : {},
|
|
354
|
+
createdAt: requireString(value.createdAt, `${sourceDescription}.createdAt`)
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function normalizeStringArray(value) {
|
|
358
|
+
if (!Array.isArray(value)) {
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
return value.map((item) => getOptionalNonEmptyString2(item)).filter((item) => item !== void 0);
|
|
362
|
+
}
|
|
363
|
+
function normalizeDeliveryStatusLedger(value, sourceDescription) {
|
|
364
|
+
if (!isPlainObject(value)) {
|
|
365
|
+
failWithBubblesError(
|
|
366
|
+
`Stored delivery status ledger in ${sourceDescription} must be a JSON object.`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
if (value.version !== DELIVERY_STATUS_LEDGER_VERSION) {
|
|
370
|
+
failWithBubblesError(
|
|
371
|
+
`Stored delivery status ledger in ${sourceDescription} uses unsupported schema version "${String(
|
|
372
|
+
value.version
|
|
373
|
+
)}".`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
pendingEvents: Array.isArray(value.pendingEvents) ? value.pendingEvents.map(
|
|
378
|
+
(event, index) => normalizeStoredDeliveryStatusEvent(
|
|
379
|
+
event,
|
|
380
|
+
`${sourceDescription}.pendingEvents[${index}]`
|
|
381
|
+
)
|
|
382
|
+
) : [],
|
|
383
|
+
postedEventKeys: normalizeStringArray(value.postedEventKeys).slice(
|
|
384
|
+
-MAX_POSTED_EVENT_KEYS
|
|
385
|
+
)
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function serializeDeliveryStatusLedger(ledger) {
|
|
389
|
+
return {
|
|
390
|
+
version: DELIVERY_STATUS_LEDGER_VERSION,
|
|
391
|
+
pendingEvents: ledger.pendingEvents,
|
|
392
|
+
postedEventKeys: ledger.postedEventKeys.slice(-MAX_POSTED_EVENT_KEYS)
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function readDeliveryStatusLedgerFileText() {
|
|
396
|
+
const ledgerFile = getDeliveryStatusLedgerFile();
|
|
397
|
+
if (!ledgerFile.exists) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
const contents = ledgerFile.textSync().trim();
|
|
401
|
+
return contents.length > 0 ? contents : null;
|
|
402
|
+
}
|
|
403
|
+
function ensureDeliveryStatusLedgerFile() {
|
|
404
|
+
const ledgerFile = getDeliveryStatusLedgerFile();
|
|
405
|
+
if (!ledgerFile.exists) {
|
|
406
|
+
ledgerFile.create({ intermediates: true });
|
|
407
|
+
}
|
|
408
|
+
return ledgerFile;
|
|
409
|
+
}
|
|
410
|
+
function readStoredDeliveryStatusLedger() {
|
|
411
|
+
const ledgerFile = getDeliveryStatusLedgerFile();
|
|
412
|
+
const rawLedger = readDeliveryStatusLedgerFileText();
|
|
413
|
+
if (rawLedger === null) {
|
|
414
|
+
return {
|
|
415
|
+
pendingEvents: [],
|
|
416
|
+
postedEventKeys: []
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
let parsedLedger;
|
|
420
|
+
try {
|
|
421
|
+
parsedLedger = JSON.parse(rawLedger);
|
|
422
|
+
} catch {
|
|
423
|
+
failWithBubblesError(
|
|
424
|
+
`Stored delivery status ledger at "${ledgerFile.uri}" is not valid JSON.`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
return normalizeDeliveryStatusLedger(
|
|
428
|
+
parsedLedger,
|
|
429
|
+
`"${ledgerFile.uri}"`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
function storeDeliveryStatusLedger(ledger) {
|
|
433
|
+
const normalizedLedger = {
|
|
434
|
+
pendingEvents: ledger.pendingEvents,
|
|
435
|
+
postedEventKeys: ledger.postedEventKeys.slice(-MAX_POSTED_EVENT_KEYS)
|
|
436
|
+
};
|
|
437
|
+
const ledgerFile = ensureDeliveryStatusLedgerFile();
|
|
438
|
+
ledgerFile.write(
|
|
439
|
+
JSON.stringify(serializeDeliveryStatusLedger(normalizedLedger), null, 2)
|
|
440
|
+
);
|
|
441
|
+
return normalizedLedger;
|
|
442
|
+
}
|
|
443
|
+
function enqueueStoredDeliveryStatusEvent(event) {
|
|
444
|
+
const ledger = readStoredDeliveryStatusLedger();
|
|
445
|
+
if (ledger.postedEventKeys.includes(event.key) || ledger.pendingEvents.some((pendingEvent) => pendingEvent.key === event.key)) {
|
|
446
|
+
return ledger;
|
|
447
|
+
}
|
|
448
|
+
return storeDeliveryStatusLedger({
|
|
449
|
+
...ledger,
|
|
450
|
+
pendingEvents: [...ledger.pendingEvents, event]
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
function markStoredDeliveryStatusEventPosted(event) {
|
|
454
|
+
const ledger = readStoredDeliveryStatusLedger();
|
|
455
|
+
const postedEventKeys = ledger.postedEventKeys.includes(event.key) ? ledger.postedEventKeys : [...ledger.postedEventKeys, event.key];
|
|
456
|
+
return storeDeliveryStatusLedger({
|
|
457
|
+
pendingEvents: ledger.pendingEvents.filter(
|
|
458
|
+
(pendingEvent) => pendingEvent.key !== event.key
|
|
459
|
+
),
|
|
460
|
+
postedEventKeys
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/notifications/delivery-status.ts
|
|
465
|
+
function warnMissingApiBaseUrl(source) {
|
|
466
|
+
console.warn(
|
|
467
|
+
`[@fishonfire/bubbles-expo] Skipping ${source} delivery-status update because "apiBaseUrl" has not been persisted yet.`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
function warnMissingAppKey(source) {
|
|
471
|
+
console.warn(
|
|
472
|
+
`[@fishonfire/bubbles-expo] Skipping ${source} delivery-status update because "appKey" has not been persisted yet.`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
async function postStoredBubblesDeliveryStatus(options) {
|
|
476
|
+
const storedDeviceState = options.storedDeviceState ?? readStoredDeviceState();
|
|
477
|
+
const { deviceId, apiBaseUrl, appKey } = storedDeviceState;
|
|
478
|
+
if (!options.notificationId) {
|
|
479
|
+
return storedDeviceState;
|
|
480
|
+
}
|
|
481
|
+
const event = createStoredDeliveryStatusEvent({
|
|
482
|
+
notificationId: options.notificationId,
|
|
483
|
+
status: options.status,
|
|
484
|
+
error: options.error,
|
|
485
|
+
actionId: options.actionId
|
|
486
|
+
});
|
|
487
|
+
enqueueStoredDeliveryStatusEvent(event);
|
|
488
|
+
if (!deviceId) {
|
|
489
|
+
return storedDeviceState;
|
|
490
|
+
}
|
|
491
|
+
if (!apiBaseUrl) {
|
|
492
|
+
warnMissingApiBaseUrl(options.source);
|
|
493
|
+
return storedDeviceState;
|
|
494
|
+
}
|
|
495
|
+
if (!appKey) {
|
|
496
|
+
warnMissingAppKey(options.source);
|
|
497
|
+
return storedDeviceState;
|
|
498
|
+
}
|
|
499
|
+
await flushStoredBubblesDeliveryStatuses({
|
|
500
|
+
apiBaseUrl,
|
|
501
|
+
appKey,
|
|
502
|
+
deviceId
|
|
503
|
+
});
|
|
504
|
+
return storedDeviceState;
|
|
505
|
+
}
|
|
506
|
+
async function flushStoredBubblesDeliveryStatuses(storedDeviceState = readStoredDeviceState()) {
|
|
507
|
+
const { deviceId, apiBaseUrl, appKey } = storedDeviceState;
|
|
508
|
+
if (!deviceId || !apiBaseUrl || !appKey) {
|
|
509
|
+
return storedDeviceState;
|
|
510
|
+
}
|
|
511
|
+
const { pendingEvents } = readStoredDeliveryStatusLedger();
|
|
512
|
+
for (const event of pendingEvents) {
|
|
513
|
+
await postStoredDeliveryStatusEvent(apiBaseUrl, appKey, deviceId, event);
|
|
514
|
+
markStoredDeliveryStatusEventPosted(event);
|
|
515
|
+
}
|
|
516
|
+
return storedDeviceState;
|
|
517
|
+
}
|
|
518
|
+
async function postStoredDeliveryStatusEvent(apiBaseUrl, appKey, deviceId, event) {
|
|
519
|
+
await postBubblesDeliveryStatus({
|
|
520
|
+
apiBaseUrl,
|
|
521
|
+
appKey,
|
|
522
|
+
deviceId,
|
|
523
|
+
notificationId: event.notificationId,
|
|
524
|
+
status: event.status,
|
|
525
|
+
error: event.error
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/notifications/local-notification.ts
|
|
530
|
+
var Notifications2 = __toESM(require("expo-notifications"));
|
|
531
|
+
|
|
532
|
+
// src/runtime/permissions.ts
|
|
533
|
+
var Notifications = __toESM(require("expo-notifications"));
|
|
534
|
+
var import_react_native = require("react-native");
|
|
535
|
+
var IOS_GRANTED_AUTHORIZATION_STATUSES = /* @__PURE__ */ new Set([
|
|
536
|
+
Notifications.IosAuthorizationStatus.AUTHORIZED,
|
|
537
|
+
Notifications.IosAuthorizationStatus.PROVISIONAL,
|
|
538
|
+
Notifications.IosAuthorizationStatus.EPHEMERAL
|
|
539
|
+
]);
|
|
540
|
+
function isNotificationPermissionGranted(permissions) {
|
|
541
|
+
if (import_react_native.Platform.OS === "ios" && permissions.ios?.status != null) {
|
|
542
|
+
return IOS_GRANTED_AUTHORIZATION_STATUSES.has(permissions.ios.status);
|
|
543
|
+
}
|
|
544
|
+
return permissions.granted;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/notifications/local-notification.ts
|
|
548
|
+
var DEFAULT_LOCAL_NOTIFICATION_TITLE = "Notification received";
|
|
549
|
+
var DEFAULT_LOCAL_NOTIFICATION_BODY = "Open the app to view this update.";
|
|
550
|
+
var DUPLICATE_NOTIFICATION_WINDOW_MS = 5e3;
|
|
551
|
+
var recentlyScheduledNotificationKeys = /* @__PURE__ */ new Map();
|
|
552
|
+
function getOptionalNonEmptyString3(value) {
|
|
553
|
+
if (typeof value !== "string") {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
const trimmedValue = value.trim();
|
|
557
|
+
return trimmedValue.length > 0 ? trimmedValue : null;
|
|
558
|
+
}
|
|
559
|
+
function buildDuplicateNotificationKey(payload) {
|
|
560
|
+
if (payload.notificationId) {
|
|
561
|
+
return `notification:${payload.notificationId}`;
|
|
562
|
+
}
|
|
563
|
+
const dataKeys = Object.keys(payload.data);
|
|
564
|
+
if (dataKeys.length === 0) {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
return `data:${JSON.stringify(payload.data)}`;
|
|
569
|
+
} catch {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function rememberScheduledNotificationKey(duplicateKey) {
|
|
574
|
+
const now = Date.now();
|
|
575
|
+
for (const [key, timestamp] of recentlyScheduledNotificationKeys) {
|
|
576
|
+
if (now - timestamp > DUPLICATE_NOTIFICATION_WINDOW_MS) {
|
|
577
|
+
recentlyScheduledNotificationKeys.delete(key);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const previousTimestamp = recentlyScheduledNotificationKeys.get(duplicateKey);
|
|
581
|
+
if (previousTimestamp !== void 0 && now - previousTimestamp <= DUPLICATE_NOTIFICATION_WINDOW_MS) {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
recentlyScheduledNotificationKeys.set(duplicateKey, now);
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
async function postLocalNotificationShownDeliveryStatus(options) {
|
|
588
|
+
try {
|
|
589
|
+
await postStoredBubblesDeliveryStatus({
|
|
590
|
+
source: options.source,
|
|
591
|
+
notificationId: options.payload.notificationId,
|
|
592
|
+
status: BUBBLES_DELIVERY_STATUSES.notificationShown
|
|
593
|
+
});
|
|
594
|
+
} catch (error) {
|
|
595
|
+
console.error(
|
|
596
|
+
`[@fishonfire/bubbles-expo] Failed to post ${options.source} shown delivery status.`,
|
|
597
|
+
error
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function buildBubblesLocalNotificationRequest(payload, options) {
|
|
602
|
+
return {
|
|
603
|
+
content: {
|
|
604
|
+
title: payload.messageTitle ?? getOptionalNonEmptyString3(options?.title) ?? DEFAULT_LOCAL_NOTIFICATION_TITLE,
|
|
605
|
+
body: payload.body ?? getOptionalNonEmptyString3(options?.body) ?? DEFAULT_LOCAL_NOTIFICATION_BODY,
|
|
606
|
+
data: payload.data
|
|
607
|
+
},
|
|
608
|
+
// Force app to trigger it 1 second from now instead of null.
|
|
609
|
+
// This prevents issues during background handling.
|
|
610
|
+
trigger: {
|
|
611
|
+
type: Notifications2.SchedulableTriggerInputTypes.TIME_INTERVAL,
|
|
612
|
+
seconds: 1
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
async function scheduleBubblesLocalNotification(options) {
|
|
617
|
+
if (!options.skipPermissionCheck) {
|
|
618
|
+
const permissions = await Notifications2.getPermissionsAsync();
|
|
619
|
+
const notificationsEnabled = isNotificationPermissionGranted(permissions);
|
|
620
|
+
if (!notificationsEnabled) {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const duplicateKey = buildDuplicateNotificationKey(options.payload);
|
|
625
|
+
if (duplicateKey !== null && !rememberScheduledNotificationKey(duplicateKey)) {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
const notificationRequest = buildBubblesLocalNotificationRequest(
|
|
629
|
+
options.payload,
|
|
630
|
+
options
|
|
631
|
+
);
|
|
632
|
+
const scheduledNotificationId = await Notifications2.scheduleNotificationAsync(
|
|
633
|
+
notificationRequest
|
|
634
|
+
);
|
|
635
|
+
await postLocalNotificationShownDeliveryStatus(options);
|
|
636
|
+
return scheduledNotificationId;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/notifications/payload.ts
|
|
640
|
+
function isPlainObject2(value) {
|
|
641
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
642
|
+
}
|
|
643
|
+
function getOptionalNonEmptyString4(value) {
|
|
644
|
+
if (typeof value !== "string") {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
const trimmedValue = value.trim();
|
|
648
|
+
return trimmedValue.length > 0 ? trimmedValue : null;
|
|
649
|
+
}
|
|
650
|
+
function getOptionalIdentifier(value) {
|
|
651
|
+
if (typeof value === "number") {
|
|
652
|
+
return Number.isFinite(value) ? String(value) : null;
|
|
653
|
+
}
|
|
654
|
+
return getOptionalNonEmptyString4(value);
|
|
655
|
+
}
|
|
656
|
+
function getBubblesNotificationData(value) {
|
|
657
|
+
return isPlainObject2(value) ? value : {};
|
|
658
|
+
}
|
|
659
|
+
function normalizeBubblesNotificationPayload(value) {
|
|
660
|
+
const data = getBubblesNotificationData(value);
|
|
661
|
+
return {
|
|
662
|
+
data,
|
|
663
|
+
notificationId: getOptionalIdentifier(data.notification_id),
|
|
664
|
+
messageTitle: getOptionalNonEmptyString4(data.message_title),
|
|
665
|
+
body: getOptionalNonEmptyString4(data.body),
|
|
666
|
+
source: getOptionalNonEmptyString4(data.source),
|
|
667
|
+
messageId: getOptionalIdentifier(data.message_id),
|
|
668
|
+
campaignId: getOptionalIdentifier(data.campaign_id),
|
|
669
|
+
url: getOptionalNonEmptyString4(data.url)
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/notifications/remote-message.ts
|
|
674
|
+
function getOptionalNonEmptyString5(value) {
|
|
675
|
+
if (typeof value !== "string") {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
const trimmedValue = value.trim();
|
|
679
|
+
return trimmedValue.length > 0 ? trimmedValue : null;
|
|
680
|
+
}
|
|
681
|
+
function getBubblesNotificationPayloadFromRemoteMessage(remoteMessage) {
|
|
682
|
+
const payload = normalizeBubblesNotificationPayload(
|
|
683
|
+
getBubblesNotificationData(remoteMessage.data)
|
|
684
|
+
);
|
|
685
|
+
return {
|
|
686
|
+
...payload,
|
|
687
|
+
messageTitle: payload.messageTitle ?? getOptionalNonEmptyString5(remoteMessage.notification?.title),
|
|
688
|
+
body: payload.body ?? getOptionalNonEmptyString5(remoteMessage.notification?.body)
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
function hasFirebaseDisplayNotification(remoteMessage) {
|
|
692
|
+
return getOptionalNonEmptyString5(remoteMessage.notification?.title) !== null || getOptionalNonEmptyString5(remoteMessage.notification?.body) !== null;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/background/firebase-message-handler.ts
|
|
696
|
+
var BACKGROUND_NOTIFICATION_SOURCE = "Firebase background message";
|
|
697
|
+
var DEFAULT_BACKGROUND_NOTIFICATION_TITLE = "Background notification received";
|
|
698
|
+
var DEFAULT_BACKGROUND_NOTIFICATION_BODY = "Open the app to view this update.";
|
|
699
|
+
function supportsFirebaseBackgroundMessages() {
|
|
700
|
+
return import_react_native2.Platform.OS === "android" || import_react_native2.Platform.OS === "ios";
|
|
701
|
+
}
|
|
702
|
+
function supportsBackgroundLocalNotificationPresentation() {
|
|
703
|
+
return import_react_native2.Platform.OS === "android";
|
|
704
|
+
}
|
|
705
|
+
function logBackgroundStatusError(description, error) {
|
|
706
|
+
console.error(
|
|
707
|
+
`[@fishonfire/bubbles-expo] Failed to post ${BACKGROUND_NOTIFICATION_SOURCE} ${description} delivery status.`,
|
|
708
|
+
error
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
function getBackgroundNotificationErrorMessage(error) {
|
|
712
|
+
if (!(error instanceof Error)) {
|
|
713
|
+
return "Failed to show notification";
|
|
714
|
+
}
|
|
715
|
+
return `Failed to show notification: ${getErrorMessage(error)}`;
|
|
716
|
+
}
|
|
717
|
+
async function postBackgroundDeliveryStatus(notificationId, status) {
|
|
718
|
+
try {
|
|
719
|
+
await postStoredBubblesDeliveryStatus({
|
|
720
|
+
source: BACKGROUND_NOTIFICATION_SOURCE,
|
|
721
|
+
notificationId,
|
|
722
|
+
status
|
|
723
|
+
});
|
|
724
|
+
} catch (error) {
|
|
725
|
+
logBackgroundStatusError(status, error);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async function postBackgroundDeliveryError(notificationId, errorMessage) {
|
|
729
|
+
try {
|
|
730
|
+
await postStoredBubblesDeliveryStatus({
|
|
731
|
+
source: BACKGROUND_NOTIFICATION_SOURCE,
|
|
732
|
+
notificationId,
|
|
733
|
+
error: errorMessage
|
|
734
|
+
});
|
|
735
|
+
} catch (error) {
|
|
736
|
+
logBackgroundStatusError("error", error);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function postBackgroundNotificationReceived(notificationId) {
|
|
740
|
+
return postBackgroundDeliveryStatus(
|
|
741
|
+
notificationId,
|
|
742
|
+
BUBBLES_DELIVERY_STATUSES.notificationReceived
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
function postBackgroundNotificationsDisabled(notificationId) {
|
|
746
|
+
return postBackgroundDeliveryStatus(
|
|
747
|
+
notificationId,
|
|
748
|
+
BUBBLES_DELIVERY_STATUSES.notificationsDisabled
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
var hasRegisteredFirebaseBackgroundMessageHandler = false;
|
|
752
|
+
var handleBubblesFirebaseBackgroundMessage = async (remoteMessage) => {
|
|
753
|
+
const payload = getBubblesNotificationPayloadFromRemoteMessage(remoteMessage);
|
|
754
|
+
const permissions = await Notifications3.getPermissionsAsync();
|
|
755
|
+
const notificationsEnabled = isNotificationPermissionGranted(permissions);
|
|
756
|
+
if (!notificationsEnabled) {
|
|
757
|
+
await postBackgroundNotificationsDisabled(payload.notificationId);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
await postBackgroundNotificationReceived(payload.notificationId);
|
|
761
|
+
if (hasFirebaseDisplayNotification(remoteMessage)) {
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (!supportsBackgroundLocalNotificationPresentation()) {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
try {
|
|
768
|
+
const scheduledNotificationId = await scheduleBubblesLocalNotification({
|
|
769
|
+
source: BACKGROUND_NOTIFICATION_SOURCE,
|
|
770
|
+
payload,
|
|
771
|
+
title: DEFAULT_BACKGROUND_NOTIFICATION_TITLE,
|
|
772
|
+
body: DEFAULT_BACKGROUND_NOTIFICATION_BODY,
|
|
773
|
+
skipPermissionCheck: true
|
|
774
|
+
});
|
|
775
|
+
if (scheduledNotificationId === null) {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
} catch (error) {
|
|
779
|
+
await postBackgroundDeliveryError(
|
|
780
|
+
payload.notificationId,
|
|
781
|
+
getBackgroundNotificationErrorMessage(error)
|
|
782
|
+
);
|
|
783
|
+
throw error;
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
function registerBubblesFirebaseBackgroundMessageHandler() {
|
|
787
|
+
if (hasRegisteredFirebaseBackgroundMessageHandler) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (!supportsFirebaseBackgroundMessages()) {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
try {
|
|
794
|
+
(0, import_messaging.setBackgroundMessageHandler)(
|
|
795
|
+
(0, import_messaging.getMessaging)(),
|
|
796
|
+
handleBubblesFirebaseBackgroundMessage
|
|
797
|
+
);
|
|
798
|
+
hasRegisteredFirebaseBackgroundMessageHandler = true;
|
|
799
|
+
} catch (error) {
|
|
800
|
+
console.error(
|
|
801
|
+
"[@fishonfire/bubbles-expo] Failed to register Firebase background message handler.",
|
|
802
|
+
error
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/background/setup.ts
|
|
808
|
+
var hasRegisteredBubblesBackgroundHandlers = false;
|
|
809
|
+
function registerBubblesBackgroundHandlers() {
|
|
810
|
+
if (hasRegisteredBubblesBackgroundHandlers) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
registerBubblesFirebaseBackgroundMessageHandler();
|
|
814
|
+
hasRegisteredBubblesBackgroundHandlers = true;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/register-task.ts
|
|
818
|
+
registerBubblesBackgroundHandlers();
|