@bubblsdk/react-native-sdk 4.1.7 → 5.0.0-alpha.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.
Files changed (176) hide show
  1. package/BubblReactNativeSdk.podspec +29 -9
  2. package/CHANGELOG.md +47 -0
  3. package/LICENSE +202 -1
  4. package/README.md +172 -83
  5. package/android/build.gradle +33 -30
  6. package/android/src/main/AndroidManifest.xml +1 -0
  7. package/android/src/main/java/tech/bubbl/reactnative/BubblJson.kt +135 -0
  8. package/android/src/main/java/tech/bubbl/reactnative/BubblSdkModule.kt +277 -0
  9. package/android/src/main/java/tech/bubbl/reactnative/BubblSdkPackage.kt +26 -0
  10. package/app.plugin.js +2 -0
  11. package/ios/BubblPush.swift +44 -0
  12. package/ios/BubblSdk.h +7 -0
  13. package/ios/BubblSdk.mm +252 -0
  14. package/ios/BubblSdkImpl.swift +335 -0
  15. package/ios-extension/BubblReactNativeNotificationService.podspec +44 -0
  16. package/ios-extension/Sources/BubblCore/API/ApiResponse.swift +90 -0
  17. package/ios-extension/Sources/BubblCore/API/CredentialStore.swift +116 -0
  18. package/ios-extension/Sources/BubblCore/API/DeviceApiClient.swift +239 -0
  19. package/ios-extension/Sources/BubblCore/API/ErrorActions.swift +68 -0
  20. package/ios-extension/Sources/BubblCore/API/Http.swift +39 -0
  21. package/ios-extension/Sources/BubblCore/API/ServerClock.swift +69 -0
  22. package/ios-extension/Sources/BubblCore/BubblVersion.swift +5 -0
  23. package/ios-extension/Sources/BubblCore/Device/ConfigSync.swift +216 -0
  24. package/ios-extension/Sources/BubblCore/Device/DeviceSync.swift +160 -0
  25. package/ios-extension/Sources/BubblCore/Device/ProvisioningProfile.swift +24 -0
  26. package/ios-extension/Sources/BubblCore/Engine/EngineCore.swift +523 -0
  27. package/ios-extension/Sources/BubblCore/Engine/Geofencing.swift +170 -0
  28. package/ios-extension/Sources/BubblCore/Engine/Permissions.swift +90 -0
  29. package/ios-extension/Sources/BubblCore/Engine/Privacy.swift +64 -0
  30. package/ios-extension/Sources/BubblCore/Engine/PrivacyControls.swift +184 -0
  31. package/ios-extension/Sources/BubblCore/Engine/Pushes.swift +61 -0
  32. package/ios-extension/Sources/BubblCore/Engine/Sandbox.swift +149 -0
  33. package/ios-extension/Sources/BubblCore/Events/EventQueue.swift +207 -0
  34. package/ios-extension/Sources/BubblCore/Events/EventStore.swift +159 -0
  35. package/ios-extension/Sources/BubblCore/Geofence/Geofence.swift +160 -0
  36. package/ios-extension/Sources/BubblCore/Geofence/GeofenceEngine.swift +488 -0
  37. package/ios-extension/Sources/BubblCore/Geofence/GeofenceState.swift +103 -0
  38. package/ios-extension/Sources/BubblCore/Geofence/RegionBudget.swift +62 -0
  39. package/ios-extension/Sources/BubblCore/Notification/BubblNotification.swift +137 -0
  40. package/ios-extension/Sources/BubblCore/Notification/NotificationEvents.swift +160 -0
  41. package/ios-extension/Sources/BubblCore/Notification/Push.swift +134 -0
  42. package/ios-extension/Sources/BubblCore/Notification/SurveyForm.swift +185 -0
  43. package/ios-extension/Sources/BubblCore/RequestSigner.swift +56 -0
  44. package/ios-extension/Sources/BubblCore/Support/AsyncMutex.swift +39 -0
  45. package/ios-extension/Sources/BubblCore/Support/BubblLog.swift +52 -0
  46. package/ios-extension/Sources/BubblCore/Support/JSON.swift +31 -0
  47. package/ios-extension/Sources/BubblCore/Support/JSONValue.swift +94 -0
  48. package/ios-extension/Sources/BubblCore/Support/Locking.swift +11 -0
  49. package/ios-extension/Sources/BubblCore/Support/ValueStore.swift +109 -0
  50. package/ios-extension/Sources/BubblNotificationService/BubblNotificationService.swift +124 -0
  51. package/ios-sdk/BubblCore/API/ApiResponse.swift +90 -0
  52. package/ios-sdk/BubblCore/API/CredentialStore.swift +116 -0
  53. package/ios-sdk/BubblCore/API/DeviceApiClient.swift +239 -0
  54. package/ios-sdk/BubblCore/API/ErrorActions.swift +68 -0
  55. package/ios-sdk/BubblCore/API/Http.swift +39 -0
  56. package/ios-sdk/BubblCore/API/ServerClock.swift +69 -0
  57. package/ios-sdk/BubblCore/BubblVersion.swift +5 -0
  58. package/ios-sdk/BubblCore/Device/ConfigSync.swift +216 -0
  59. package/ios-sdk/BubblCore/Device/DeviceSync.swift +160 -0
  60. package/ios-sdk/BubblCore/Device/ProvisioningProfile.swift +24 -0
  61. package/ios-sdk/BubblCore/Engine/EngineCore.swift +523 -0
  62. package/ios-sdk/BubblCore/Engine/Geofencing.swift +170 -0
  63. package/ios-sdk/BubblCore/Engine/Permissions.swift +90 -0
  64. package/ios-sdk/BubblCore/Engine/Privacy.swift +64 -0
  65. package/ios-sdk/BubblCore/Engine/PrivacyControls.swift +184 -0
  66. package/ios-sdk/BubblCore/Engine/Pushes.swift +61 -0
  67. package/ios-sdk/BubblCore/Engine/Sandbox.swift +149 -0
  68. package/ios-sdk/BubblCore/Events/EventQueue.swift +207 -0
  69. package/ios-sdk/BubblCore/Events/EventStore.swift +159 -0
  70. package/ios-sdk/BubblCore/Geofence/Geofence.swift +160 -0
  71. package/ios-sdk/BubblCore/Geofence/GeofenceEngine.swift +488 -0
  72. package/ios-sdk/BubblCore/Geofence/GeofenceState.swift +103 -0
  73. package/ios-sdk/BubblCore/Geofence/RegionBudget.swift +62 -0
  74. package/ios-sdk/BubblCore/Notification/BubblNotification.swift +137 -0
  75. package/ios-sdk/BubblCore/Notification/NotificationEvents.swift +160 -0
  76. package/ios-sdk/BubblCore/Notification/Push.swift +134 -0
  77. package/ios-sdk/BubblCore/Notification/SurveyForm.swift +185 -0
  78. package/ios-sdk/BubblCore/RequestSigner.swift +56 -0
  79. package/ios-sdk/BubblCore/Support/AsyncMutex.swift +39 -0
  80. package/ios-sdk/BubblCore/Support/BubblLog.swift +52 -0
  81. package/ios-sdk/BubblCore/Support/JSON.swift +31 -0
  82. package/ios-sdk/BubblCore/Support/JSONValue.swift +94 -0
  83. package/ios-sdk/BubblCore/Support/Locking.swift +11 -0
  84. package/ios-sdk/BubblCore/Support/ValueStore.swift +109 -0
  85. package/ios-sdk/BubblLaunch/BubblLaunch.m +45 -0
  86. package/ios-sdk/BubblLaunch/include/BubblLaunch.h +5 -0
  87. package/ios-sdk/BubblSDK/Bubbl.swift +308 -0
  88. package/ios-sdk/BubblSDK/BubblBackend.swift +118 -0
  89. package/ios-sdk/BubblSDK/BubblCredential.swift +30 -0
  90. package/ios-sdk/BubblSDK/BubblMessage.swift +151 -0
  91. package/ios-sdk/BubblSDK/BubblOptions.swift +39 -0
  92. package/ios-sdk/BubblSDK/BubblPermissions.swift +77 -0
  93. package/ios-sdk/BubblSDK/BubblTestDeviceResult.swift +10 -0
  94. package/ios-sdk/BubblSDK/Engine/BackgroundRefresh.swift +92 -0
  95. package/ios-sdk/BubblSDK/Engine/EngineHost+Geofences.swift +177 -0
  96. package/ios-sdk/BubblSDK/Engine/EngineHost+Privacy.swift +65 -0
  97. package/ios-sdk/BubblSDK/Engine/EngineHost+Push.swift +154 -0
  98. package/ios-sdk/BubblSDK/Engine/EngineHost.swift +356 -0
  99. package/ios-sdk/BubblSDK/Engine/SupportedBubbl.swift +318 -0
  100. package/ios-sdk/BubblSDK/Engine/SystemLog.swift +27 -0
  101. package/ios-sdk/BubblSDK/Engine/WorkRunner.swift +107 -0
  102. package/ios-sdk/BubblSDK/Notification/Look.swift +78 -0
  103. package/ios-sdk/BubblSDK/Notification/NotificationMedia.swift +329 -0
  104. package/ios-sdk/BubblSDK/Notification/NotificationModel.swift +172 -0
  105. package/ios-sdk/BubblSDK/Notification/NotificationScreen.swift +428 -0
  106. package/ios-sdk/BubblSDK/Notification/ScreenPresenter.swift +148 -0
  107. package/ios-sdk/BubblSDK/Permissions/PermissionFlow.swift +167 -0
  108. package/ios-sdk/BubblSDK/Permissions/PrivacyView.swift +110 -0
  109. package/ios-sdk/BubblSDK/Platform/AppleDevicePlatform.swift +95 -0
  110. package/ios-sdk/BubblSDK/Platform/KeychainCredentialStore.swift +114 -0
  111. package/ios-sdk/BubblSDK/Platform/LocationService.swift +418 -0
  112. package/ios-sdk/BubblSDK/Platform/PushIntegration.swift +288 -0
  113. package/ios-sdk/BubblSDK/Platform/URLSessionHttpClient.swift +69 -0
  114. package/ios-sdk/BubblSDK/PrivacyInfo.xcprivacy +76 -0
  115. package/jest/index.js +75 -0
  116. package/lib/commonjs/BubblModule.js +14 -0
  117. package/lib/commonjs/BubblModule.js.map +1 -0
  118. package/lib/commonjs/BubblModule.web.js +13 -0
  119. package/lib/commonjs/BubblModule.web.js.map +1 -0
  120. package/lib/commonjs/NativeBubblSdk.js +20 -0
  121. package/lib/commonjs/NativeBubblSdk.js.map +1 -0
  122. package/lib/commonjs/index.js +336 -0
  123. package/lib/commonjs/index.js.map +1 -0
  124. package/lib/commonjs/package.json +1 -0
  125. package/lib/commonjs/types.js +2 -0
  126. package/lib/commonjs/types.js.map +1 -0
  127. package/lib/module/BubblModule.js +5 -0
  128. package/lib/module/BubblModule.js.map +1 -0
  129. package/lib/module/BubblModule.web.js +9 -0
  130. package/lib/module/BubblModule.web.js.map +1 -0
  131. package/lib/module/NativeBubblSdk.js +18 -0
  132. package/lib/module/NativeBubblSdk.js.map +1 -0
  133. package/lib/module/index.js +331 -0
  134. package/lib/module/index.js.map +1 -0
  135. package/lib/module/package.json +1 -0
  136. package/lib/module/types.js +2 -0
  137. package/lib/module/types.js.map +1 -0
  138. package/lib/typescript/commonjs/package.json +1 -0
  139. package/lib/typescript/commonjs/src/BubblModule.d.ts +3 -0
  140. package/lib/typescript/commonjs/src/BubblModule.d.ts.map +1 -0
  141. package/lib/typescript/commonjs/src/BubblModule.web.d.ts +8 -0
  142. package/lib/typescript/commonjs/src/BubblModule.web.d.ts.map +1 -0
  143. package/lib/typescript/commonjs/src/NativeBubblSdk.d.ts +59 -0
  144. package/lib/typescript/commonjs/src/NativeBubblSdk.d.ts.map +1 -0
  145. package/lib/typescript/commonjs/src/index.d.ts +111 -0
  146. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  147. package/lib/typescript/commonjs/src/types.d.ts +192 -0
  148. package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
  149. package/lib/typescript/module/package.json +1 -0
  150. package/lib/typescript/module/src/BubblModule.d.ts +3 -0
  151. package/lib/typescript/module/src/BubblModule.d.ts.map +1 -0
  152. package/lib/typescript/module/src/BubblModule.web.d.ts +8 -0
  153. package/lib/typescript/module/src/BubblModule.web.d.ts.map +1 -0
  154. package/lib/typescript/module/src/NativeBubblSdk.d.ts +59 -0
  155. package/lib/typescript/module/src/NativeBubblSdk.d.ts.map +1 -0
  156. package/lib/typescript/module/src/index.d.ts +111 -0
  157. package/lib/typescript/module/src/index.d.ts.map +1 -0
  158. package/lib/typescript/module/src/types.d.ts +192 -0
  159. package/lib/typescript/module/src/types.d.ts.map +1 -0
  160. package/package.json +105 -24
  161. package/plugin/build/imageNotifications.d.ts +35 -0
  162. package/plugin/build/imageNotifications.js +198 -0
  163. package/plugin/build/index.d.ts +84 -0
  164. package/plugin/build/index.js +176 -0
  165. package/src/BubblModule.ts +2 -0
  166. package/src/BubblModule.web.ts +9 -0
  167. package/src/NativeBubblSdk.ts +67 -0
  168. package/src/index.ts +338 -282
  169. package/src/types.ts +182 -0
  170. package/android/src/main/kotlin/tech/bubbl/reactnative/BubblSdkModule.kt +0 -680
  171. package/android/src/main/kotlin/tech/bubbl/reactnative/BubblSdkNotificationIntents.kt +0 -32
  172. package/android/src/main/kotlin/tech/bubbl/reactnative/BubblSdkPackage.kt +0 -14
  173. package/ios/BubblSdk.swift +0 -802
  174. package/ios/BubblSdkBridge.m +0 -86
  175. package/react-native.config.js +0 -11
  176. package/src/specs/NativeBubblSdk.ts +0 -54
