@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.mjs
ADDED
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ANDROID_CHANNEL_IMPORTANCE_VALUES,
|
|
3
|
+
BUBBLES_DELIVERY_STATUSES,
|
|
4
|
+
BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY,
|
|
5
|
+
BubblesAppAuthenticationError,
|
|
6
|
+
buildBubblesDeliveryStatusPayload,
|
|
7
|
+
buildCreateDevicePayload,
|
|
8
|
+
buildStoredDeliveryStatusEventKey,
|
|
9
|
+
buildUpdateDevicePayload,
|
|
10
|
+
createBubblesDevice,
|
|
11
|
+
createBubblesDeviceClient,
|
|
12
|
+
createStoredDeliveryStatusEvent,
|
|
13
|
+
describeNotificationPermissionStatus,
|
|
14
|
+
enqueueStoredDeliveryStatusEvent,
|
|
15
|
+
ensureDefaultNotificationChannel,
|
|
16
|
+
ensureNotificationPermissions,
|
|
17
|
+
extractDeviceIdFromDeviceResponse,
|
|
18
|
+
failWithBubblesError,
|
|
19
|
+
flushStoredBubblesDeliveryStatuses,
|
|
20
|
+
getAndroidNotificationChannelImportance,
|
|
21
|
+
getBubblesNotificationDataFromNotification,
|
|
22
|
+
getBubblesNotificationPayloadFromRemoteMessage,
|
|
23
|
+
getBubblesNotificationsRuntimeConfig,
|
|
24
|
+
getDefaultNotificationChannelConfig,
|
|
25
|
+
getErrorMessage,
|
|
26
|
+
getNotificationPermissions,
|
|
27
|
+
getRequiredNonEmptyString,
|
|
28
|
+
getRequiredNonEmptyStringFromSource,
|
|
29
|
+
getSupportedPlatform,
|
|
30
|
+
hasFirebaseDisplayNotification,
|
|
31
|
+
isNotificationPermissionGranted,
|
|
32
|
+
markStoredDeliveryStatusEventPosted,
|
|
33
|
+
normalizeBubblesNotificationPayload,
|
|
34
|
+
patchStoredDeviceState,
|
|
35
|
+
postBubblesDeliveryStatus,
|
|
36
|
+
postStoredBubblesDeliveryStatus,
|
|
37
|
+
readStoredApiBaseUrl,
|
|
38
|
+
readStoredAppKey,
|
|
39
|
+
readStoredDeliveryStatusLedger,
|
|
40
|
+
readStoredDeviceId,
|
|
41
|
+
readStoredDeviceState,
|
|
42
|
+
registerBubblesBackgroundHandlers,
|
|
43
|
+
scheduleBubblesLocalNotification,
|
|
44
|
+
storeApiBaseUrl,
|
|
45
|
+
storeAppKey,
|
|
46
|
+
storeDeliveryStatusLedger,
|
|
47
|
+
storeDeviceId,
|
|
48
|
+
storeDeviceState,
|
|
49
|
+
syncBubblesDevice,
|
|
50
|
+
updateBubblesDevice
|
|
51
|
+
} from "./chunk-6NPXFGQX.mjs";
|
|
52
|
+
|
|
53
|
+
// src/runtime/tokens.ts
|
|
54
|
+
import {
|
|
55
|
+
getMessaging,
|
|
56
|
+
getToken,
|
|
57
|
+
isDeviceRegisteredForRemoteMessages,
|
|
58
|
+
registerDeviceForRemoteMessages
|
|
59
|
+
} from "@react-native-firebase/messaging";
|
|
60
|
+
import { Platform } from "react-native";
|
|
61
|
+
async function getFirebaseMessagingToken(platform) {
|
|
62
|
+
try {
|
|
63
|
+
const messagingInstance = getMessaging();
|
|
64
|
+
if (!isDeviceRegisteredForRemoteMessages(messagingInstance)) {
|
|
65
|
+
await registerDeviceForRemoteMessages(messagingInstance);
|
|
66
|
+
}
|
|
67
|
+
const token = await getToken(messagingInstance);
|
|
68
|
+
return getRequiredNonEmptyStringFromSource(
|
|
69
|
+
token,
|
|
70
|
+
`${platform} FCM token from Firebase Messaging`
|
|
71
|
+
);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
const reason = getErrorMessage(error);
|
|
74
|
+
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.";
|
|
75
|
+
failWithBubblesError(
|
|
76
|
+
`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}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function getDeviceRegistrationState(options) {
|
|
81
|
+
const platform = getSupportedPlatform(Platform.OS);
|
|
82
|
+
const permissions = await getNotificationPermissions(options);
|
|
83
|
+
const permissionStatus = describeNotificationPermissionStatus(permissions);
|
|
84
|
+
const notificationsEnabled = isNotificationPermissionGranted(permissions);
|
|
85
|
+
if (!notificationsEnabled) {
|
|
86
|
+
return {
|
|
87
|
+
platform,
|
|
88
|
+
tokenType: null,
|
|
89
|
+
token: null,
|
|
90
|
+
permissionStatus,
|
|
91
|
+
notificationsEnabled: false
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const token = await getFirebaseMessagingToken(platform);
|
|
95
|
+
return {
|
|
96
|
+
platform,
|
|
97
|
+
tokenType: "fcm",
|
|
98
|
+
token,
|
|
99
|
+
permissionStatus,
|
|
100
|
+
notificationsEnabled: true
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function getDeviceToken(options) {
|
|
104
|
+
const registrationState = await getDeviceRegistrationState(options);
|
|
105
|
+
if (!registrationState.notificationsEnabled || !registrationState.token) {
|
|
106
|
+
failWithBubblesError("Push notification permission was not granted.");
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
platform: registrationState.platform,
|
|
110
|
+
tokenType: "fcm",
|
|
111
|
+
token: registrationState.token
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async function getFCMToken(options) {
|
|
115
|
+
const tokenResult = await getDeviceToken(options);
|
|
116
|
+
return tokenResult.token;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/runtime/BubblesNotificationsProvider.tsx
|
|
120
|
+
import { useCallback as useCallback2, useEffect, useRef as useRef2, useState } from "react";
|
|
121
|
+
|
|
122
|
+
// src/runtime/context.ts
|
|
123
|
+
import { createContext } from "react";
|
|
124
|
+
var BubblesNotificationsContext = createContext(null);
|
|
125
|
+
|
|
126
|
+
// src/runtime/notification-handler.ts
|
|
127
|
+
import * as Notifications from "expo-notifications";
|
|
128
|
+
var DEFAULT_FOREGROUND_PRESENTATION = {
|
|
129
|
+
shouldShowBanner: true,
|
|
130
|
+
shouldShowList: true,
|
|
131
|
+
shouldPlaySound: false,
|
|
132
|
+
shouldSetBadge: false
|
|
133
|
+
};
|
|
134
|
+
function applyBubblesNotificationHandler(options) {
|
|
135
|
+
Notifications.setNotificationHandler({
|
|
136
|
+
handleNotification: async (notification) => {
|
|
137
|
+
const presentation = {
|
|
138
|
+
shouldShowBanner: options?.shouldShowBanner ?? DEFAULT_FOREGROUND_PRESENTATION.shouldShowBanner,
|
|
139
|
+
shouldShowList: options?.shouldShowList ?? DEFAULT_FOREGROUND_PRESENTATION.shouldShowList,
|
|
140
|
+
shouldPlaySound: options?.shouldPlaySound ?? DEFAULT_FOREGROUND_PRESENTATION.shouldPlaySound,
|
|
141
|
+
shouldSetBadge: options?.shouldSetBadge ?? DEFAULT_FOREGROUND_PRESENTATION.shouldSetBadge
|
|
142
|
+
};
|
|
143
|
+
return presentation;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function clearBubblesNotificationHandler() {
|
|
148
|
+
Notifications.setNotificationHandler(null);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/runtime/response-observer.ts
|
|
152
|
+
import * as Notifications2 from "expo-notifications";
|
|
153
|
+
function buildNotificationResponseKey(response) {
|
|
154
|
+
const payload = normalizeBubblesNotificationPayload(
|
|
155
|
+
getBubblesNotificationDataFromNotification(response.notification)
|
|
156
|
+
);
|
|
157
|
+
if (payload.notificationId) {
|
|
158
|
+
return `notification:${payload.notificationId}:action:${response.actionIdentifier}`;
|
|
159
|
+
}
|
|
160
|
+
return `request:${response.notification.request.identifier}:action:${response.actionIdentifier}`;
|
|
161
|
+
}
|
|
162
|
+
function logNotificationResponseError(description, error) {
|
|
163
|
+
console.error(
|
|
164
|
+
`[@fishonfire/bubbles-expo] Failed during notification response ${description}.`,
|
|
165
|
+
error
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
function buildObservedNotificationResponse(response, storedDeviceState) {
|
|
169
|
+
const payload = normalizeBubblesNotificationPayload(
|
|
170
|
+
getBubblesNotificationDataFromNotification(response.notification)
|
|
171
|
+
);
|
|
172
|
+
return {
|
|
173
|
+
notificationId: payload.notificationId,
|
|
174
|
+
event: {
|
|
175
|
+
notification: response.notification,
|
|
176
|
+
url: payload.url,
|
|
177
|
+
notificationId: payload.notificationId,
|
|
178
|
+
deviceId: storedDeviceState.deviceId,
|
|
179
|
+
data: payload.data
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function postNotificationClickedDeliveryStatus(storedDeviceState, notificationId, actionId) {
|
|
184
|
+
void postStoredBubblesDeliveryStatus({
|
|
185
|
+
source: "notification response",
|
|
186
|
+
storedDeviceState,
|
|
187
|
+
notificationId,
|
|
188
|
+
status: BUBBLES_DELIVERY_STATUSES.notificationClicked,
|
|
189
|
+
actionId
|
|
190
|
+
}).catch((error) => {
|
|
191
|
+
logNotificationResponseError("delivery-status update", error);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function handleNotificationResponse(response, options, source) {
|
|
195
|
+
const storedDeviceState = readStoredDeviceState();
|
|
196
|
+
const observedResponse = buildObservedNotificationResponse(
|
|
197
|
+
response,
|
|
198
|
+
storedDeviceState
|
|
199
|
+
);
|
|
200
|
+
postNotificationClickedDeliveryStatus(
|
|
201
|
+
storedDeviceState,
|
|
202
|
+
observedResponse.notificationId,
|
|
203
|
+
response.actionIdentifier
|
|
204
|
+
);
|
|
205
|
+
options.onNotificationResponse?.(observedResponse.event);
|
|
206
|
+
}
|
|
207
|
+
function processStartupNotificationResponse(processNotificationResponse) {
|
|
208
|
+
try {
|
|
209
|
+
const lastResponse = Notifications2.getLastNotificationResponse();
|
|
210
|
+
if (!lastResponse) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
processNotificationResponse(lastResponse);
|
|
214
|
+
Notifications2.clearLastNotificationResponse();
|
|
215
|
+
} catch (error) {
|
|
216
|
+
logNotificationResponseError("initial response handling", error);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function observeBubblesNotificationResponses(options) {
|
|
220
|
+
let lastHandledResponseKey = null;
|
|
221
|
+
const processNotificationResponse = (response) => {
|
|
222
|
+
const responseKey = buildNotificationResponseKey(response);
|
|
223
|
+
if (lastHandledResponseKey === responseKey) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
lastHandledResponseKey = responseKey;
|
|
227
|
+
handleNotificationResponse(response, options, "listener");
|
|
228
|
+
};
|
|
229
|
+
processStartupNotificationResponse((response) => {
|
|
230
|
+
const responseKey = buildNotificationResponseKey(response);
|
|
231
|
+
lastHandledResponseKey = responseKey;
|
|
232
|
+
handleNotificationResponse(response, options, "startup");
|
|
233
|
+
});
|
|
234
|
+
const subscription = Notifications2.addNotificationResponseReceivedListener(
|
|
235
|
+
processNotificationResponse
|
|
236
|
+
);
|
|
237
|
+
return () => {
|
|
238
|
+
subscription.remove();
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/runtime/presentation.ts
|
|
243
|
+
function applyBubblesForegroundPresentation(options) {
|
|
244
|
+
applyBubblesNotificationHandler(options);
|
|
245
|
+
}
|
|
246
|
+
function clearBubblesForegroundPresentation() {
|
|
247
|
+
clearBubblesNotificationHandler();
|
|
248
|
+
}
|
|
249
|
+
function observeBubblesNotificationOpenEvents(options) {
|
|
250
|
+
return observeBubblesNotificationResponses(options);
|
|
251
|
+
}
|
|
252
|
+
async function prepareBubblesNotificationPresentation(options) {
|
|
253
|
+
return ensureDefaultNotificationChannel(options);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/runtime/foreground-message-observer.ts
|
|
257
|
+
import {
|
|
258
|
+
getMessaging as getMessaging2,
|
|
259
|
+
onMessage
|
|
260
|
+
} from "@react-native-firebase/messaging";
|
|
261
|
+
import { Platform as Platform2 } from "react-native";
|
|
262
|
+
function supportsFirebaseForegroundMessages() {
|
|
263
|
+
return Platform2.OS === "android" || Platform2.OS === "ios";
|
|
264
|
+
}
|
|
265
|
+
async function handleFirebaseForegroundMessage(remoteMessage) {
|
|
266
|
+
const payload = getBubblesNotificationPayloadFromRemoteMessage(remoteMessage);
|
|
267
|
+
if (!hasFirebaseDisplayNotification(remoteMessage)) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
await scheduleBubblesLocalNotification({
|
|
271
|
+
source: "Firebase foreground message",
|
|
272
|
+
payload
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
function observeBubblesForegroundMessages() {
|
|
276
|
+
if (!supportsFirebaseForegroundMessages()) {
|
|
277
|
+
return () => {
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const unsubscribe = onMessage(
|
|
281
|
+
getMessaging2(),
|
|
282
|
+
(remoteMessage) => {
|
|
283
|
+
void handleFirebaseForegroundMessage(remoteMessage).catch(
|
|
284
|
+
(error) => {
|
|
285
|
+
console.error(
|
|
286
|
+
"[@fishonfire/bubbles-expo] Failed while handling a Firebase foreground message.",
|
|
287
|
+
error
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
return () => {
|
|
294
|
+
unsubscribe();
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/runtime/installations.ts
|
|
299
|
+
import { getId, getInstallations } from "@react-native-firebase/installations";
|
|
300
|
+
async function getFirebaseInstallationId(platform) {
|
|
301
|
+
try {
|
|
302
|
+
const installationsInstance = getInstallations();
|
|
303
|
+
const installationId = await getId(installationsInstance);
|
|
304
|
+
return getRequiredNonEmptyStringFromSource(
|
|
305
|
+
installationId,
|
|
306
|
+
`${platform} Firebase installation id from Firebase Installations`
|
|
307
|
+
);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
const reason = getErrorMessage(error);
|
|
310
|
+
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.";
|
|
311
|
+
failWithBubblesError(
|
|
312
|
+
`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}`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/runtime/transport.ts
|
|
318
|
+
function observeBubblesForegroundRemoteMessages() {
|
|
319
|
+
return observeBubblesForegroundMessages();
|
|
320
|
+
}
|
|
321
|
+
async function getBubblesDeviceRegistrationState(options) {
|
|
322
|
+
return getDeviceRegistrationState(options);
|
|
323
|
+
}
|
|
324
|
+
async function getBubblesInstallationId(platform) {
|
|
325
|
+
return getFirebaseInstallationId(platform);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/runtime/sync-device.ts
|
|
329
|
+
var BubblesNotificationsSyncError = class extends Error {
|
|
330
|
+
constructor(message, snapshot, cause) {
|
|
331
|
+
super(message);
|
|
332
|
+
this.name = "BubblesNotificationsSyncError";
|
|
333
|
+
this.snapshot = snapshot;
|
|
334
|
+
this.cause = cause;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
function fail(message) {
|
|
338
|
+
throw new Error(`[@fishonfire/bubbles-expo] ${message}`);
|
|
339
|
+
}
|
|
340
|
+
function toError(error) {
|
|
341
|
+
if (error instanceof Error) {
|
|
342
|
+
return error;
|
|
343
|
+
}
|
|
344
|
+
return new Error(String(error));
|
|
345
|
+
}
|
|
346
|
+
function getRequiredNonEmptyString2(value, fieldName) {
|
|
347
|
+
if (typeof value !== "string") {
|
|
348
|
+
fail(`"${fieldName}" must be a string.`);
|
|
349
|
+
}
|
|
350
|
+
const trimmedValue = value.trim();
|
|
351
|
+
if (!trimmedValue) {
|
|
352
|
+
fail(`"${fieldName}" must not be empty.`);
|
|
353
|
+
}
|
|
354
|
+
return trimmedValue;
|
|
355
|
+
}
|
|
356
|
+
function buildRuntimeSnapshot(deviceId, registrationState) {
|
|
357
|
+
return {
|
|
358
|
+
deviceId,
|
|
359
|
+
pushToken: registrationState.token,
|
|
360
|
+
tokenType: registrationState.tokenType,
|
|
361
|
+
permissionStatus: registrationState.permissionStatus,
|
|
362
|
+
notificationsEnabled: registrationState.notificationsEnabled
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
async function syncDeviceRegistrationState(options) {
|
|
366
|
+
const apiBaseUrl = getRequiredNonEmptyString2(options.apiBaseUrl, "apiBaseUrl");
|
|
367
|
+
const appKey = getRequiredNonEmptyString2(options.appKey, "appKey");
|
|
368
|
+
storeApiBaseUrl(apiBaseUrl);
|
|
369
|
+
storeAppKey(appKey);
|
|
370
|
+
const snapshot = buildRuntimeSnapshot(
|
|
371
|
+
options.deviceId,
|
|
372
|
+
options.registrationState
|
|
373
|
+
);
|
|
374
|
+
try {
|
|
375
|
+
const installationId = options.registrationState.notificationsEnabled ? await getBubblesInstallationId(options.registrationState.platform) : null;
|
|
376
|
+
const syncResult = await syncBubblesDevice({
|
|
377
|
+
apiBaseUrl,
|
|
378
|
+
appId: options.appId,
|
|
379
|
+
appKey,
|
|
380
|
+
userId: options.userId,
|
|
381
|
+
aliasing: options.aliasing,
|
|
382
|
+
appVersion: options.appVersion,
|
|
383
|
+
deviceId: options.deviceId,
|
|
384
|
+
platform: options.registrationState.platform,
|
|
385
|
+
pushToken: options.registrationState.token,
|
|
386
|
+
fid: installationId,
|
|
387
|
+
notificationsEnabled: options.registrationState.notificationsEnabled
|
|
388
|
+
});
|
|
389
|
+
const nextDeviceId = syncResult.deviceId ?? options.deviceId;
|
|
390
|
+
storeDeviceId(nextDeviceId);
|
|
391
|
+
return {
|
|
392
|
+
...snapshot,
|
|
393
|
+
action: syncResult.action,
|
|
394
|
+
deviceId: nextDeviceId
|
|
395
|
+
};
|
|
396
|
+
} catch (error) {
|
|
397
|
+
const normalizedError = toError(error);
|
|
398
|
+
throw new BubblesNotificationsSyncError(
|
|
399
|
+
normalizedError.message,
|
|
400
|
+
snapshot,
|
|
401
|
+
error
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function syncExistingBubblesDevice(options) {
|
|
406
|
+
const registrationState = await getBubblesDeviceRegistrationState({
|
|
407
|
+
requestPermissions: false
|
|
408
|
+
});
|
|
409
|
+
return syncDeviceRegistrationState({
|
|
410
|
+
...options,
|
|
411
|
+
deviceId: options.deviceId,
|
|
412
|
+
registrationState
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/runtime/register-device.ts
|
|
417
|
+
var DEFAULT_PERMISSION_REQUEST_OPTIONS = {
|
|
418
|
+
ios: {
|
|
419
|
+
allowAlert: true,
|
|
420
|
+
allowBadge: true,
|
|
421
|
+
allowSound: true
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
function buildRegistrationOptions(options) {
|
|
425
|
+
if (options?.requestPermissions === false) {
|
|
426
|
+
return {
|
|
427
|
+
requestPermissions: false,
|
|
428
|
+
permissionRequestOptions: options.permissionRequestOptions
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
requestPermissions: options?.requestPermissions,
|
|
433
|
+
permissionRequestOptions: options?.permissionRequestOptions ?? DEFAULT_PERMISSION_REQUEST_OPTIONS
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
async function registerBubblesDevice(options) {
|
|
437
|
+
const registrationState = await getBubblesDeviceRegistrationState(
|
|
438
|
+
buildRegistrationOptions(options.registrationOptions)
|
|
439
|
+
);
|
|
440
|
+
return syncDeviceRegistrationState({
|
|
441
|
+
...options,
|
|
442
|
+
deviceId: options.deviceId,
|
|
443
|
+
registrationState
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/runtime/provider-operation-queue.ts
|
|
448
|
+
import {
|
|
449
|
+
useCallback,
|
|
450
|
+
useRef
|
|
451
|
+
} from "react";
|
|
452
|
+
|
|
453
|
+
// src/runtime/provider-state.ts
|
|
454
|
+
function createInitialRuntimeState(deviceId, error) {
|
|
455
|
+
return {
|
|
456
|
+
deviceId,
|
|
457
|
+
pushToken: null,
|
|
458
|
+
tokenType: null,
|
|
459
|
+
permissionStatus: "unknown",
|
|
460
|
+
notificationsEnabled: false,
|
|
461
|
+
isSyncing: false,
|
|
462
|
+
error
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function normalizeProviderError(error) {
|
|
466
|
+
if (error instanceof Error) {
|
|
467
|
+
return error;
|
|
468
|
+
}
|
|
469
|
+
return new Error(String(error));
|
|
470
|
+
}
|
|
471
|
+
function normalizeUserId(value) {
|
|
472
|
+
if (value === null) {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
const trimmedValue = value.trim();
|
|
476
|
+
return trimmedValue.length > 0 ? trimmedValue : null;
|
|
477
|
+
}
|
|
478
|
+
function getAutomaticSyncSignature(input) {
|
|
479
|
+
return JSON.stringify({
|
|
480
|
+
appId: input.appId,
|
|
481
|
+
appKey: input.appKey,
|
|
482
|
+
apiBaseUrl: input.apiBaseUrl,
|
|
483
|
+
userId: input.userId,
|
|
484
|
+
aliasing: input.aliasing ?? [],
|
|
485
|
+
appVersion: input.appVersion ?? null,
|
|
486
|
+
deviceId: input.deviceId
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
function getRuntimeStateUpdate(snapshot) {
|
|
490
|
+
return {
|
|
491
|
+
deviceId: snapshot.deviceId,
|
|
492
|
+
pushToken: snapshot.pushToken,
|
|
493
|
+
tokenType: snapshot.tokenType,
|
|
494
|
+
permissionStatus: snapshot.permissionStatus,
|
|
495
|
+
notificationsEnabled: snapshot.notificationsEnabled
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function getInitialRuntimeState() {
|
|
499
|
+
try {
|
|
500
|
+
return createInitialRuntimeState(readStoredDeviceId(), null);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
return createInitialRuntimeState(
|
|
503
|
+
null,
|
|
504
|
+
normalizeProviderError(error)
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/runtime/provider-operation-queue.ts
|
|
510
|
+
function useBubblesRuntimeOperationQueue(setState) {
|
|
511
|
+
const isMountedRef = useRef(true);
|
|
512
|
+
const pendingOperationsRef = useRef(0);
|
|
513
|
+
const operationQueueRef = useRef(Promise.resolve());
|
|
514
|
+
const enqueueOperation = useCallback((operation) => {
|
|
515
|
+
pendingOperationsRef.current += 1;
|
|
516
|
+
setState((currentState) => ({
|
|
517
|
+
...currentState,
|
|
518
|
+
isSyncing: true,
|
|
519
|
+
error: null
|
|
520
|
+
}));
|
|
521
|
+
const queuedOperation = operationQueueRef.current.catch(() => void 0).then(operation);
|
|
522
|
+
operationQueueRef.current = queuedOperation.then(
|
|
523
|
+
() => void 0,
|
|
524
|
+
() => void 0
|
|
525
|
+
);
|
|
526
|
+
return queuedOperation.then(
|
|
527
|
+
(result) => {
|
|
528
|
+
pendingOperationsRef.current -= 1;
|
|
529
|
+
if (!isMountedRef.current) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
setState((currentState) => ({
|
|
533
|
+
...currentState,
|
|
534
|
+
...result ?? {},
|
|
535
|
+
isSyncing: pendingOperationsRef.current > 0,
|
|
536
|
+
error: null
|
|
537
|
+
}));
|
|
538
|
+
},
|
|
539
|
+
(error) => {
|
|
540
|
+
pendingOperationsRef.current -= 1;
|
|
541
|
+
const normalizedError = normalizeProviderError(error);
|
|
542
|
+
const snapshot = error instanceof BubblesNotificationsSyncError ? error.snapshot : void 0;
|
|
543
|
+
if (isMountedRef.current) {
|
|
544
|
+
setState((currentState) => ({
|
|
545
|
+
...currentState,
|
|
546
|
+
...snapshot ? getRuntimeStateUpdate(snapshot) : {},
|
|
547
|
+
isSyncing: pendingOperationsRef.current > 0,
|
|
548
|
+
error: normalizedError
|
|
549
|
+
}));
|
|
550
|
+
}
|
|
551
|
+
throw normalizedError;
|
|
552
|
+
}
|
|
553
|
+
);
|
|
554
|
+
}, [setState]);
|
|
555
|
+
return {
|
|
556
|
+
enqueueOperation,
|
|
557
|
+
isMountedRef
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/runtime/BubblesNotificationsProvider.tsx
|
|
562
|
+
import { jsx } from "react/jsx-runtime";
|
|
563
|
+
function BubblesNotificationsProvider(props) {
|
|
564
|
+
const {
|
|
565
|
+
appId,
|
|
566
|
+
appKey,
|
|
567
|
+
apiBaseUrl,
|
|
568
|
+
ready,
|
|
569
|
+
userId,
|
|
570
|
+
aliasing,
|
|
571
|
+
appVersion,
|
|
572
|
+
onLogout,
|
|
573
|
+
foregroundPresentation,
|
|
574
|
+
onNotificationResponse,
|
|
575
|
+
children
|
|
576
|
+
} = props;
|
|
577
|
+
const normalizedUserId = normalizeUserId(userId);
|
|
578
|
+
const [state, setState] = useState(getInitialRuntimeState);
|
|
579
|
+
const { enqueueOperation, isMountedRef } = useBubblesRuntimeOperationQueue(setState);
|
|
580
|
+
const lastAutomaticSyncSignatureRef = useRef2(null);
|
|
581
|
+
const previousUserIdRef = useRef2(normalizedUserId);
|
|
582
|
+
const buildAutomaticSyncSignature = useCallback2((deviceId) => {
|
|
583
|
+
return getAutomaticSyncSignature({
|
|
584
|
+
appId,
|
|
585
|
+
appKey,
|
|
586
|
+
apiBaseUrl,
|
|
587
|
+
userId: normalizedUserId,
|
|
588
|
+
aliasing,
|
|
589
|
+
appVersion,
|
|
590
|
+
deviceId
|
|
591
|
+
});
|
|
592
|
+
}, [
|
|
593
|
+
appId,
|
|
594
|
+
appKey,
|
|
595
|
+
apiBaseUrl,
|
|
596
|
+
normalizedUserId,
|
|
597
|
+
aliasing,
|
|
598
|
+
appVersion
|
|
599
|
+
]);
|
|
600
|
+
useEffect(() => {
|
|
601
|
+
return () => {
|
|
602
|
+
isMountedRef.current = false;
|
|
603
|
+
clearBubblesForegroundPresentation();
|
|
604
|
+
};
|
|
605
|
+
}, [isMountedRef]);
|
|
606
|
+
useEffect(() => {
|
|
607
|
+
let isCancelled = false;
|
|
608
|
+
void (async () => {
|
|
609
|
+
try {
|
|
610
|
+
storeApiBaseUrl(getRequiredNonEmptyString(apiBaseUrl, "apiBaseUrl"));
|
|
611
|
+
storeAppKey(getRequiredNonEmptyString(appKey, "appKey"));
|
|
612
|
+
await prepareBubblesNotificationPresentation();
|
|
613
|
+
} catch (error) {
|
|
614
|
+
if (!isCancelled && isMountedRef.current) {
|
|
615
|
+
setState((currentState) => ({
|
|
616
|
+
...currentState,
|
|
617
|
+
error: normalizeProviderError(error)
|
|
618
|
+
}));
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
})();
|
|
622
|
+
return () => {
|
|
623
|
+
isCancelled = true;
|
|
624
|
+
};
|
|
625
|
+
}, [apiBaseUrl, appKey, isMountedRef]);
|
|
626
|
+
useEffect(() => {
|
|
627
|
+
applyBubblesForegroundPresentation(foregroundPresentation);
|
|
628
|
+
}, [foregroundPresentation]);
|
|
629
|
+
useEffect(() => {
|
|
630
|
+
return observeBubblesForegroundRemoteMessages();
|
|
631
|
+
}, []);
|
|
632
|
+
useEffect(() => {
|
|
633
|
+
return observeBubblesNotificationOpenEvents({
|
|
634
|
+
onNotificationResponse
|
|
635
|
+
});
|
|
636
|
+
}, [onNotificationResponse]);
|
|
637
|
+
useEffect(() => {
|
|
638
|
+
const previousUserId = previousUserIdRef.current;
|
|
639
|
+
if (previousUserId !== null && normalizedUserId === null) {
|
|
640
|
+
lastAutomaticSyncSignatureRef.current = null;
|
|
641
|
+
if (onLogout) {
|
|
642
|
+
void enqueueOperation(async () => {
|
|
643
|
+
await onLogout({
|
|
644
|
+
previousUserId,
|
|
645
|
+
deviceId: state.deviceId
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
previousUserIdRef.current = normalizedUserId;
|
|
651
|
+
}, [enqueueOperation, normalizedUserId, onLogout, state.deviceId]);
|
|
652
|
+
useEffect(() => {
|
|
653
|
+
if (!ready || normalizedUserId === null || state.deviceId === null) {
|
|
654
|
+
lastAutomaticSyncSignatureRef.current = null;
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const syncSignature = buildAutomaticSyncSignature(state.deviceId);
|
|
658
|
+
if (lastAutomaticSyncSignatureRef.current === syncSignature) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
lastAutomaticSyncSignatureRef.current = syncSignature;
|
|
662
|
+
const storedDeviceId = state.deviceId;
|
|
663
|
+
void enqueueOperation(async () => {
|
|
664
|
+
const result = await syncExistingBubblesDevice({
|
|
665
|
+
appId,
|
|
666
|
+
appKey,
|
|
667
|
+
apiBaseUrl,
|
|
668
|
+
userId: normalizedUserId,
|
|
669
|
+
aliasing,
|
|
670
|
+
appVersion,
|
|
671
|
+
deviceId: storedDeviceId
|
|
672
|
+
});
|
|
673
|
+
return getRuntimeStateUpdate(result);
|
|
674
|
+
});
|
|
675
|
+
}, [
|
|
676
|
+
ready,
|
|
677
|
+
normalizedUserId,
|
|
678
|
+
state.deviceId,
|
|
679
|
+
appId,
|
|
680
|
+
appKey,
|
|
681
|
+
apiBaseUrl,
|
|
682
|
+
aliasing,
|
|
683
|
+
appVersion,
|
|
684
|
+
buildAutomaticSyncSignature,
|
|
685
|
+
enqueueOperation
|
|
686
|
+
]);
|
|
687
|
+
function registerDevice(options) {
|
|
688
|
+
return enqueueOperation(async () => {
|
|
689
|
+
if (!ready) {
|
|
690
|
+
failWithBubblesError(
|
|
691
|
+
'"registerDevice()" requires "ready" to be true before the device can be synced.'
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
if (normalizedUserId === null) {
|
|
695
|
+
failWithBubblesError(
|
|
696
|
+
'"registerDevice()" requires a non-null "userId" before the device can be synced.'
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
const result = await registerBubblesDevice({
|
|
700
|
+
appId,
|
|
701
|
+
appKey,
|
|
702
|
+
apiBaseUrl,
|
|
703
|
+
userId: normalizedUserId,
|
|
704
|
+
aliasing,
|
|
705
|
+
appVersion,
|
|
706
|
+
deviceId: state.deviceId,
|
|
707
|
+
registrationOptions: options
|
|
708
|
+
});
|
|
709
|
+
if (result.deviceId !== null) {
|
|
710
|
+
lastAutomaticSyncSignatureRef.current = buildAutomaticSyncSignature(
|
|
711
|
+
result.deviceId
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
return getRuntimeStateUpdate(result);
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
const contextValue = {
|
|
718
|
+
registerDevice,
|
|
719
|
+
deviceId: state.deviceId,
|
|
720
|
+
pushToken: state.pushToken,
|
|
721
|
+
tokenType: state.tokenType,
|
|
722
|
+
permissionStatus: state.permissionStatus,
|
|
723
|
+
notificationsEnabled: state.notificationsEnabled,
|
|
724
|
+
isSyncing: state.isSyncing,
|
|
725
|
+
error: state.error
|
|
726
|
+
};
|
|
727
|
+
return /* @__PURE__ */ jsx(BubblesNotificationsContext.Provider, { value: contextValue, children });
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/runtime/useBubblesNotifications.ts
|
|
731
|
+
import { useContext } from "react";
|
|
732
|
+
function fail2(message) {
|
|
733
|
+
throw new Error(`[@fishonfire/bubbles-expo] ${message}`);
|
|
734
|
+
}
|
|
735
|
+
function useBubblesNotifications() {
|
|
736
|
+
const context = useContext(BubblesNotificationsContext);
|
|
737
|
+
if (context === null) {
|
|
738
|
+
fail2(
|
|
739
|
+
'"useBubblesNotifications()" must be used inside "BubblesNotificationsProvider".'
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
return context;
|
|
743
|
+
}
|
|
744
|
+
export {
|
|
745
|
+
ANDROID_CHANNEL_IMPORTANCE_VALUES,
|
|
746
|
+
BUBBLES_DELIVERY_STATUSES,
|
|
747
|
+
BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY,
|
|
748
|
+
BubblesAppAuthenticationError,
|
|
749
|
+
BubblesNotificationsProvider,
|
|
750
|
+
buildBubblesDeliveryStatusPayload,
|
|
751
|
+
buildCreateDevicePayload,
|
|
752
|
+
buildStoredDeliveryStatusEventKey,
|
|
753
|
+
buildUpdateDevicePayload,
|
|
754
|
+
createBubblesDevice,
|
|
755
|
+
createBubblesDeviceClient,
|
|
756
|
+
createStoredDeliveryStatusEvent,
|
|
757
|
+
describeNotificationPermissionStatus,
|
|
758
|
+
enqueueStoredDeliveryStatusEvent,
|
|
759
|
+
ensureDefaultNotificationChannel,
|
|
760
|
+
ensureNotificationPermissions,
|
|
761
|
+
extractDeviceIdFromDeviceResponse,
|
|
762
|
+
flushStoredBubblesDeliveryStatuses,
|
|
763
|
+
getAndroidNotificationChannelImportance,
|
|
764
|
+
getBubblesNotificationsRuntimeConfig,
|
|
765
|
+
getDefaultNotificationChannelConfig,
|
|
766
|
+
getDeviceRegistrationState,
|
|
767
|
+
getDeviceToken,
|
|
768
|
+
getFCMToken,
|
|
769
|
+
getNotificationPermissions,
|
|
770
|
+
isNotificationPermissionGranted,
|
|
771
|
+
markStoredDeliveryStatusEventPosted,
|
|
772
|
+
patchStoredDeviceState,
|
|
773
|
+
postBubblesDeliveryStatus,
|
|
774
|
+
postStoredBubblesDeliveryStatus,
|
|
775
|
+
readStoredApiBaseUrl,
|
|
776
|
+
readStoredAppKey,
|
|
777
|
+
readStoredDeliveryStatusLedger,
|
|
778
|
+
readStoredDeviceId,
|
|
779
|
+
readStoredDeviceState,
|
|
780
|
+
registerBubblesBackgroundHandlers,
|
|
781
|
+
storeApiBaseUrl,
|
|
782
|
+
storeAppKey,
|
|
783
|
+
storeDeliveryStatusLedger,
|
|
784
|
+
storeDeviceId,
|
|
785
|
+
storeDeviceState,
|
|
786
|
+
syncBubblesDevice,
|
|
787
|
+
updateBubblesDevice,
|
|
788
|
+
useBubblesNotifications
|
|
789
|
+
};
|