@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.
- package/BubblReactNativeSdk.podspec +29 -9
- package/CHANGELOG.md +47 -0
- package/LICENSE +202 -1
- package/README.md +172 -83
- package/android/build.gradle +33 -30
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/tech/bubbl/reactnative/BubblJson.kt +135 -0
- package/android/src/main/java/tech/bubbl/reactnative/BubblSdkModule.kt +277 -0
- package/android/src/main/java/tech/bubbl/reactnative/BubblSdkPackage.kt +26 -0
- package/app.plugin.js +2 -0
- package/ios/BubblPush.swift +44 -0
- package/ios/BubblSdk.h +7 -0
- package/ios/BubblSdk.mm +252 -0
- package/ios/BubblSdkImpl.swift +335 -0
- package/ios-extension/BubblReactNativeNotificationService.podspec +44 -0
- package/ios-extension/Sources/BubblCore/API/ApiResponse.swift +90 -0
- package/ios-extension/Sources/BubblCore/API/CredentialStore.swift +116 -0
- package/ios-extension/Sources/BubblCore/API/DeviceApiClient.swift +239 -0
- package/ios-extension/Sources/BubblCore/API/ErrorActions.swift +68 -0
- package/ios-extension/Sources/BubblCore/API/Http.swift +39 -0
- package/ios-extension/Sources/BubblCore/API/ServerClock.swift +69 -0
- package/ios-extension/Sources/BubblCore/BubblVersion.swift +5 -0
- package/ios-extension/Sources/BubblCore/Device/ConfigSync.swift +216 -0
- package/ios-extension/Sources/BubblCore/Device/DeviceSync.swift +160 -0
- package/ios-extension/Sources/BubblCore/Device/ProvisioningProfile.swift +24 -0
- package/ios-extension/Sources/BubblCore/Engine/EngineCore.swift +523 -0
- package/ios-extension/Sources/BubblCore/Engine/Geofencing.swift +170 -0
- package/ios-extension/Sources/BubblCore/Engine/Permissions.swift +90 -0
- package/ios-extension/Sources/BubblCore/Engine/Privacy.swift +64 -0
- package/ios-extension/Sources/BubblCore/Engine/PrivacyControls.swift +184 -0
- package/ios-extension/Sources/BubblCore/Engine/Pushes.swift +61 -0
- package/ios-extension/Sources/BubblCore/Engine/Sandbox.swift +149 -0
- package/ios-extension/Sources/BubblCore/Events/EventQueue.swift +207 -0
- package/ios-extension/Sources/BubblCore/Events/EventStore.swift +159 -0
- package/ios-extension/Sources/BubblCore/Geofence/Geofence.swift +160 -0
- package/ios-extension/Sources/BubblCore/Geofence/GeofenceEngine.swift +488 -0
- package/ios-extension/Sources/BubblCore/Geofence/GeofenceState.swift +103 -0
- package/ios-extension/Sources/BubblCore/Geofence/RegionBudget.swift +62 -0
- package/ios-extension/Sources/BubblCore/Notification/BubblNotification.swift +137 -0
- package/ios-extension/Sources/BubblCore/Notification/NotificationEvents.swift +160 -0
- package/ios-extension/Sources/BubblCore/Notification/Push.swift +134 -0
- package/ios-extension/Sources/BubblCore/Notification/SurveyForm.swift +185 -0
- package/ios-extension/Sources/BubblCore/RequestSigner.swift +56 -0
- package/ios-extension/Sources/BubblCore/Support/AsyncMutex.swift +39 -0
- package/ios-extension/Sources/BubblCore/Support/BubblLog.swift +52 -0
- package/ios-extension/Sources/BubblCore/Support/JSON.swift +31 -0
- package/ios-extension/Sources/BubblCore/Support/JSONValue.swift +94 -0
- package/ios-extension/Sources/BubblCore/Support/Locking.swift +11 -0
- package/ios-extension/Sources/BubblCore/Support/ValueStore.swift +109 -0
- package/ios-extension/Sources/BubblNotificationService/BubblNotificationService.swift +124 -0
- package/ios-sdk/BubblCore/API/ApiResponse.swift +90 -0
- package/ios-sdk/BubblCore/API/CredentialStore.swift +116 -0
- package/ios-sdk/BubblCore/API/DeviceApiClient.swift +239 -0
- package/ios-sdk/BubblCore/API/ErrorActions.swift +68 -0
- package/ios-sdk/BubblCore/API/Http.swift +39 -0
- package/ios-sdk/BubblCore/API/ServerClock.swift +69 -0
- package/ios-sdk/BubblCore/BubblVersion.swift +5 -0
- package/ios-sdk/BubblCore/Device/ConfigSync.swift +216 -0
- package/ios-sdk/BubblCore/Device/DeviceSync.swift +160 -0
- package/ios-sdk/BubblCore/Device/ProvisioningProfile.swift +24 -0
- package/ios-sdk/BubblCore/Engine/EngineCore.swift +523 -0
- package/ios-sdk/BubblCore/Engine/Geofencing.swift +170 -0
- package/ios-sdk/BubblCore/Engine/Permissions.swift +90 -0
- package/ios-sdk/BubblCore/Engine/Privacy.swift +64 -0
- package/ios-sdk/BubblCore/Engine/PrivacyControls.swift +184 -0
- package/ios-sdk/BubblCore/Engine/Pushes.swift +61 -0
- package/ios-sdk/BubblCore/Engine/Sandbox.swift +149 -0
- package/ios-sdk/BubblCore/Events/EventQueue.swift +207 -0
- package/ios-sdk/BubblCore/Events/EventStore.swift +159 -0
- package/ios-sdk/BubblCore/Geofence/Geofence.swift +160 -0
- package/ios-sdk/BubblCore/Geofence/GeofenceEngine.swift +488 -0
- package/ios-sdk/BubblCore/Geofence/GeofenceState.swift +103 -0
- package/ios-sdk/BubblCore/Geofence/RegionBudget.swift +62 -0
- package/ios-sdk/BubblCore/Notification/BubblNotification.swift +137 -0
- package/ios-sdk/BubblCore/Notification/NotificationEvents.swift +160 -0
- package/ios-sdk/BubblCore/Notification/Push.swift +134 -0
- package/ios-sdk/BubblCore/Notification/SurveyForm.swift +185 -0
- package/ios-sdk/BubblCore/RequestSigner.swift +56 -0
- package/ios-sdk/BubblCore/Support/AsyncMutex.swift +39 -0
- package/ios-sdk/BubblCore/Support/BubblLog.swift +52 -0
- package/ios-sdk/BubblCore/Support/JSON.swift +31 -0
- package/ios-sdk/BubblCore/Support/JSONValue.swift +94 -0
- package/ios-sdk/BubblCore/Support/Locking.swift +11 -0
- package/ios-sdk/BubblCore/Support/ValueStore.swift +109 -0
- package/ios-sdk/BubblLaunch/BubblLaunch.m +45 -0
- package/ios-sdk/BubblLaunch/include/BubblLaunch.h +5 -0
- package/ios-sdk/BubblSDK/Bubbl.swift +308 -0
- package/ios-sdk/BubblSDK/BubblBackend.swift +118 -0
- package/ios-sdk/BubblSDK/BubblCredential.swift +30 -0
- package/ios-sdk/BubblSDK/BubblMessage.swift +151 -0
- package/ios-sdk/BubblSDK/BubblOptions.swift +39 -0
- package/ios-sdk/BubblSDK/BubblPermissions.swift +77 -0
- package/ios-sdk/BubblSDK/BubblTestDeviceResult.swift +10 -0
- package/ios-sdk/BubblSDK/Engine/BackgroundRefresh.swift +92 -0
- package/ios-sdk/BubblSDK/Engine/EngineHost+Geofences.swift +177 -0
- package/ios-sdk/BubblSDK/Engine/EngineHost+Privacy.swift +65 -0
- package/ios-sdk/BubblSDK/Engine/EngineHost+Push.swift +154 -0
- package/ios-sdk/BubblSDK/Engine/EngineHost.swift +356 -0
- package/ios-sdk/BubblSDK/Engine/SupportedBubbl.swift +318 -0
- package/ios-sdk/BubblSDK/Engine/SystemLog.swift +27 -0
- package/ios-sdk/BubblSDK/Engine/WorkRunner.swift +107 -0
- package/ios-sdk/BubblSDK/Notification/Look.swift +78 -0
- package/ios-sdk/BubblSDK/Notification/NotificationMedia.swift +329 -0
- package/ios-sdk/BubblSDK/Notification/NotificationModel.swift +172 -0
- package/ios-sdk/BubblSDK/Notification/NotificationScreen.swift +428 -0
- package/ios-sdk/BubblSDK/Notification/ScreenPresenter.swift +148 -0
- package/ios-sdk/BubblSDK/Permissions/PermissionFlow.swift +167 -0
- package/ios-sdk/BubblSDK/Permissions/PrivacyView.swift +110 -0
- package/ios-sdk/BubblSDK/Platform/AppleDevicePlatform.swift +95 -0
- package/ios-sdk/BubblSDK/Platform/KeychainCredentialStore.swift +114 -0
- package/ios-sdk/BubblSDK/Platform/LocationService.swift +418 -0
- package/ios-sdk/BubblSDK/Platform/PushIntegration.swift +288 -0
- package/ios-sdk/BubblSDK/Platform/URLSessionHttpClient.swift +69 -0
- package/ios-sdk/BubblSDK/PrivacyInfo.xcprivacy +76 -0
- package/jest/index.js +75 -0
- package/lib/commonjs/BubblModule.js +14 -0
- package/lib/commonjs/BubblModule.js.map +1 -0
- package/lib/commonjs/BubblModule.web.js +13 -0
- package/lib/commonjs/BubblModule.web.js.map +1 -0
- package/lib/commonjs/NativeBubblSdk.js +20 -0
- package/lib/commonjs/NativeBubblSdk.js.map +1 -0
- package/lib/commonjs/index.js +336 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/BubblModule.js +5 -0
- package/lib/module/BubblModule.js.map +1 -0
- package/lib/module/BubblModule.web.js +9 -0
- package/lib/module/BubblModule.web.js.map +1 -0
- package/lib/module/NativeBubblSdk.js +18 -0
- package/lib/module/NativeBubblSdk.js.map +1 -0
- package/lib/module/index.js +331 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/BubblModule.d.ts +3 -0
- package/lib/typescript/commonjs/src/BubblModule.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/BubblModule.web.d.ts +8 -0
- package/lib/typescript/commonjs/src/BubblModule.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/NativeBubblSdk.d.ts +59 -0
- package/lib/typescript/commonjs/src/NativeBubblSdk.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +111 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/types.d.ts +192 -0
- package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/BubblModule.d.ts +3 -0
- package/lib/typescript/module/src/BubblModule.d.ts.map +1 -0
- package/lib/typescript/module/src/BubblModule.web.d.ts +8 -0
- package/lib/typescript/module/src/BubblModule.web.d.ts.map +1 -0
- package/lib/typescript/module/src/NativeBubblSdk.d.ts +59 -0
- package/lib/typescript/module/src/NativeBubblSdk.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +111 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/lib/typescript/module/src/types.d.ts +192 -0
- package/lib/typescript/module/src/types.d.ts.map +1 -0
- package/package.json +105 -24
- package/plugin/build/imageNotifications.d.ts +35 -0
- package/plugin/build/imageNotifications.js +198 -0
- package/plugin/build/index.d.ts +84 -0
- package/plugin/build/index.js +176 -0
- package/src/BubblModule.ts +2 -0
- package/src/BubblModule.web.ts +9 -0
- package/src/NativeBubblSdk.ts +67 -0
- package/src/index.ts +338 -282
- package/src/types.ts +182 -0
- package/android/src/main/kotlin/tech/bubbl/reactnative/BubblSdkModule.kt +0 -680
- package/android/src/main/kotlin/tech/bubbl/reactnative/BubblSdkNotificationIntents.kt +0 -32
- package/android/src/main/kotlin/tech/bubbl/reactnative/BubblSdkPackage.kt +0 -14
- package/ios/BubblSdk.swift +0 -802
- package/ios/BubblSdkBridge.m +0 -86
- package/react-native.config.js +0 -11
- package/src/specs/NativeBubblSdk.ts +0 -54
package/src/index.ts
CHANGED
|
@@ -1,323 +1,379 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
|
217
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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
|
-
|
|
255
|
-
|
|
212
|
+
|
|
213
|
+
/** For requireConsent: true starts everything, false is optOut. */
|
|
214
|
+
setConsent(granted: boolean): void {
|
|
215
|
+
native()?.setConsent(granted);
|
|
256
216
|
},
|
|
257
|
-
|
|
258
|
-
|
|
217
|
+
|
|
218
|
+
/** Stop for this user; the server is told. */
|
|
219
|
+
optOut(): void {
|
|
220
|
+
native()?.optOut();
|
|
259
221
|
},
|
|
260
|
-
|
|
261
|
-
|
|
222
|
+
|
|
223
|
+
/** Erase the device on the server and here; Bubbl stays off afterwards. */
|
|
224
|
+
deleteMyData(): void {
|
|
225
|
+
native()?.deleteMyData();
|
|
262
226
|
},
|
|
263
|
-
|
|
264
|
-
|
|
227
|
+
|
|
228
|
+
/** Location (geofences) off or on; the rest carries on. */
|
|
229
|
+
setLocationEnabled(enabled: boolean): void {
|
|
230
|
+
native()?.setLocationEnabled(enabled);
|
|
265
231
|
},
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
270
|
-
|
|
254
|
+
|
|
255
|
+
/** The device's segments (replaces any set before). */
|
|
256
|
+
setSegments(segments: string[]): void {
|
|
257
|
+
native()?.setSegments(segments);
|
|
271
258
|
},
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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
|
-
|
|
279
|
-
|
|
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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
285
|
-
|
|
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
|
-
|
|
288
|
-
|
|
304
|
+
/** …the user opened it. */
|
|
305
|
+
reportOpened(message: BubblMessage): void {
|
|
306
|
+
native()?.reportOpened(message.json);
|
|
289
307
|
},
|
|
290
|
-
|
|
291
|
-
|
|
308
|
+
/** …the user tapped its call to action. */
|
|
309
|
+
reportCtaClicked(message: BubblMessage): void {
|
|
310
|
+
native()?.reportCtaClicked(message.json);
|
|
292
311
|
},
|
|
293
|
-
|
|
294
|
-
|
|
312
|
+
/** …the user closed it without acting. */
|
|
313
|
+
reportDismissed(message: BubblMessage): void {
|
|
314
|
+
native()?.reportDismissed(message.json);
|
|
295
315
|
},
|
|
296
|
-
|
|
297
|
-
|
|
316
|
+
/** …its media was shown or started playing. */
|
|
317
|
+
reportMediaViewed(message: BubblMessage): void {
|
|
318
|
+
native()?.reportMediaViewed(message.json);
|
|
298
319
|
},
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
303
|
-
|
|
324
|
+
/** …the user started answering its survey. */
|
|
325
|
+
reportSurveyStarted(message: BubblMessage): void {
|
|
326
|
+
native()?.reportSurveyStarted(message.json);
|
|
304
327
|
},
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
309
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|