package/src/index.ts CHANGED
@@ -1,323 +1,379 @@
1
- import {
2
- NativeEventEmitter,
3
- NativeModules,
4
- type EmitterSubscription,
5
- } from 'react-native';
6
- import NativeBubblSdk from './specs/NativeBubblSdk';
7
-
8
- export type BubblEnvironment = 'development' | 'staging' | 'production';
9
- export type BubblLogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug';
10
- export type BubblNotificationRenderingMode = 'sdkDefault' | 'hostRendered' | 'eventOnly';
11
- export type BubblNotificationSource = 'firebase' | 'apns' | 'runtime' | 'geofence' | 'manual';
12
- export type BubblNotificationModalTheme = 'light' | 'dark';
13
-
14
- export type BubblNotificationModalStyle = {
15
- theme?: BubblNotificationModalTheme;
16
- transparentBackdrop?: boolean;
17
- backdropColor?: string | null;
18
- cardBackgroundColor?: string | null;
19
- cardBorderColor?: string | null;
20
- titleColor?: string | null;
21
- bodyColor?: string | null;
22
- accentColor?: string | null;
23
- iconBackgroundColor?: string | null;
24
- iconTextColor?: string | null;
25
- primaryButtonBackgroundColor?: string | null;
26
- primaryButtonTextColor?: string | null;
27
- secondaryButtonBackgroundColor?: string | null;
28
- secondaryButtonTextColor?: string | null;
29
- textButtonColor?: string | null;
30
- surveyBackgroundColor?: string | null;
31
- inputBackgroundColor?: string | null;
32
- inputTextColor?: string | null;
33
- inputBorderColor?: string | null;
34
- cornerRadius?: number | null;
35
- buttonCornerRadius?: number | null;
1
+ import { NativeEventEmitter, Platform } from 'react-native';
2
+ import NativeBubblSdk from './BubblModule';
3
+ import type { Spec } from './NativeBubblSdk';
4
+ import type {
5
+ BubblCredential,
6
+ BubblDiagnostics,
7
+ BubblEvent,
8
+ BubblMessage,
9
+ BubblOptions,
10
+ BubblPermissionStatus,
11
+ BubblTestDeviceResult,
12
+ EventListener,
13
+ NotificationListener,
14
+ Subscription,
15
+ SurveyAnswers,
16
+ } from './types';
17
+
18
+ export type * from './types';
19
+
20
+ /** The one native event every event, and every notification for the app's listener, arrives on. */
21
+ const NATIVE_EVENT = 'BubblSdkEvent';
22
+
23
+ const PACKAGE = '@bubblsdk/react-native-sdk';
24
+ const JEST_MOCK = `jest.mock('${PACKAGE}', () => require('${PACKAGE}/jest'))`;
25
+
26
+ type Globals = {
27
+ __turboModuleProxy?: unknown;
28
+ RN$Bridgeless?: boolean;
29
+ process?: { env?: Record<string, string | undefined> };
36
30
  };
37
31
 
38
- export type BubblConfig = {
39
- apiKey: string;
40
- environment?: BubblEnvironment;
41
- runtimeBaseUrl?: string | null;
42
- transmissionBaseUrl?: string | null;
43
- ingestBaseUrl?: string | null;
44
- segments?: string[];
45
- correlationId?: string | null;
46
- defaultDistanceMeters?: number;
47
- refreshIntervalSeconds?: number;
48
- enablePushHandling?: boolean;
49
- enableLocationTracking?: boolean;
50
- notificationRenderingMode?: BubblNotificationRenderingMode;
51
- enableDefaultNotificationModal?: boolean;
52
- defaultNotificationModalStyle?: BubblNotificationModalStyle | null;
53
- enableDefaultSurveyUi?: boolean;
54
- logLevel?: BubblLogLevel;
55
- };
56
-
57
- export type BubblBootResult = {
58
- ready: boolean;
59
- fromCache: boolean;
60
- deviceRegistered: boolean;
61
- requiresPermission: string[];
62
- warnings: string[];
63
- };
64
-
65
- export type BubblLocation = {
66
- latitude: number;
67
- longitude: number;
68
- };
69
-
70
- export type BubblGeofenceTransitionType = 'enter' | 'exit';
71
-
72
- export type BubblGeofenceTransition = {
73
- type: BubblGeofenceTransitionType;
74
- campaignId?: string | null;
75
- locationId?: string | null;
76
- location: BubblLocation;
77
- };
78
-
79
- export type BubblGeofenceVertex = BubblLocation;
80
-
81
- export type BubblGeofencePolygon = {
82
- campaignId?: string | null;
83
- campaignName?: string | null;
84
- locationId?: string | null;
85
- vertices: BubblGeofenceVertex[];
86
- };
87
-
88
- export type BubblGeofenceCircle = {
89
- campaignId?: string | null;
90
- campaignName?: string | null;
91
- locationId?: string | null;
92
- center: BubblGeofenceVertex;
93
- radius: number;
94
- };
95
-
96
- export type BubblGeofenceSnapshot = {
97
- stats: {
98
- campaignsTotal: number;
99
- polygonsTotal: number;
100
- };
101
- polygons: BubblGeofencePolygon[];
102
- circles: BubblGeofenceCircle[];
103
- };
104
-
105
- export type BubblTrackEvent = {
106
- type: string;
107
- activity: string;
108
- locationId?: string | null;
109
- curatedNotificationId?: string | null;
110
- latitude?: number | null;
111
- longitude?: number | null;
112
- };
113
-
114
- export type BubblSurveyAnswer = {
115
- questionId: string;
116
- type: string;
117
- value?: string | null;
118
- choiceIds?: string[];
119
- };
120
-
121
- export type BubblSurveyResponse = {
122
- curatedNotificationId: string;
123
- locationId?: string | null;
124
- answers: BubblSurveyAnswer[];
125
- };
126
-
127
- export type BubblNotificationPayload = {
128
- id: string;
129
- title: string;
130
- body: string;
131
- source?: BubblNotificationSource;
132
- locationId?: string | null;
133
- curatedNotificationId?: string | null;
134
- correlationId?: string | null;
135
- media?: BubblNotificationMedia | null;
136
- cta?: BubblNotificationCta | null;
137
- survey?: BubblNotificationSurvey | null;
138
- raw?: Record<string, string>;
139
- };
140
-
141
- export type BubblNotificationMedia = {
142
- url: string;
143
- type?: string | null;
144
- altText?: string | null;
145
- };
146
-
147
- export type BubblNotificationCta = {
148
- label: string;
149
- url?: string | null;
150
- action?: string | null;
151
- };
152
-
153
- export type BubblNotificationSurvey = {
154
- questions?: BubblSurveyQuestion[];
155
- };
156
-
157
- export type BubblSurveyQuestion = {
158
- id: string;
159
- title: string;
160
- type: string;
161
- choices?: BubblSurveyChoice[];
162
- };
163
-
164
- export type BubblSurveyChoice = {
165
- id: string;
166
- label: string;
167
- };
168
-
169
- export type BubblNotificationDisplayResult = {
170
- displayed: boolean;
171
- reason?: string | null;
172
- };
32
+ /** Why there's no native module, as specifically as can be told from here. */
33
+ function missingReason(): string {
34
+ const globals = globalThis as Globals;
35
+ if (Platform.OS === 'web') {
36
+ return `${PACKAGE}: Bubbl is for Android and iOS; on the web its calls do nothing.`;
37
+ }
38
+ if (globals.process?.env?.JEST_WORKER_ID != null) {
39
+ return `${PACKAGE}: there's no native module in Jest, so Bubbl's calls do nothing. Mock it: ${JEST_MOCK}.`;
40
+ }
41
+ if (globals.__turboModuleProxy == null && globals.RN$Bridgeless !== true) {
42
+ return (
43
+ `${PACKAGE}: Bubbl needs React Native's New Architecture, and this app runs the old one, so ` +
44
+ "Bubbl's calls do nothing. Turn it on (newArchEnabled=true in android/gradle.properties, " +
45
+ 'RCT_NEW_ARCH_ENABLED=1 for pod install, or newArchEnabled in the Expo config) and rebuild.'
46
+ );
47
+ }
48
+ return (
49
+ `${PACKAGE}: the native module isn't in this build, so Bubbl's calls do nothing. Rebuild the app ` +
50
+ "after installing (npx expo prebuild, or pod install / a Gradle sync); Expo Go can't load it."
51
+ );
52
+ }
53
+ let reportedMissing = false;
54
+
55
+ /**
56
+ * The native module, or null where there isn't one (web, Expo Go, Jest, a build from before the
57
+ * install), said once in the console. Bubbl never stops the app, so without it calls do nothing.
58
+ */
59
+ function native(): Spec | null {
60
+ if (!NativeBubblSdk && !reportedMissing) {
61
+ reportedMissing = true;
62
+ (Platform.OS === 'web' ? console.warn : console.error)(missingReason());
63
+ }
64
+ return NativeBubblSdk ?? null;
65
+ }
173
66
 
174
- export type BubblDiagnostics = {
175
- sdkVersion: string;
176
- platform: 'react-native';
177
- booted: boolean;
178
- pendingIngestCount: number;
179
- pushTokenSuffix?: string | null;
180
- };
67
+ const eventListeners = new Set<EventListener>();
68
+ let notificationListener: NotificationListener | null = null;
69
+ let nativeSubscription: { remove(): void } | null = null;
70
+
71
+ /**
72
+ * Listens to the native event once anything wants it: events go to every event listener, and
73
+ * "notification.handle" (a notification the native side held back for the app's listener, only
74
+ * ever while the app is in the foreground) goes to the notification listener.
75
+ */
76
+ function listenToNative(): void {
77
+ if (nativeSubscription || !NativeBubblSdk) {
78
+ return;
79
+ }
80
+ nativeSubscription = new NativeEventEmitter(NativeBubblSdk as never).addListener(
81
+ NATIVE_EVENT,
82
+ (json: unknown) => {
83
+ let payload: { type?: string; token?: string; message?: unknown; locationId?: string };
84
+ try {
85
+ payload = JSON.parse(String(json));
86
+ } catch {
87
+ return;
88
+ }
89
+ if (payload.type === 'notification.handle') {
90
+ void handleNotification(String(payload.token), payload.message as BubblMessage);
91
+ return;
92
+ }
93
+ const event = payload as BubblEvent;
94
+ eventListeners.forEach((listener) => {
95
+ try {
96
+ listener(event);
97
+ } catch (error) {
98
+ console.error('Bubbl: an event listener threw', error);
99
+ }
100
+ });
101
+ },
102
+ );
103
+ }
181
104
 
182
- export type BubblEvent =
183
- | { type: 'ready' }
184
- | { type: 'diagnostic'; diagnostics: BubblDiagnostics }
185
- | { type: 'notificationReceived'; payload: BubblNotificationPayload }
186
- | { type: 'notificationDisplayed'; payload: BubblNotificationPayload }
187
- | { type: 'notificationTapped'; payload: BubblNotificationPayload; action?: string | null }
188
- | { type: 'notificationCtaTapped'; payload: BubblNotificationPayload; action?: string | null }
189
- | { type: 'notificationMediaViewed'; payload: BubblNotificationPayload }
190
- | { type: 'notificationSurveyRequested'; payload: BubblNotificationPayload }
191
- | { type: 'locationUpdated'; location: BubblLocation }
192
- | { type: 'geofenceSnapshot'; snapshot: BubblGeofenceSnapshot }
193
- | { type: 'geofenceEntered'; transition: BubblGeofenceTransition }
194
- | { type: 'geofenceExited'; transition: BubblGeofenceTransition }
195
- | { type: 'error'; code: string; message: string };
196
-
197
- export type BubblEventSubscription = Pick<EmitterSubscription, 'remove'>;
198
-
199
- const EVENT_NAME = 'BubblSdkEvent';
200
-
201
- type NativeBubblSdkShape = NonNullable<typeof NativeBubblSdk>;
202
-
203
- function nativeModule(): NativeBubblSdkShape {
204
- const module = NativeBubblSdk ?? NativeModules.BubblSdk;
205
- if (!module) {
206
- throw new Error('BubblSdk native module is not linked.');
105
+ function stopListeningIfIdle(): void {
106
+ if (eventListeners.size === 0 && !notificationListener) {
107
+ nativeSubscription?.remove();
108
+ nativeSubscription = null;
207
109
  }
110
+ }
208
111
 
209
- return module as NativeBubblSdkShape;
112
+ /**
113
+ * The app's listener decides; anything but true (false, a throw, no listener any more) is Bubbl's
114
+ * to show. Claimed first, so native knows JS has it; finished after, so native shows it unless the
115
+ * app does. If JS goes away in between (a reload), native shows it: it's never lost.
116
+ */
117
+ async function handleNotification(token: string, message: BubblMessage): Promise<void> {
118
+ native()?.claimNotification(token);
119
+ let showsItself = false;
120
+ try {
121
+ showsItself = notificationListener ? (await notificationListener(message)) === true : false;
122
+ } catch (error) {
123
+ console.error('Bubbl: the notification listener threw; Bubbl shows the notification', error);
124
+ }
125
+ native()?.finishNotification(token, showsItself);
210
126
  }
211
127
 
212
- function eventEmitter(): NativeEventEmitter {
213
- return new NativeEventEmitter(NativeModules.BubblSdk ?? (NativeBubblSdk as object | null));
128
+ /** Diagnostics where there's no native module: nothing runs, and lastError says why. */
129
+ function unavailableDiagnostics(): BubblDiagnostics {
130
+ return {
131
+ started: false,
132
+ sdkVersion: '',
133
+ supported: false,
134
+ sdkSupported: true,
135
+ registered: false,
136
+ installId: null,
137
+ active: false,
138
+ pausedUntil: null,
139
+ consentRequired: false,
140
+ consent: null,
141
+ locationEnabled: false,
142
+ permissions: null,
143
+ hasPushToken: false,
144
+ geofences: 0,
145
+ queuedEvents: 0,
146
+ lastError: missingReason(),
147
+ credentialRejected: false,
148
+ environment: null,
149
+ testDevice: null,
150
+ backgroundLocation: false,
151
+ transitionsDroppedWhileLocked: 0,
152
+ backgroundRestricted: null,
153
+ batteryOptimized: null,
154
+ backgroundRefreshAvailable: null,
155
+ lowPowerMode: null,
156
+ osWatchesGeofences: null,
157
+ };
214
158
  }
215
159
 
216
- function validateApiKey(config: BubblConfig): void {
217
- if (!config.apiKey?.trim()) {
218
- throw new Error('apiKey is required');
219
- }
160
+ function parse<T>(json: string | null): T | null {
161
+ return json == null ? null : (JSON.parse(json) as T);
220
162
  }
221
163
 
164
+ /**
165
+ * Bubbl for a React Native app: the same functions as the native SDKs (docs/PUBLIC_API.md).
166
+ * Install the package, then call start once, early, e.g. at the top of index.js:
167
+ *
168
+ * Bubbl.start('pk_live_…', { baseUrl: 'https://…' });
169
+ *
170
+ * Bubbl never stops the app: a mistake (an empty key, an http:// URL) is logged natively, not
171
+ * thrown, calls before start do nothing, and so do calls where there's no native module (web,
172
+ * Expo Go, Jest), said once in the console. Geofences and pushes keep working with the app
173
+ * closed: the native SDK starts itself from its saved settings when the OS wakes the app.
174
+ */
222
175
  export const Bubbl = {
223
- async boot(config: BubblConfig): Promise<BubblBootResult> {
224
- validateApiKey(config);
225
- return nativeModule().boot(config) as Promise<BubblBootResult>;
176
+ /**
177
+ * Whether Bubbl works on this device: iOS 17 and later, Android 8.1 (API 27) and later. The
178
+ * package installs on older versions too, and there every call does nothing (permissions give
179
+ * null, the listener is never asked, diagnostics say supported: false). False where there's no
180
+ * native module.
181
+ */
182
+ get isSupported(): boolean {
183
+ return native()?.isSupported() ?? false;
226
184
  },
227
- async shutdown(): Promise<void> {
228
- return nativeModule().shutdown();
229
- },
230
- async startLocationTracking(): Promise<void> {
231
- return nativeModule().startLocationTracking();
232
- },
233
- async stopLocationTracking(): Promise<void> {
234
- return nativeModule().stopLocationTracking();
235
- },
236
- async refresh(): Promise<void> {
237
- return nativeModule().refresh();
238
- },
239
- async refreshGeofence(location: BubblLocation): Promise<void> {
240
- return nativeModule().refreshGeofence(location.latitude, location.longitude);
241
- },
242
- async handleLocationUpdate(location: BubblLocation): Promise<void> {
243
- return nativeModule().handleLocationUpdate(location.latitude, location.longitude);
244
- },
245
- async refreshPush(): Promise<void> {
246
- return nativeModule().refreshPush();
185
+
186
+ /** Start Bubbl. Calling again with the same key and options changes nothing. */
187
+ start(apiKey: string, options: BubblOptions): void {
188
+ native()?.start(apiKey, JSON.stringify(options));
247
189
  },
248
- async getConfiguration(): Promise<Record<string, unknown> | null> {
249
- return nativeModule().getConfiguration() as Promise<Record<string, unknown> | null>;
190
+
191
+ /**
192
+ * Start with a device credential issued outside the app (installs provisioned ahead of time, an
193
+ * app's own pairing flow) instead of an API key: the device never registers itself. As start
194
+ * otherwise, at every launch. A different credential than last time starts afresh as a new
195
+ * device; if the server refuses it, Bubbl stops ('credential.rejected', diagnostics) until it's
196
+ * started with a new one. An incomplete credential is logged natively (without the secret).
197
+ */
198
+ startWithCredential(credential: BubblCredential, options: BubblOptions): void {
199
+ // The secret goes to the native SDK and nowhere else: not into JSON, not into a log.
200
+ native()?.startWithCredential(
201
+ credential.keyId,
202
+ credential.secret,
203
+ credential.installId,
204
+ JSON.stringify(options),
205
+ );
250
206
  },
251
- async getPrivacyText(): Promise<string> {
252
- return nativeModule().getPrivacyText();
207
+
208
+ /** Stop on this device until start is called again; the server isn't told, nothing is dropped. */
209
+ stop(): void {
210
+ native()?.stop();
253
211
  },
254
- async updateSegments(tags: string[]): Promise<void> {
255
- return nativeModule().updateSegments(tags);
212
+
213
+ /** For requireConsent: true starts everything, false is optOut. */
214
+ setConsent(granted: boolean): void {
215
+ native()?.setConsent(granted);
256
216
  },
257
- async setCorrelationId(value: string): Promise<void> {
258
- return nativeModule().setCorrelationId(value);
217
+
218
+ /** Stop for this user; the server is told. */
219
+ optOut(): void {
220
+ native()?.optOut();
259
221
  },
260
- async clearCorrelationId(): Promise<void> {
261
- return nativeModule().clearCorrelationId();
222
+
223
+ /** Erase the device on the server and here; Bubbl stays off afterwards. */
224
+ deleteMyData(): void {
225
+ native()?.deleteMyData();
262
226
  },
263
- async setDefaultNotificationModalEnabled(enabled: boolean): Promise<void> {
264
- return nativeModule().setDefaultNotificationModalEnabled(enabled);
227
+
228
+ /** Location (geofences) off or on; the rest carries on. */
229
+ setLocationEnabled(enabled: boolean): void {
230
+ native()?.setLocationEnabled(enabled);
265
231
  },
266
- async setDefaultNotificationModalStyle(style: BubblNotificationModalStyle | null): Promise<void> {
267
- return nativeModule().setDefaultNotificationModalStyle(style);
232
+
233
+ /**
234
+ * The permissions Bubbl uses. Asking shows the privacy view first when the dashboard says so,
235
+ * then the system prompt.
236
+ */
237
+ permissions: {
238
+ /** Where the device stands; null before start. */
239
+ async status(): Promise<BubblPermissionStatus | null> {
240
+ return parse((await native()?.permissionStatus()) ?? null);
241
+ },
242
+ async requestNotifications(): Promise<BubblPermissionStatus | null> {
243
+ return parse((await native()?.requestNotifications()) ?? null);
244
+ },
245
+ /** While the app is in use, or always (geofences with the app closed): a second step. */
246
+ async requestLocation(options: { always?: boolean } = {}): Promise<BubblPermissionStatus | null> {
247
+ return parse((await native()?.requestLocation(options.always === true)) ?? null);
248
+ },
249
+ /** The app's page in system settings, for a permission refused for good. */
250
+ openSettings(): void {
251
+ native()?.openSettings();
252
+ },
268
253
  },
269
- async resetDefaultNotificationModalStyle(): Promise<void> {
270
- return nativeModule().setDefaultNotificationModalStyle(null);
254
+
255
+ /** The device's segments (replaces any set before). */
256
+ setSegments(segments: string[]): void {
257
+ native()?.setSegments(segments);
271
258
  },
272
- async disableDefaultNotificationModal(): Promise<void> {
273
- return nativeModule().setDefaultNotificationModalEnabled(false);
259
+
260
+ /**
261
+ * A custom event: a name of letters, digits and . _ : - (at most 100), with up to 50 flat
262
+ * properties (text, numbers, true/false, null).
263
+ */
264
+ track(name: string, properties: Record<string, string | number | boolean | null> = {}): void {
265
+ native()?.track(name, JSON.stringify(properties));
274
266
  },
275
- async enableDefaultNotificationModal(): Promise<void> {
276
- return nativeModule().setDefaultNotificationModalEnabled(true);
267
+
268
+ /** BubblEvents as they happen. */
269
+ addEventListener(listener: EventListener): Subscription {
270
+ eventListeners.add(listener);
271
+ listenToNative();
272
+ return {
273
+ remove: () => {
274
+ eventListeners.delete(listener);
275
+ stopListeningIfIdle();
276
+ },
277
+ };
277
278
  },
278
- async registerPushToken(token: string): Promise<void> {
279
- return nativeModule().registerPushToken(token);
279
+
280
+ /**
281
+ * The app's say before Bubbl shows a notification while the app is open: return true to show it
282
+ * yourself (then report what happens with the report functions), false to let Bubbl. With the
283
+ * app in the background it isn't asked: Bubbl posts its system notification, whose tap opens
284
+ * Bubbl's card. Null goes back to Bubbl showing everything.
285
+ */
286
+ setNotificationListener(listener: NotificationListener | null): void {
287
+ notificationListener = listener;
288
+ if (listener) {
289
+ listenToNative();
290
+ }
291
+ native()?.setNotificationListenerEnabled(listener != null);
292
+ stopListeningIfIdle();
280
293
  },
281
- async handleFirebasePayload(payload: Record<string, string>): Promise<BubblNotificationPayload | null> {
282
- return nativeModule().handleFirebasePayload(payload) as Promise<BubblNotificationPayload | null>;
294
+
295
+ /** Show a message in Bubbl's notification screen (e.g. one kept from before). */
296
+ present(message: BubblMessage): void {
297
+ native()?.present(message.json);
283
298
  },
284
- async showNotification(payload: BubblNotificationPayload): Promise<BubblNotificationDisplayResult> {
285
- return nativeModule().showNotification(payload) as Promise<BubblNotificationDisplayResult>;
299
+
300
+ /** For an app drawing notifications itself: it's on screen. */
301
+ reportDisplayed(message: BubblMessage): void {
302
+ native()?.reportDisplayed(message.json);
286
303
  },
287
- async handleNotificationPayload(payload: BubblNotificationPayload): Promise<BubblNotificationDisplayResult> {
288
- return nativeModule().handleNotificationPayload(payload) as Promise<BubblNotificationDisplayResult>;
304
+ /** …the user opened it. */
305
+ reportOpened(message: BubblMessage): void {
306
+ native()?.reportOpened(message.json);
289
307
  },
290
- async handleNotificationOpen(payload: BubblNotificationPayload, action?: string | null): Promise<void> {
291
- return nativeModule().handleNotificationOpen(payload, action);
308
+ /** …the user tapped its call to action. */
309
+ reportCtaClicked(message: BubblMessage): void {
310
+ native()?.reportCtaClicked(message.json);
292
311
  },
293
- async openNotificationModal(payload: BubblNotificationPayload, action?: string | null): Promise<boolean> {
294
- return nativeModule().openNotificationModal(payload, action);
312
+ /** …the user closed it without acting. */
313
+ reportDismissed(message: BubblMessage): void {
314
+ native()?.reportDismissed(message.json);
295
315
  },
296
- async handleNotificationCta(payload: BubblNotificationPayload, action?: string | null): Promise<void> {
297
- return nativeModule().handleNotificationCta(payload, action);
316
+ /** …its media was shown or started playing. */
317
+ reportMediaViewed(message: BubblMessage): void {
318
+ native()?.reportMediaViewed(message.json);
298
319
  },
299
- async handleNotificationMediaViewed(payload: BubblNotificationPayload): Promise<void> {
300
- return nativeModule().handleNotificationMediaViewed(payload);
320
+ /** …its video or audio played to the end, durationSeconds long. */
321
+ reportMediaCompleted(message: BubblMessage, durationSeconds: number): void {
322
+ native()?.reportMediaCompleted(message.json, durationSeconds);
301
323
  },
302
- async handleNotificationSurveyRequested(payload: BubblNotificationPayload): Promise<void> {
303
- return nativeModule().handleNotificationSurveyRequested(payload);
324
+ /** …the user started answering its survey. */
325
+ reportSurveyStarted(message: BubblMessage): void {
326
+ native()?.reportSurveyStarted(message.json);
304
327
  },
305
- async track(event: BubblTrackEvent): Promise<void> {
306
- return nativeModule().track(event);
328
+
329
+ /**
330
+ * Send the answers to a survey the app showed itself, checked as the server checks them: false
331
+ * (with the reason logged natively) when they can't be sent.
332
+ */
333
+ async submitSurvey(message: BubblMessage, answers: SurveyAnswers): Promise<boolean> {
334
+ return (await native()?.submitSurvey(message.json, JSON.stringify(answers))) ?? false;
307
335
  },
308
- async submitSurveyResponse(response: BubblSurveyResponse): Promise<void> {
309
- return nativeModule().submitSurveyResponse(response);
336
+
337
+ /** Open the message's call to action as Bubbl would, and report it. */
338
+ openCta(message: BubblMessage): void {
339
+ native()?.openCta(message.json);
310
340
  },
311
- async flush(): Promise<{ pendingCount: number }> {
312
- return nativeModule().flush() as Promise<{ pendingCount: number }>;
341
+
342
+ /** Whether a push's data is Bubbl's, so the app's own push handling can skip it. */
343
+ isBubblMessage(data: Record<string, unknown> | null | undefined): boolean {
344
+ return data?.bubbl === '1';
313
345
  },
346
+
347
+ /**
348
+ * Sandbox: approve this device as a test device with the code the dashboard shows, so it gets
349
+ * the Sandbox's notifications. Never throws: not approved comes with the reason as message
350
+ * (a wrong or expired code, no network, before start, an unsupported device).
351
+ */
352
+ async registerTestDevice(code: string): Promise<BubblTestDeviceResult> {
353
+ const module = native();
354
+ if (!module) {
355
+ return { approved: false, message: missingReason() };
356
+ }
357
+ const result = JSON.parse(await module.registerTestDevice(code)) as { approved: boolean; message?: string | null };
358
+ return result.message ? { approved: result.approved, message: result.message } : { approved: result.approved };
359
+ },
360
+
361
+ /**
362
+ * Where Bubbl stands on this device. Where there's no native module (see above) it still
363
+ * resolves, like every other call: not started, with the reason as lastError.
364
+ */
314
365
  async diagnostics(): Promise<BubblDiagnostics> {
315
- return nativeModule().diagnostics() as Promise<BubblDiagnostics>;
316
- },
317
- events: {
318
- addListener(listener: (event: BubblEvent) => void): BubblEventSubscription {
319
- return eventEmitter().addListener(EVENT_NAME, listener);
320
- },
366
+ const module = native();
367
+ if (!module) {
368
+ return unavailableDiagnostics();
369
+ }
370
+ const diagnostics = JSON.parse(await module.diagnostics()) as BubblDiagnostics & {
371
+ testDevice: { status: 'pending' | 'approved'; code?: string | null } | null;
372
+ };
373
+ // The code only while there is one (native sends null).
374
+ const device = diagnostics.testDevice;
375
+ diagnostics.testDevice = device && (device.code ? { status: device.status, code: device.code } : { status: device.status });
376
+ return diagnostics;
321
377
  },
322
378
  };
323
379