@fishonfire/bubbles-expo 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -162,10 +162,25 @@ export function EnableNotificationsButton() {
162
162
  }
163
163
  ```
164
164
 
165
- `useBubblesNotifications()` also exposes `deviceId`, `pushToken`, `permissionStatus`, `notificationsEnabled`, `tokenType`, and `error`.
165
+ `useBubblesNotifications()` also exposes `addDeviceAttribute`, `deviceId`, `pushToken`, `permissionStatus`, `notificationsEnabled`, `tokenType`, and `error`.
166
166
 
167
167
  If your app already requested notification permissions, call `registerDevice({ requestPermissions: false })`.
168
168
 
169
+ ## Add custom device attributes
170
+
171
+ After the provider has a backend `deviceId`, apps can add their own device attributes:
172
+
173
+ ```tsx
174
+ const { addDeviceAttribute, deviceId } = useBubblesNotifications();
175
+
176
+ if (deviceId) {
177
+ void addDeviceAttribute('plan', {
178
+ tier: 'pro',
179
+ seats: 4,
180
+ });
181
+ }
182
+ ```
183
+
169
184
  ## Example application
170
185
  A example application can be found at: https://github.com/fishonfire/bubbles-notifications-expo-example
171
186
 
@@ -177,4 +192,4 @@ A example application can be found at: https://github.com/fishonfire/bubbles-not
177
192
  ## Copyright and Licence
178
193
  Copyright (c) 2026, Fish on Fire.
179
194
 
180
- Source code is licensed under the [`GPL License`](https://github.com/fishonfire/bubbles-notifications-expo/blob/develop/LICENSE).
195
+ Source code is licensed under the [`GPL License`](https://github.com/fishonfire/bubbles-notifications-expo/blob/develop/LICENSE).
@@ -458,6 +458,78 @@ function throwAppAuthenticationError(error) {
458
458
  }
459
459
  throw error;
460
460
  }
461
+ function normalizeDeviceAttributes(value) {
462
+ return normalizeDeviceAttributeObject(value, "attributes", /* @__PURE__ */ new WeakSet());
463
+ }
464
+ function normalizeDeviceAttributeValue(value, sourceDescription, seenValues) {
465
+ if (value === null) {
466
+ return null;
467
+ }
468
+ if (typeof value === "string" || typeof value === "boolean") {
469
+ return value;
470
+ }
471
+ if (typeof value === "number") {
472
+ if (!Number.isFinite(value)) {
473
+ failWithBubblesError(`${sourceDescription} must be a finite number.`);
474
+ }
475
+ return value;
476
+ }
477
+ if (Array.isArray(value)) {
478
+ if (seenValues.has(value)) {
479
+ failWithBubblesError(`${sourceDescription} must not contain cycles.`);
480
+ }
481
+ seenValues.add(value);
482
+ const normalizedValue = value.map(
483
+ (entry, index) => normalizeDeviceAttributeValue(
484
+ entry,
485
+ `${sourceDescription}[${index}]`,
486
+ seenValues
487
+ )
488
+ );
489
+ seenValues.delete(value);
490
+ return normalizedValue;
491
+ }
492
+ if (isPlainObject(value)) {
493
+ return normalizeDeviceAttributeObject(
494
+ value,
495
+ sourceDescription,
496
+ seenValues
497
+ );
498
+ }
499
+ failWithBubblesError(
500
+ `${sourceDescription} must be a JSON-compatible value.`
501
+ );
502
+ }
503
+ function normalizeDeviceAttributeObject(value, sourceDescription, seenValues) {
504
+ if (!isPlainObject(value)) {
505
+ failWithBubblesError('"attributes" must be an object.');
506
+ }
507
+ const valuePrototype = Object.getPrototypeOf(value);
508
+ if (valuePrototype !== Object.prototype && valuePrototype !== null) {
509
+ failWithBubblesError(
510
+ `${sourceDescription} must be a JSON-compatible object.`
511
+ );
512
+ }
513
+ if (Object.getOwnPropertySymbols(value).length > 0) {
514
+ failWithBubblesError(
515
+ `${sourceDescription} must not contain symbol keys.`
516
+ );
517
+ }
518
+ if (seenValues.has(value)) {
519
+ failWithBubblesError(`${sourceDescription} must not contain cycles.`);
520
+ }
521
+ seenValues.add(value);
522
+ const normalizedValue = {};
523
+ for (const [attributeName, attributeValue] of Object.entries(value)) {
524
+ normalizedValue[attributeName] = normalizeDeviceAttributeValue(
525
+ attributeValue,
526
+ `${sourceDescription}.${attributeName}`,
527
+ seenValues
528
+ );
529
+ }
530
+ seenValues.delete(value);
531
+ return normalizedValue;
532
+ }
461
533
  function extractDeviceIdFromDeviceResponse(value) {
462
534
  if (!value || typeof value !== "object" || Array.isArray(value)) {
463
535
  return null;
@@ -497,6 +569,18 @@ async function updateBubblesDevice(options) {
497
569
  throwAppAuthenticationError(error);
498
570
  }
499
571
  }
572
+ async function updateBubblesDeviceAttributes(options) {
573
+ const client = resolveDeviceClient(options);
574
+ try {
575
+ return await client.updateDeviceAttributes(
576
+ normalizeIdentifier(options.deviceId, "deviceId"),
577
+ normalizeDeviceAttributes(options.attributes),
578
+ withAppKeyHeader(options.appKey, options.requestOptions)
579
+ );
580
+ } catch (error) {
581
+ throwAppAuthenticationError(error);
582
+ }
583
+ }
500
584
  async function syncBubblesDevice(options) {
501
585
  const client = resolveDeviceClient(options);
502
586
  if (options.deviceId !== null) {
@@ -1279,6 +1363,7 @@ export {
1279
1363
  extractDeviceIdFromDeviceResponse,
1280
1364
  createBubblesDevice,
1281
1365
  updateBubblesDevice,
1366
+ updateBubblesDeviceAttributes,
1282
1367
  syncBubblesDevice,
1283
1368
  BUBBLES_DELIVERY_STATUSES,
1284
1369
  buildBubblesDeliveryStatusPayload,
package/dist/index.d.mts CHANGED
@@ -93,10 +93,20 @@ interface SyncBubblesDeviceResult<TResponse = unknown> {
93
93
  deviceId: string | null;
94
94
  response: TResponse;
95
95
  }
96
+ type BubblesDeviceAttributeValue = string | number | boolean | null | BubblesDeviceAttributeValue[] | {
97
+ [key: string]: BubblesDeviceAttributeValue;
98
+ };
99
+ type BubblesDeviceAttributes = Record<string, BubblesDeviceAttributeValue>;
100
+ interface UpdateBubblesDeviceAttributesOptions extends BubblesDeviceClientInput {
101
+ deviceId: string | number;
102
+ attributes: BubblesDeviceAttributes;
103
+ requestOptions?: RequestOptions;
104
+ }
96
105
  declare function createBubblesDeviceClient(options: BubblesDeviceClientOptions): DeviceClient;
97
106
  declare function extractDeviceIdFromDeviceResponse(value: unknown): string | null;
98
107
  declare function createBubblesDevice<TResponse = unknown>(options: CreateBubblesDeviceOptions): Promise<TResponse>;
99
108
  declare function updateBubblesDevice<TResponse = unknown>(options: UpdateBubblesDeviceOptions): Promise<TResponse>;
109
+ declare function updateBubblesDeviceAttributes<TResponse = unknown>(options: UpdateBubblesDeviceAttributesOptions): Promise<TResponse>;
100
110
  declare function syncBubblesDevice<TResponse = unknown>(options: SyncBubblesDeviceOptions): Promise<SyncBubblesDeviceResult<TResponse>>;
101
111
 
102
112
  type DeviceClientFactoryOptions = Omit<DeviceClientOptions, 'baseUrl'>;
@@ -220,6 +230,8 @@ declare function getDeviceRegistrationState(options?: GetDeviceTokenOptions): Pr
220
230
  declare function getDeviceToken(options?: GetDeviceTokenOptions): Promise<DeviceTokenResult>;
221
231
  declare function getFCMToken(options?: GetDeviceTokenOptions): Promise<string>;
222
232
 
233
+ declare function collectBubblesDeviceAttributes(): Promise<BubblesDeviceAttributes>;
234
+
223
235
  interface RegisterDeviceOptions {
224
236
  requestPermissions?: boolean;
225
237
  permissionRequestOptions?: PermissionRequestOptions;
@@ -256,6 +268,7 @@ interface BubblesNotificationsProviderProps {
256
268
  }
257
269
  interface BubblesNotificationsContextValue {
258
270
  registerDevice: (options?: RegisterDeviceOptions) => Promise<void>;
271
+ addDeviceAttribute: (name: string, value: BubblesDeviceAttributeValue) => Promise<void>;
259
272
  deviceId: string | null;
260
273
  pushToken: string | null;
261
274
  tokenType: NativeTokenType | null;
@@ -303,4 +316,4 @@ interface BubblesNotificationPayload {
303
316
  url: string | null;
304
317
  }
305
318
 
306
- export { ANDROID_CHANNEL_IMPORTANCE_VALUES, type AndroidChannelImportance, BUBBLES_DELIVERY_STATUSES, BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY, BubblesAppAuthenticationError, type BubblesCreateDevicePayload, type BubblesDeliveryStatus, type BubblesDeliveryStatusPayload, type BubblesDevicePayload, type BubblesDevicePlatform, type BubblesForegroundNotificationPayloadContract, type BubblesNotificationDataContract, type BubblesNotificationPayload, type BubblesNotificationResponseEvent, type BubblesNotificationsContextValue, type BubblesNotificationsLogoutEvent, BubblesNotificationsProvider, type BubblesNotificationsProviderProps, type BubblesNotificationsRuntimeConfig, type BubblesSilentNotificationPayloadContract, type BubblesUpdateDevicePayload, type BubblesVisibleNotificationPayloadContract, type BuildBubblesCreateDevicePayloadOptions, type BuildBubblesDeliveryStatusPayloadOptions, type BuildBubblesDevicePayloadOptions, type CreateBubblesDeviceOptions, type CreateStoredDeliveryStatusEventOptions, type DefaultNotificationChannelConfig, type DeviceRegistrationState, type DeviceTokenResult, type EnsureDefaultNotificationChannelOptions, type ForegroundPresentationOptions, type GetDeviceTokenOptions, type GetNotificationPermissionsOptions, type NativeTokenType, type PermissionRequestOptions, type PostBubblesDeliveryStatusOptions, type PostStoredBubblesDeliveryStatusOptions, type RegisterDeviceOptions, type StoredDeliveryStatusEvent, type StoredDeliveryStatusLedger, type StoredDeviceState, type StoredDeviceStateUpdate, type SupportedPlatform, type SyncBubblesDeviceOptions, type SyncBubblesDeviceResult, type UpdateBubblesDeviceOptions, buildBubblesDeliveryStatusPayload, buildCreateDevicePayload, buildStoredDeliveryStatusEventKey, buildUpdateDevicePayload, createBubblesDevice, createBubblesDeviceClient, createStoredDeliveryStatusEvent, describeNotificationPermissionStatus, enqueueStoredDeliveryStatusEvent, ensureDefaultNotificationChannel, ensureNotificationPermissions, extractDeviceIdFromDeviceResponse, flushStoredBubblesDeliveryStatuses, getAndroidNotificationChannelImportance, getBubblesNotificationsRuntimeConfig, getDefaultNotificationChannelConfig, getDeviceRegistrationState, getDeviceToken, getFCMToken, getNotificationPermissions, isNotificationPermissionGranted, markStoredDeliveryStatusEventPosted, patchStoredDeviceState, postBubblesDeliveryStatus, postStoredBubblesDeliveryStatus, readStoredApiBaseUrl, readStoredAppKey, readStoredDeliveryStatusLedger, readStoredDeviceId, readStoredDeviceState, registerBubblesBackgroundHandlers, storeApiBaseUrl, storeAppKey, storeDeliveryStatusLedger, storeDeviceId, storeDeviceState, syncBubblesDevice, updateBubblesDevice, useBubblesNotifications };
319
+ export { ANDROID_CHANNEL_IMPORTANCE_VALUES, type AndroidChannelImportance, BUBBLES_DELIVERY_STATUSES, BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY, BubblesAppAuthenticationError, type BubblesCreateDevicePayload, type BubblesDeliveryStatus, type BubblesDeliveryStatusPayload, type BubblesDeviceAttributeValue, type BubblesDeviceAttributes, type BubblesDevicePayload, type BubblesDevicePlatform, type BubblesForegroundNotificationPayloadContract, type BubblesNotificationDataContract, type BubblesNotificationPayload, type BubblesNotificationResponseEvent, type BubblesNotificationsContextValue, type BubblesNotificationsLogoutEvent, BubblesNotificationsProvider, type BubblesNotificationsProviderProps, type BubblesNotificationsRuntimeConfig, type BubblesSilentNotificationPayloadContract, type BubblesUpdateDevicePayload, type BubblesVisibleNotificationPayloadContract, type BuildBubblesCreateDevicePayloadOptions, type BuildBubblesDeliveryStatusPayloadOptions, type BuildBubblesDevicePayloadOptions, type CreateBubblesDeviceOptions, type CreateStoredDeliveryStatusEventOptions, type DefaultNotificationChannelConfig, type DeviceRegistrationState, type DeviceTokenResult, type EnsureDefaultNotificationChannelOptions, type ForegroundPresentationOptions, type GetDeviceTokenOptions, type GetNotificationPermissionsOptions, type NativeTokenType, type PermissionRequestOptions, type PostBubblesDeliveryStatusOptions, type PostStoredBubblesDeliveryStatusOptions, type RegisterDeviceOptions, type StoredDeliveryStatusEvent, type StoredDeliveryStatusLedger, type StoredDeviceState, type StoredDeviceStateUpdate, type SupportedPlatform, type SyncBubblesDeviceOptions, type SyncBubblesDeviceResult, type UpdateBubblesDeviceAttributesOptions, type UpdateBubblesDeviceOptions, buildBubblesDeliveryStatusPayload, buildCreateDevicePayload, buildStoredDeliveryStatusEventKey, buildUpdateDevicePayload, collectBubblesDeviceAttributes, createBubblesDevice, createBubblesDeviceClient, createStoredDeliveryStatusEvent, describeNotificationPermissionStatus, enqueueStoredDeliveryStatusEvent, ensureDefaultNotificationChannel, ensureNotificationPermissions, extractDeviceIdFromDeviceResponse, flushStoredBubblesDeliveryStatuses, getAndroidNotificationChannelImportance, getBubblesNotificationsRuntimeConfig, getDefaultNotificationChannelConfig, getDeviceRegistrationState, getDeviceToken, getFCMToken, getNotificationPermissions, isNotificationPermissionGranted, markStoredDeliveryStatusEventPosted, patchStoredDeviceState, postBubblesDeliveryStatus, postStoredBubblesDeliveryStatus, readStoredApiBaseUrl, readStoredAppKey, readStoredDeliveryStatusLedger, readStoredDeviceId, readStoredDeviceState, registerBubblesBackgroundHandlers, storeApiBaseUrl, storeAppKey, storeDeliveryStatusLedger, storeDeviceId, storeDeviceState, syncBubblesDevice, updateBubblesDevice, updateBubblesDeviceAttributes, useBubblesNotifications };
package/dist/index.d.ts CHANGED
@@ -93,10 +93,20 @@ interface SyncBubblesDeviceResult<TResponse = unknown> {
93
93
  deviceId: string | null;
94
94
  response: TResponse;
95
95
  }
96
+ type BubblesDeviceAttributeValue = string | number | boolean | null | BubblesDeviceAttributeValue[] | {
97
+ [key: string]: BubblesDeviceAttributeValue;
98
+ };
99
+ type BubblesDeviceAttributes = Record<string, BubblesDeviceAttributeValue>;
100
+ interface UpdateBubblesDeviceAttributesOptions extends BubblesDeviceClientInput {
101
+ deviceId: string | number;
102
+ attributes: BubblesDeviceAttributes;
103
+ requestOptions?: RequestOptions;
104
+ }
96
105
  declare function createBubblesDeviceClient(options: BubblesDeviceClientOptions): DeviceClient;
97
106
  declare function extractDeviceIdFromDeviceResponse(value: unknown): string | null;
98
107
  declare function createBubblesDevice<TResponse = unknown>(options: CreateBubblesDeviceOptions): Promise<TResponse>;
99
108
  declare function updateBubblesDevice<TResponse = unknown>(options: UpdateBubblesDeviceOptions): Promise<TResponse>;
109
+ declare function updateBubblesDeviceAttributes<TResponse = unknown>(options: UpdateBubblesDeviceAttributesOptions): Promise<TResponse>;
100
110
  declare function syncBubblesDevice<TResponse = unknown>(options: SyncBubblesDeviceOptions): Promise<SyncBubblesDeviceResult<TResponse>>;
101
111
 
102
112
  type DeviceClientFactoryOptions = Omit<DeviceClientOptions, 'baseUrl'>;
@@ -220,6 +230,8 @@ declare function getDeviceRegistrationState(options?: GetDeviceTokenOptions): Pr
220
230
  declare function getDeviceToken(options?: GetDeviceTokenOptions): Promise<DeviceTokenResult>;
221
231
  declare function getFCMToken(options?: GetDeviceTokenOptions): Promise<string>;
222
232
 
233
+ declare function collectBubblesDeviceAttributes(): Promise<BubblesDeviceAttributes>;
234
+
223
235
  interface RegisterDeviceOptions {
224
236
  requestPermissions?: boolean;
225
237
  permissionRequestOptions?: PermissionRequestOptions;
@@ -256,6 +268,7 @@ interface BubblesNotificationsProviderProps {
256
268
  }
257
269
  interface BubblesNotificationsContextValue {
258
270
  registerDevice: (options?: RegisterDeviceOptions) => Promise<void>;
271
+ addDeviceAttribute: (name: string, value: BubblesDeviceAttributeValue) => Promise<void>;
259
272
  deviceId: string | null;
260
273
  pushToken: string | null;
261
274
  tokenType: NativeTokenType | null;
@@ -303,4 +316,4 @@ interface BubblesNotificationPayload {
303
316
  url: string | null;
304
317
  }
305
318
 
306
- export { ANDROID_CHANNEL_IMPORTANCE_VALUES, type AndroidChannelImportance, BUBBLES_DELIVERY_STATUSES, BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY, BubblesAppAuthenticationError, type BubblesCreateDevicePayload, type BubblesDeliveryStatus, type BubblesDeliveryStatusPayload, type BubblesDevicePayload, type BubblesDevicePlatform, type BubblesForegroundNotificationPayloadContract, type BubblesNotificationDataContract, type BubblesNotificationPayload, type BubblesNotificationResponseEvent, type BubblesNotificationsContextValue, type BubblesNotificationsLogoutEvent, BubblesNotificationsProvider, type BubblesNotificationsProviderProps, type BubblesNotificationsRuntimeConfig, type BubblesSilentNotificationPayloadContract, type BubblesUpdateDevicePayload, type BubblesVisibleNotificationPayloadContract, type BuildBubblesCreateDevicePayloadOptions, type BuildBubblesDeliveryStatusPayloadOptions, type BuildBubblesDevicePayloadOptions, type CreateBubblesDeviceOptions, type CreateStoredDeliveryStatusEventOptions, type DefaultNotificationChannelConfig, type DeviceRegistrationState, type DeviceTokenResult, type EnsureDefaultNotificationChannelOptions, type ForegroundPresentationOptions, type GetDeviceTokenOptions, type GetNotificationPermissionsOptions, type NativeTokenType, type PermissionRequestOptions, type PostBubblesDeliveryStatusOptions, type PostStoredBubblesDeliveryStatusOptions, type RegisterDeviceOptions, type StoredDeliveryStatusEvent, type StoredDeliveryStatusLedger, type StoredDeviceState, type StoredDeviceStateUpdate, type SupportedPlatform, type SyncBubblesDeviceOptions, type SyncBubblesDeviceResult, type UpdateBubblesDeviceOptions, buildBubblesDeliveryStatusPayload, buildCreateDevicePayload, buildStoredDeliveryStatusEventKey, buildUpdateDevicePayload, createBubblesDevice, createBubblesDeviceClient, createStoredDeliveryStatusEvent, describeNotificationPermissionStatus, enqueueStoredDeliveryStatusEvent, ensureDefaultNotificationChannel, ensureNotificationPermissions, extractDeviceIdFromDeviceResponse, flushStoredBubblesDeliveryStatuses, getAndroidNotificationChannelImportance, getBubblesNotificationsRuntimeConfig, getDefaultNotificationChannelConfig, getDeviceRegistrationState, getDeviceToken, getFCMToken, getNotificationPermissions, isNotificationPermissionGranted, markStoredDeliveryStatusEventPosted, patchStoredDeviceState, postBubblesDeliveryStatus, postStoredBubblesDeliveryStatus, readStoredApiBaseUrl, readStoredAppKey, readStoredDeliveryStatusLedger, readStoredDeviceId, readStoredDeviceState, registerBubblesBackgroundHandlers, storeApiBaseUrl, storeAppKey, storeDeliveryStatusLedger, storeDeviceId, storeDeviceState, syncBubblesDevice, updateBubblesDevice, useBubblesNotifications };
319
+ export { ANDROID_CHANNEL_IMPORTANCE_VALUES, type AndroidChannelImportance, BUBBLES_DELIVERY_STATUSES, BUBBLES_NOTIFICATIONS_EXPO_EXTRA_KEY, BubblesAppAuthenticationError, type BubblesCreateDevicePayload, type BubblesDeliveryStatus, type BubblesDeliveryStatusPayload, type BubblesDeviceAttributeValue, type BubblesDeviceAttributes, type BubblesDevicePayload, type BubblesDevicePlatform, type BubblesForegroundNotificationPayloadContract, type BubblesNotificationDataContract, type BubblesNotificationPayload, type BubblesNotificationResponseEvent, type BubblesNotificationsContextValue, type BubblesNotificationsLogoutEvent, BubblesNotificationsProvider, type BubblesNotificationsProviderProps, type BubblesNotificationsRuntimeConfig, type BubblesSilentNotificationPayloadContract, type BubblesUpdateDevicePayload, type BubblesVisibleNotificationPayloadContract, type BuildBubblesCreateDevicePayloadOptions, type BuildBubblesDeliveryStatusPayloadOptions, type BuildBubblesDevicePayloadOptions, type CreateBubblesDeviceOptions, type CreateStoredDeliveryStatusEventOptions, type DefaultNotificationChannelConfig, type DeviceRegistrationState, type DeviceTokenResult, type EnsureDefaultNotificationChannelOptions, type ForegroundPresentationOptions, type GetDeviceTokenOptions, type GetNotificationPermissionsOptions, type NativeTokenType, type PermissionRequestOptions, type PostBubblesDeliveryStatusOptions, type PostStoredBubblesDeliveryStatusOptions, type RegisterDeviceOptions, type StoredDeliveryStatusEvent, type StoredDeliveryStatusLedger, type StoredDeviceState, type StoredDeviceStateUpdate, type SupportedPlatform, type SyncBubblesDeviceOptions, type SyncBubblesDeviceResult, type UpdateBubblesDeviceAttributesOptions, type UpdateBubblesDeviceOptions, buildBubblesDeliveryStatusPayload, buildCreateDevicePayload, buildStoredDeliveryStatusEventKey, buildUpdateDevicePayload, collectBubblesDeviceAttributes, createBubblesDevice, createBubblesDeviceClient, createStoredDeliveryStatusEvent, describeNotificationPermissionStatus, enqueueStoredDeliveryStatusEvent, ensureDefaultNotificationChannel, ensureNotificationPermissions, extractDeviceIdFromDeviceResponse, flushStoredBubblesDeliveryStatuses, getAndroidNotificationChannelImportance, getBubblesNotificationsRuntimeConfig, getDefaultNotificationChannelConfig, getDeviceRegistrationState, getDeviceToken, getFCMToken, getNotificationPermissions, isNotificationPermissionGranted, markStoredDeliveryStatusEventPosted, patchStoredDeviceState, postBubblesDeliveryStatus, postStoredBubblesDeliveryStatus, readStoredApiBaseUrl, readStoredAppKey, readStoredDeliveryStatusLedger, readStoredDeviceId, readStoredDeviceState, registerBubblesBackgroundHandlers, storeApiBaseUrl, storeAppKey, storeDeliveryStatusLedger, storeDeviceId, storeDeviceState, syncBubblesDevice, updateBubblesDevice, updateBubblesDeviceAttributes, useBubblesNotifications };
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ __export(index_exports, {
39
39
  buildCreateDevicePayload: () => buildCreateDevicePayload,
40
40
  buildStoredDeliveryStatusEventKey: () => buildStoredDeliveryStatusEventKey,
41
41
  buildUpdateDevicePayload: () => buildUpdateDevicePayload,
42
+ collectBubblesDeviceAttributes: () => collectBubblesDeviceAttributes,
42
43
  createBubblesDevice: () => createBubblesDevice,
43
44
  createBubblesDeviceClient: () => createBubblesDeviceClient,
44
45
  createStoredDeliveryStatusEvent: () => createStoredDeliveryStatusEvent,
@@ -73,6 +74,7 @@ __export(index_exports, {
73
74
  storeDeviceState: () => storeDeviceState,
74
75
  syncBubblesDevice: () => syncBubblesDevice,
75
76
  updateBubblesDevice: () => updateBubblesDevice,
77
+ updateBubblesDeviceAttributes: () => updateBubblesDeviceAttributes,
76
78
  useBubblesNotifications: () => useBubblesNotifications
77
79
  });
78
80
  module.exports = __toCommonJS(index_exports);
@@ -535,6 +537,78 @@ function throwAppAuthenticationError(error) {
535
537
  }
536
538
  throw error;
537
539
  }
540
+ function normalizeDeviceAttributes(value) {
541
+ return normalizeDeviceAttributeObject(value, "attributes", /* @__PURE__ */ new WeakSet());
542
+ }
543
+ function normalizeDeviceAttributeValue(value, sourceDescription, seenValues) {
544
+ if (value === null) {
545
+ return null;
546
+ }
547
+ if (typeof value === "string" || typeof value === "boolean") {
548
+ return value;
549
+ }
550
+ if (typeof value === "number") {
551
+ if (!Number.isFinite(value)) {
552
+ failWithBubblesError(`${sourceDescription} must be a finite number.`);
553
+ }
554
+ return value;
555
+ }
556
+ if (Array.isArray(value)) {
557
+ if (seenValues.has(value)) {
558
+ failWithBubblesError(`${sourceDescription} must not contain cycles.`);
559
+ }
560
+ seenValues.add(value);
561
+ const normalizedValue = value.map(
562
+ (entry, index) => normalizeDeviceAttributeValue(
563
+ entry,
564
+ `${sourceDescription}[${index}]`,
565
+ seenValues
566
+ )
567
+ );
568
+ seenValues.delete(value);
569
+ return normalizedValue;
570
+ }
571
+ if (isPlainObject(value)) {
572
+ return normalizeDeviceAttributeObject(
573
+ value,
574
+ sourceDescription,
575
+ seenValues
576
+ );
577
+ }
578
+ failWithBubblesError(
579
+ `${sourceDescription} must be a JSON-compatible value.`
580
+ );
581
+ }
582
+ function normalizeDeviceAttributeObject(value, sourceDescription, seenValues) {
583
+ if (!isPlainObject(value)) {
584
+ failWithBubblesError('"attributes" must be an object.');
585
+ }
586
+ const valuePrototype = Object.getPrototypeOf(value);
587
+ if (valuePrototype !== Object.prototype && valuePrototype !== null) {
588
+ failWithBubblesError(
589
+ `${sourceDescription} must be a JSON-compatible object.`
590
+ );
591
+ }
592
+ if (Object.getOwnPropertySymbols(value).length > 0) {
593
+ failWithBubblesError(
594
+ `${sourceDescription} must not contain symbol keys.`
595
+ );
596
+ }
597
+ if (seenValues.has(value)) {
598
+ failWithBubblesError(`${sourceDescription} must not contain cycles.`);
599
+ }
600
+ seenValues.add(value);
601
+ const normalizedValue = {};
602
+ for (const [attributeName, attributeValue] of Object.entries(value)) {
603
+ normalizedValue[attributeName] = normalizeDeviceAttributeValue(
604
+ attributeValue,
605
+ `${sourceDescription}.${attributeName}`,
606
+ seenValues
607
+ );
608
+ }
609
+ seenValues.delete(value);
610
+ return normalizedValue;
611
+ }
538
612
  function extractDeviceIdFromDeviceResponse(value) {
539
613
  if (!value || typeof value !== "object" || Array.isArray(value)) {
540
614
  return null;
@@ -574,6 +648,18 @@ async function updateBubblesDevice(options) {
574
648
  throwAppAuthenticationError(error);
575
649
  }
576
650
  }
651
+ async function updateBubblesDeviceAttributes(options) {
652
+ const client = resolveDeviceClient(options);
653
+ try {
654
+ return await client.updateDeviceAttributes(
655
+ normalizeIdentifier(options.deviceId, "deviceId"),
656
+ normalizeDeviceAttributes(options.attributes),
657
+ withAppKeyHeader(options.appKey, options.requestOptions)
658
+ );
659
+ } catch (error) {
660
+ throwAppAuthenticationError(error);
661
+ }
662
+ }
577
663
  async function syncBubblesDevice(options) {
578
664
  const client = resolveDeviceClient(options);
579
665
  if (options.deviceId !== null) {
@@ -1392,6 +1478,73 @@ async function getFCMToken(options) {
1392
1478
  return tokenResult.token;
1393
1479
  }
1394
1480
 
1481
+ // src/runtime/device-attributes.ts
1482
+ var Application = __toESM(require("expo-application"));
1483
+ var Device = __toESM(require("expo-device"));
1484
+ function compactAttributes(attributes) {
1485
+ return Object.fromEntries(
1486
+ Object.entries(attributes).filter((entry) => entry[1] !== null && entry[1] !== void 0)
1487
+ );
1488
+ }
1489
+ function addSection(attributes, name, section) {
1490
+ const compactedSection = compactAttributes(section);
1491
+ if (Object.keys(compactedSection).length > 0) {
1492
+ attributes[name] = compactedSection;
1493
+ }
1494
+ }
1495
+ function getDeviceTypeName(deviceType2) {
1496
+ switch (deviceType2) {
1497
+ case Device.DeviceType.PHONE:
1498
+ return "phone";
1499
+ case Device.DeviceType.TABLET:
1500
+ return "tablet";
1501
+ case Device.DeviceType.DESKTOP:
1502
+ return "desktop";
1503
+ case Device.DeviceType.TV:
1504
+ return "tv";
1505
+ case Device.DeviceType.UNKNOWN:
1506
+ return "unknown";
1507
+ default:
1508
+ return void 0;
1509
+ }
1510
+ }
1511
+ async function getDeviceType() {
1512
+ if (Device.deviceType !== null && Device.deviceType !== void 0) {
1513
+ return Device.deviceType;
1514
+ }
1515
+ try {
1516
+ return await Device.getDeviceTypeAsync();
1517
+ } catch {
1518
+ return null;
1519
+ }
1520
+ }
1521
+ async function collectBubblesDeviceAttributes() {
1522
+ const attributes = {};
1523
+ const deviceType2 = await getDeviceType();
1524
+ addSection(attributes, "device", {
1525
+ manufacturer: Device.manufacturer,
1526
+ brand: Device.brand,
1527
+ modelName: Device.modelName,
1528
+ modelId: typeof Device.modelId === "string" ? Device.modelId : void 0,
1529
+ deviceName: Device.deviceName,
1530
+ deviceType: getDeviceTypeName(deviceType2),
1531
+ totalMemory: Device.totalMemory
1532
+ });
1533
+ addSection(attributes, "os", {
1534
+ name: Device.osName,
1535
+ version: Device.osVersion,
1536
+ buildId: Device.osBuildId,
1537
+ platformApiLevel: Device.platformApiLevel
1538
+ });
1539
+ addSection(attributes, "app", {
1540
+ applicationId: Application.applicationId,
1541
+ applicationName: Application.applicationName,
1542
+ nativeApplicationVersion: Application.nativeApplicationVersion,
1543
+ nativeBuildVersion: Application.nativeBuildVersion
1544
+ });
1545
+ return attributes;
1546
+ }
1547
+
1395
1548
  // src/runtime/BubblesNotificationsProvider.tsx
1396
1549
  var import_react3 = require("react");
1397
1550
 
@@ -1635,6 +1788,20 @@ function buildRuntimeSnapshot(deviceId, registrationState) {
1635
1788
  notificationsEnabled: registrationState.notificationsEnabled
1636
1789
  };
1637
1790
  }
1791
+ async function syncBubblesDeviceAttributesBestEffort(apiBaseUrl, appKey, deviceId) {
1792
+ try {
1793
+ const attributes = await collectBubblesDeviceAttributes();
1794
+ if (Object.keys(attributes).length > 0) {
1795
+ await updateBubblesDeviceAttributes({
1796
+ apiBaseUrl,
1797
+ appKey,
1798
+ deviceId,
1799
+ attributes
1800
+ });
1801
+ }
1802
+ } catch {
1803
+ }
1804
+ }
1638
1805
  async function syncDeviceRegistrationState(options) {
1639
1806
  const apiBaseUrl = getRequiredNonEmptyString3(options.apiBaseUrl, "apiBaseUrl");
1640
1807
  const appKey = getRequiredNonEmptyString3(options.appKey, "appKey");
@@ -1661,6 +1828,13 @@ async function syncDeviceRegistrationState(options) {
1661
1828
  });
1662
1829
  const nextDeviceId = syncResult.deviceId ?? options.deviceId;
1663
1830
  storeDeviceId(nextDeviceId);
1831
+ if (nextDeviceId) {
1832
+ await syncBubblesDeviceAttributesBestEffort(
1833
+ apiBaseUrl,
1834
+ appKey,
1835
+ nextDeviceId
1836
+ );
1837
+ }
1664
1838
  return {
1665
1839
  ...snapshot,
1666
1840
  action: syncResult.action,
@@ -1848,6 +2022,7 @@ function BubblesNotificationsProvider(props) {
1848
2022
  const [state, setState] = (0, import_react3.useState)(getInitialRuntimeState);
1849
2023
  const { enqueueOperation, isMountedRef } = useBubblesRuntimeOperationQueue(setState);
1850
2024
  const lastAutomaticSyncSignatureRef = (0, import_react3.useRef)(null);
2025
+ const currentDeviceIdRef = (0, import_react3.useRef)(state.deviceId);
1851
2026
  const previousUserIdRef = (0, import_react3.useRef)(normalizedUserId);
1852
2027
  const buildAutomaticSyncSignature = (0, import_react3.useCallback)((deviceId) => {
1853
2028
  return getAutomaticSyncSignature({
@@ -1873,6 +2048,9 @@ function BubblesNotificationsProvider(props) {
1873
2048
  clearBubblesForegroundPresentation();
1874
2049
  };
1875
2050
  }, [isMountedRef]);
2051
+ (0, import_react3.useEffect)(() => {
2052
+ currentDeviceIdRef.current = state.deviceId;
2053
+ }, [state.deviceId]);
1876
2054
  (0, import_react3.useEffect)(() => {
1877
2055
  let isCancelled = false;
1878
2056
  void (async () => {
@@ -1940,6 +2118,7 @@ function BubblesNotificationsProvider(props) {
1940
2118
  appVersion,
1941
2119
  deviceId: storedDeviceId
1942
2120
  });
2121
+ currentDeviceIdRef.current = result.deviceId;
1943
2122
  return getRuntimeStateUpdate(result);
1944
2123
  });
1945
2124
  }, [
@@ -1973,9 +2152,10 @@ function BubblesNotificationsProvider(props) {
1973
2152
  userId: normalizedUserId,
1974
2153
  aliasing,
1975
2154
  appVersion,
1976
- deviceId: state.deviceId,
2155
+ deviceId: currentDeviceIdRef.current,
1977
2156
  registrationOptions: options
1978
2157
  });
2158
+ currentDeviceIdRef.current = result.deviceId;
1979
2159
  if (result.deviceId !== null) {
1980
2160
  lastAutomaticSyncSignatureRef.current = buildAutomaticSyncSignature(
1981
2161
  result.deviceId
@@ -1984,8 +2164,33 @@ function BubblesNotificationsProvider(props) {
1984
2164
  return getRuntimeStateUpdate(result);
1985
2165
  });
1986
2166
  }
2167
+ function addDeviceAttribute(name, value) {
2168
+ return enqueueOperation(async () => {
2169
+ const attributeName = getRequiredNonEmptyString(name, "attributeName");
2170
+ if (value === void 0) {
2171
+ failWithBubblesError(
2172
+ '"addDeviceAttribute()" requires "value" to be a JSON-compatible value.'
2173
+ );
2174
+ }
2175
+ const deviceId = currentDeviceIdRef.current;
2176
+ if (deviceId === null) {
2177
+ failWithBubblesError(
2178
+ '"addDeviceAttribute()" requires a backend "deviceId" before attributes can be synced.'
2179
+ );
2180
+ }
2181
+ await updateBubblesDeviceAttributes({
2182
+ apiBaseUrl,
2183
+ appKey,
2184
+ deviceId,
2185
+ attributes: {
2186
+ [attributeName]: value
2187
+ }
2188
+ });
2189
+ });
2190
+ }
1987
2191
  const contextValue = {
1988
2192
  registerDevice,
2193
+ addDeviceAttribute,
1989
2194
  deviceId: state.deviceId,
1990
2195
  pushToken: state.pushToken,
1991
2196
  tokenType: state.tokenType,
@@ -2022,6 +2227,7 @@ function useBubblesNotifications() {
2022
2227
  buildCreateDevicePayload,
2023
2228
  buildStoredDeliveryStatusEventKey,
2024
2229
  buildUpdateDevicePayload,
2230
+ collectBubblesDeviceAttributes,
2025
2231
  createBubblesDevice,
2026
2232
  createBubblesDeviceClient,
2027
2233
  createStoredDeliveryStatusEvent,
@@ -2056,5 +2262,6 @@ function useBubblesNotifications() {
2056
2262
  storeDeviceState,
2057
2263
  syncBubblesDevice,
2058
2264
  updateBubblesDevice,
2265
+ updateBubblesDeviceAttributes,
2059
2266
  useBubblesNotifications
2060
2267
  });
package/dist/index.mjs CHANGED
@@ -47,8 +47,9 @@ import {
47
47
  storeDeviceId,
48
48
  storeDeviceState,
49
49
  syncBubblesDevice,
50
- updateBubblesDevice
51
- } from "./chunk-6NPXFGQX.mjs";
50
+ updateBubblesDevice,
51
+ updateBubblesDeviceAttributes
52
+ } from "./chunk-3BAGWXHY.mjs";
52
53
 
53
54
  // src/runtime/tokens.ts
54
55
  import {
@@ -116,6 +117,73 @@ async function getFCMToken(options) {
116
117
  return tokenResult.token;
117
118
  }
118
119
 
120
+ // src/runtime/device-attributes.ts
121
+ import * as Application from "expo-application";
122
+ import * as Device from "expo-device";
123
+ function compactAttributes(attributes) {
124
+ return Object.fromEntries(
125
+ Object.entries(attributes).filter((entry) => entry[1] !== null && entry[1] !== void 0)
126
+ );
127
+ }
128
+ function addSection(attributes, name, section) {
129
+ const compactedSection = compactAttributes(section);
130
+ if (Object.keys(compactedSection).length > 0) {
131
+ attributes[name] = compactedSection;
132
+ }
133
+ }
134
+ function getDeviceTypeName(deviceType2) {
135
+ switch (deviceType2) {
136
+ case Device.DeviceType.PHONE:
137
+ return "phone";
138
+ case Device.DeviceType.TABLET:
139
+ return "tablet";
140
+ case Device.DeviceType.DESKTOP:
141
+ return "desktop";
142
+ case Device.DeviceType.TV:
143
+ return "tv";
144
+ case Device.DeviceType.UNKNOWN:
145
+ return "unknown";
146
+ default:
147
+ return void 0;
148
+ }
149
+ }
150
+ async function getDeviceType() {
151
+ if (Device.deviceType !== null && Device.deviceType !== void 0) {
152
+ return Device.deviceType;
153
+ }
154
+ try {
155
+ return await Device.getDeviceTypeAsync();
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
160
+ async function collectBubblesDeviceAttributes() {
161
+ const attributes = {};
162
+ const deviceType2 = await getDeviceType();
163
+ addSection(attributes, "device", {
164
+ manufacturer: Device.manufacturer,
165
+ brand: Device.brand,
166
+ modelName: Device.modelName,
167
+ modelId: typeof Device.modelId === "string" ? Device.modelId : void 0,
168
+ deviceName: Device.deviceName,
169
+ deviceType: getDeviceTypeName(deviceType2),
170
+ totalMemory: Device.totalMemory
171
+ });
172
+ addSection(attributes, "os", {
173
+ name: Device.osName,
174
+ version: Device.osVersion,
175
+ buildId: Device.osBuildId,
176
+ platformApiLevel: Device.platformApiLevel
177
+ });
178
+ addSection(attributes, "app", {
179
+ applicationId: Application.applicationId,
180
+ applicationName: Application.applicationName,
181
+ nativeApplicationVersion: Application.nativeApplicationVersion,
182
+ nativeBuildVersion: Application.nativeBuildVersion
183
+ });
184
+ return attributes;
185
+ }
186
+
119
187
  // src/runtime/BubblesNotificationsProvider.tsx
120
188
  import { useCallback as useCallback2, useEffect, useRef as useRef2, useState } from "react";
121
189
 
@@ -362,6 +430,20 @@ function buildRuntimeSnapshot(deviceId, registrationState) {
362
430
  notificationsEnabled: registrationState.notificationsEnabled
363
431
  };
364
432
  }
433
+ async function syncBubblesDeviceAttributesBestEffort(apiBaseUrl, appKey, deviceId) {
434
+ try {
435
+ const attributes = await collectBubblesDeviceAttributes();
436
+ if (Object.keys(attributes).length > 0) {
437
+ await updateBubblesDeviceAttributes({
438
+ apiBaseUrl,
439
+ appKey,
440
+ deviceId,
441
+ attributes
442
+ });
443
+ }
444
+ } catch {
445
+ }
446
+ }
365
447
  async function syncDeviceRegistrationState(options) {
366
448
  const apiBaseUrl = getRequiredNonEmptyString2(options.apiBaseUrl, "apiBaseUrl");
367
449
  const appKey = getRequiredNonEmptyString2(options.appKey, "appKey");
@@ -388,6 +470,13 @@ async function syncDeviceRegistrationState(options) {
388
470
  });
389
471
  const nextDeviceId = syncResult.deviceId ?? options.deviceId;
390
472
  storeDeviceId(nextDeviceId);
473
+ if (nextDeviceId) {
474
+ await syncBubblesDeviceAttributesBestEffort(
475
+ apiBaseUrl,
476
+ appKey,
477
+ nextDeviceId
478
+ );
479
+ }
391
480
  return {
392
481
  ...snapshot,
393
482
  action: syncResult.action,
@@ -578,6 +667,7 @@ function BubblesNotificationsProvider(props) {
578
667
  const [state, setState] = useState(getInitialRuntimeState);
579
668
  const { enqueueOperation, isMountedRef } = useBubblesRuntimeOperationQueue(setState);
580
669
  const lastAutomaticSyncSignatureRef = useRef2(null);
670
+ const currentDeviceIdRef = useRef2(state.deviceId);
581
671
  const previousUserIdRef = useRef2(normalizedUserId);
582
672
  const buildAutomaticSyncSignature = useCallback2((deviceId) => {
583
673
  return getAutomaticSyncSignature({
@@ -603,6 +693,9 @@ function BubblesNotificationsProvider(props) {
603
693
  clearBubblesForegroundPresentation();
604
694
  };
605
695
  }, [isMountedRef]);
696
+ useEffect(() => {
697
+ currentDeviceIdRef.current = state.deviceId;
698
+ }, [state.deviceId]);
606
699
  useEffect(() => {
607
700
  let isCancelled = false;
608
701
  void (async () => {
@@ -670,6 +763,7 @@ function BubblesNotificationsProvider(props) {
670
763
  appVersion,
671
764
  deviceId: storedDeviceId
672
765
  });
766
+ currentDeviceIdRef.current = result.deviceId;
673
767
  return getRuntimeStateUpdate(result);
674
768
  });
675
769
  }, [
@@ -703,9 +797,10 @@ function BubblesNotificationsProvider(props) {
703
797
  userId: normalizedUserId,
704
798
  aliasing,
705
799
  appVersion,
706
- deviceId: state.deviceId,
800
+ deviceId: currentDeviceIdRef.current,
707
801
  registrationOptions: options
708
802
  });
803
+ currentDeviceIdRef.current = result.deviceId;
709
804
  if (result.deviceId !== null) {
710
805
  lastAutomaticSyncSignatureRef.current = buildAutomaticSyncSignature(
711
806
  result.deviceId
@@ -714,8 +809,33 @@ function BubblesNotificationsProvider(props) {
714
809
  return getRuntimeStateUpdate(result);
715
810
  });
716
811
  }
812
+ function addDeviceAttribute(name, value) {
813
+ return enqueueOperation(async () => {
814
+ const attributeName = getRequiredNonEmptyString(name, "attributeName");
815
+ if (value === void 0) {
816
+ failWithBubblesError(
817
+ '"addDeviceAttribute()" requires "value" to be a JSON-compatible value.'
818
+ );
819
+ }
820
+ const deviceId = currentDeviceIdRef.current;
821
+ if (deviceId === null) {
822
+ failWithBubblesError(
823
+ '"addDeviceAttribute()" requires a backend "deviceId" before attributes can be synced.'
824
+ );
825
+ }
826
+ await updateBubblesDeviceAttributes({
827
+ apiBaseUrl,
828
+ appKey,
829
+ deviceId,
830
+ attributes: {
831
+ [attributeName]: value
832
+ }
833
+ });
834
+ });
835
+ }
717
836
  const contextValue = {
718
837
  registerDevice,
838
+ addDeviceAttribute,
719
839
  deviceId: state.deviceId,
720
840
  pushToken: state.pushToken,
721
841
  tokenType: state.tokenType,
@@ -751,6 +871,7 @@ export {
751
871
  buildCreateDevicePayload,
752
872
  buildStoredDeliveryStatusEventKey,
753
873
  buildUpdateDevicePayload,
874
+ collectBubblesDeviceAttributes,
754
875
  createBubblesDevice,
755
876
  createBubblesDeviceClient,
756
877
  createStoredDeliveryStatusEvent,
@@ -785,5 +906,6 @@ export {
785
906
  storeDeviceState,
786
907
  syncBubblesDevice,
787
908
  updateBubblesDevice,
909
+ updateBubblesDeviceAttributes,
788
910
  useBubblesNotifications
789
911
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  registerBubblesBackgroundHandlers
3
- } from "./chunk-6NPXFGQX.mjs";
3
+ } from "./chunk-3BAGWXHY.mjs";
4
4
 
5
5
  // src/register-task.ts
6
6
  registerBubblesBackgroundHandlers();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fishonfire/bubbles-expo",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Expo config plugin, explicit background registration API, and React provider for Bubbles notifications.",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -68,8 +68,10 @@
68
68
  }
69
69
  },
70
70
  "dependencies": {
71
+ "expo-application": "~57.0.2",
71
72
  "@fishonfire/bubbles-js": "0.1.0",
72
73
  "expo-constants": "~57.0.16",
74
+ "expo-device": "~57.0.1",
73
75
  "expo-file-system": "~57.0.6"
74
76
  },
75
77
  "devDependencies": {