@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
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
|
+
});
|