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