@bubblsdk/react-native-sdk 4.1.7 → 5.0.0-alpha.2
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 +52 -1
- 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 +79 -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 +320 -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 +169 -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
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
#if canImport(UserNotifications)
|
|
3
|
+
import UserNotifications
|
|
4
|
+
#endif
|
|
5
|
+
#if !COCOAPODS
|
|
6
|
+
import BubblCore
|
|
7
|
+
#endif
|
|
8
|
+
|
|
9
|
+
/// Bubbl, for an iOS app: the same functions as Android's `tech.bubbl.sdk.Bubbl`
|
|
10
|
+
/// (docs/PUBLIC_API.md). Two steps to install: add the package, and in
|
|
11
|
+
/// `application(_:didFinishLaunchingWithOptions:)` (or the SwiftUI `App`'s init)
|
|
12
|
+
///
|
|
13
|
+
/// Bubbl.start(apiKey: "pk_live_…", options: BubblOptions(baseUrl: "https://…"))
|
|
14
|
+
///
|
|
15
|
+
/// Every call is safe from any thread. Calls before `start` do nothing (and log a warning), except
|
|
16
|
+
/// `permissions.openSettings()`, which needs nothing of Bubbl's. Bubbl never stops the app: a mistake (an empty key, an http:// URL) is logged, not thrown.
|
|
17
|
+
///
|
|
18
|
+
/// Bubbl works on iOS 17 and later. An app supporting older versions can still include it: on
|
|
19
|
+
/// those phones `isSupported` is false and every call does nothing (see `isSupported`).
|
|
20
|
+
public enum Bubbl {
|
|
21
|
+
/// This SDK's version.
|
|
22
|
+
public static let sdkVersion = BubblVersion.sdk
|
|
23
|
+
|
|
24
|
+
/// Whether Bubbl works on this device (iOS 17 and later). When false, `start` logs that it
|
|
25
|
+
/// does nothing here and returns: nothing is installed or sent, nothing prompts, no
|
|
26
|
+
/// notification delegate or background task is set up, and no location is used. Every other
|
|
27
|
+
/// call does nothing too, permission calls report nil, notification listeners are never
|
|
28
|
+
/// called, and `diagnostics()` says `supported: false`.
|
|
29
|
+
public static var isSupported: Bool { backend.isSupported }
|
|
30
|
+
|
|
31
|
+
// MARK: - Starting
|
|
32
|
+
|
|
33
|
+
/// Start Bubbl. Call once per launch, early (so background launches for a geofence or a push
|
|
34
|
+
/// have it too); calling again with the same key and options changes nothing.
|
|
35
|
+
public static func start(apiKey: String, options: BubblOptions) {
|
|
36
|
+
backend.start(apiKey: apiKey, options: options)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Start Bubbl with a device credential issued outside the app (installs provisioned ahead of
|
|
40
|
+
/// time, an app's own pairing flow) instead of an API key: the device never registers itself.
|
|
41
|
+
/// As `start(apiKey:options:)` otherwise, and called the same way at every launch. A different
|
|
42
|
+
/// credential than last time (or switching from an API key) starts afresh as a new device:
|
|
43
|
+
/// nothing kept from before is sent under the new one. If the server refuses the credential,
|
|
44
|
+
/// Bubbl stops and says so (`BubblEvent.credentialRejected`, diagnostics) until it's started
|
|
45
|
+
/// with a new one.
|
|
46
|
+
public static func start(credential: BubblCredential, options: BubblOptions) {
|
|
47
|
+
backend.start(credential: credential, options: options)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Stop Bubbl on this device until `start` is called again: nothing more runs; nothing is
|
|
51
|
+
/// dropped, and the server isn't told (unlike `optOut`).
|
|
52
|
+
public static func stop() {
|
|
53
|
+
backend.stop()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// MARK: - Consent and privacy
|
|
57
|
+
|
|
58
|
+
/// The user's answer, for apps started with `requireConsent` (and to give consent again after
|
|
59
|
+
/// an opt-out): true starts everything; false is the same as `optOut()`.
|
|
60
|
+
public static func setConsent(_ granted: Bool) {
|
|
61
|
+
backend.setConsent(granted)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Stop Bubbl for this user: nothing more is sent, shown or tracked, what's queued is dropped,
|
|
65
|
+
/// and the server is told.
|
|
66
|
+
public static func optOut() {
|
|
67
|
+
backend.optOut()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Erase this device and everything Bubbl recorded about it, on the server and here. It keeps
|
|
71
|
+
/// trying until it's done, and Bubbl stays off afterwards (`setConsent(true)` later starts
|
|
72
|
+
/// afresh, as a new device).
|
|
73
|
+
public static func deleteMyData() {
|
|
74
|
+
backend.deleteMyData()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Turn Bubbl's use of location (geofences) off, or on again; the rest carries on.
|
|
78
|
+
public static func setLocationEnabled(_ enabled: Bool) {
|
|
79
|
+
backend.setLocationEnabled(enabled)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// MARK: - Sandbox
|
|
83
|
+
|
|
84
|
+
/// Approve this device as a Sandbox test device with a registration code from the dashboard
|
|
85
|
+
/// (Test devices › Registration codes; each approves one device, within 24 hours), e.g. from a
|
|
86
|
+
/// hidden debug menu or a device farm's set-up. The other ways need no code in the app: the
|
|
87
|
+
/// dashboard's "Add a test device", or matching the code Bubbl logs at start in Sandbox.
|
|
88
|
+
/// `approved` once it is; otherwise `message` says why, to show whoever typed the code (a
|
|
89
|
+
/// wrong, used or expired code, the Sandbox's 25 test devices taken, before `start`, no
|
|
90
|
+
/// network). Also said in the log.
|
|
91
|
+
public static func registerTestDevice(_ code: String) async -> BubblTestDeviceResult {
|
|
92
|
+
await backend.registerTestDevice(code)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// `registerTestDevice(_:)` for code that can't await; `completion` runs on the main thread.
|
|
96
|
+
public static func registerTestDevice(_ code: String, completion: @escaping @Sendable @MainActor (BubblTestDeviceResult) -> Void) {
|
|
97
|
+
Task {
|
|
98
|
+
let result = await registerTestDevice(code)
|
|
99
|
+
await MainActor.run { completion(result) }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// MARK: - Permissions
|
|
104
|
+
|
|
105
|
+
/// The permissions Bubbl uses: where they stand, and asking for them.
|
|
106
|
+
public static let permissions = BubblPermissions()
|
|
107
|
+
|
|
108
|
+
// MARK: - Targeting and events
|
|
109
|
+
|
|
110
|
+
/// The device's segments, for targeting campaigns (replaces any set before).
|
|
111
|
+
public static func setSegments(_ segments: [String]) {
|
|
112
|
+
backend.setSegments(segments)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Record an event of the app's own, for reports: a `name` of letters, digits and . _ : - (at
|
|
116
|
+
/// most 100), and up to 50 flat `properties` (text, numbers, true/false or nil).
|
|
117
|
+
public static func track(_ name: String, properties: [String: Any?] = [:]) {
|
|
118
|
+
backend.track(name, properties: properties)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// MARK: - Notifications
|
|
122
|
+
|
|
123
|
+
/// Have a say over each notification before Bubbl shows it: return true to show it yourself
|
|
124
|
+
/// (then report what happens with `reportDisplayed` and the others), false to let Bubbl. Nil
|
|
125
|
+
/// goes back to Bubbl showing everything. Called on the main thread.
|
|
126
|
+
public static func setNotificationListener(_ listener: (@MainActor @Sendable (BubblMessage) -> Bool)?) {
|
|
127
|
+
backend.setNotificationListener(listener)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Be told of what Bubbl does as it happens (`BubblEvent`), on the main thread. Keep the token
|
|
131
|
+
/// to remove the listener with.
|
|
132
|
+
@discardableResult
|
|
133
|
+
public static func addEventListener(_ listener: @escaping @MainActor @Sendable (BubblEvent) -> Void) -> BubblEventToken {
|
|
134
|
+
backend.addEventListener(listener)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public static func removeEventListener(_ token: BubblEventToken) {
|
|
138
|
+
backend.removeEventListener(token)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Show `message` in Bubbl's notification screen (one the listener held back, say).
|
|
142
|
+
public static func present(_ message: BubblMessage) {
|
|
143
|
+
backend.present(message)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// For an app drawing notifications itself: it's on screen.
|
|
147
|
+
public static func reportDisplayed(_ message: BubblMessage) {
|
|
148
|
+
backend.report("reportDisplayed") { try await $0.displayed(message.notification) }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// …the user opened it (tapped it, rather than it opening on its own).
|
|
152
|
+
public static func reportOpened(_ message: BubblMessage) {
|
|
153
|
+
backend.report("reportOpened") { try await $0.opened(message.notification) }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// …the user tapped its call to action.
|
|
157
|
+
public static func reportCtaClicked(_ message: BubblMessage) {
|
|
158
|
+
backend.report("reportCtaClicked") { try await $0.ctaClicked(message.notification) }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// …the user closed it without acting.
|
|
162
|
+
public static func reportDismissed(_ message: BubblMessage) {
|
|
163
|
+
backend.report("reportDismissed") { try await $0.dismissed(message.notification) }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// …its media (video, audio, a YouTube video, an image) was shown or started playing.
|
|
167
|
+
public static func reportMediaViewed(_ message: BubblMessage) {
|
|
168
|
+
backend.report("reportMediaViewed") { try await $0.mediaViewed(message.notification, positionSeconds: 0) }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// …its video or audio played to the end, `durationSeconds` long.
|
|
172
|
+
public static func reportMediaCompleted(_ message: BubblMessage, durationSeconds: Double) {
|
|
173
|
+
backend.report("reportMediaCompleted") { try await $0.mediaCompleted(message.notification, positionSeconds: durationSeconds) }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// …the user started answering its survey.
|
|
177
|
+
public static func reportSurveyStarted(_ message: BubblMessage) {
|
|
178
|
+
backend.report("reportSurveyStarted") { try await $0.surveyStarted(message.notification) }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// Send the answers to a survey the app showed itself: `answers` by question id, as each
|
|
182
|
+
/// `BubblQuestion` says. Checked as the server checks them; false (and a warning logged, with
|
|
183
|
+
/// why) when they can't be sent, e.g. a required question unanswered or a rating of 6.
|
|
184
|
+
@discardableResult
|
|
185
|
+
public static func submitSurvey(_ message: BubblMessage, answers: [String: Any?]) -> Bool {
|
|
186
|
+
backend.submitSurvey(message, answers: answers)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// Open `message`'s call to action as Bubbl's screen would, and report the click.
|
|
190
|
+
public static func openCta(_ message: BubblMessage) {
|
|
191
|
+
backend.openCta(message)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// MARK: - Push
|
|
195
|
+
|
|
196
|
+
/// Whether a push is Bubbl's (its payload's `userInfo`), so the app's own handling can leave it
|
|
197
|
+
/// alone.
|
|
198
|
+
public static func isBubblMessage(_ userInfo: [AnyHashable: Any]) -> Bool {
|
|
199
|
+
backend.isBubblMessage(userInfo)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#if os(iOS)
|
|
203
|
+
// Bubbl takes the device token and its own pushes by itself. With BubblAutoIntegrationEnabled
|
|
204
|
+
// set to NO in Info.plist it doesn't, and the app hands them over with these three.
|
|
205
|
+
|
|
206
|
+
/// The device token, from the app delegate's
|
|
207
|
+
/// `application(_:didRegisterForRemoteNotificationsWithDeviceToken:)`. Before `start` too.
|
|
208
|
+
public static func setPushToken(_ deviceToken: Data) {
|
|
209
|
+
backend.setPushToken(deviceToken)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/// From the notification center delegate's `userNotificationCenter(_:willPresent:…)`: true
|
|
213
|
+
/// when the push is Bubbl's, and Bubbl calls `completionHandler`; false when it isn't, and the
|
|
214
|
+
/// app's own code decides.
|
|
215
|
+
public static func willPresent(_ notification: UNNotification, completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) -> Bool {
|
|
216
|
+
backend.willPresent(notification, completionHandler: completionHandler)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// From the notification center delegate's `userNotificationCenter(_:didReceive:…)`: true when
|
|
220
|
+
/// the tapped notification is Bubbl's, and Bubbl calls `completionHandler`; false when it
|
|
221
|
+
/// isn't.
|
|
222
|
+
public static func didReceive(_ response: UNNotificationResponse, completionHandler: @escaping () -> Void) -> Bool {
|
|
223
|
+
backend.didReceive(response, completionHandler: completionHandler)
|
|
224
|
+
}
|
|
225
|
+
#endif
|
|
226
|
+
|
|
227
|
+
// MARK: - Diagnostics
|
|
228
|
+
|
|
229
|
+
/// Where Bubbl stands on this device, for support and for an app's own debug screen. The same
|
|
230
|
+
/// fields as Android's, and iOS's own at the end.
|
|
231
|
+
public struct Diagnostics: Sendable, Equatable {
|
|
232
|
+
public let started: Bool
|
|
233
|
+
/// Bubbl works on this device (iOS 17 and later): `Bubbl.isSupported`.
|
|
234
|
+
public let supported: Bool
|
|
235
|
+
public let sdkVersion: String
|
|
236
|
+
/// False when the workspace requires a newer SDK.
|
|
237
|
+
public let sdkSupported: Bool
|
|
238
|
+
public let registered: Bool
|
|
239
|
+
/// The install id the engine keeps (how Dashboard › Active users finds this device); nil
|
|
240
|
+
/// before there is one.
|
|
241
|
+
public let installId: String?
|
|
242
|
+
/// Running: started, consent allows it, not paused by the server, supported.
|
|
243
|
+
public let active: Bool
|
|
244
|
+
/// Unix seconds until which the server paused Bubbl, if it has.
|
|
245
|
+
public let pausedUntil: Int64?
|
|
246
|
+
public let consentRequired: Bool
|
|
247
|
+
public let consent: Bool?
|
|
248
|
+
public let locationEnabled: Bool
|
|
249
|
+
/// Where the device stands on the permissions Bubbl uses (as `permissions.status()`).
|
|
250
|
+
public let permissions: BubblPermissionStatus?
|
|
251
|
+
public let hasPushToken: Bool
|
|
252
|
+
public let geofences: Int
|
|
253
|
+
public let queuedEvents: Int
|
|
254
|
+
public let lastError: String?
|
|
255
|
+
/// Started with a credential the server refused (`Bubbl.start(credential:options:)`).
|
|
256
|
+
public let credentialRejected: Bool
|
|
257
|
+
/// Geofences work with the app closed ("Always"); else only while it's open.
|
|
258
|
+
public let backgroundLocation: Bool
|
|
259
|
+
/// Geofence entries and exits that happened before the first unlock after a reboot, when
|
|
260
|
+
/// they couldn't be kept (always 0 on Android).
|
|
261
|
+
public let transitionsDroppedWhileLocked: Int
|
|
262
|
+
/// iOS: Background App Refresh is on for the app.
|
|
263
|
+
public let backgroundRefreshAvailable: Bool
|
|
264
|
+
/// iOS: Low Power Mode is on (background work is held back).
|
|
265
|
+
public let lowPowerMode: Bool
|
|
266
|
+
/// iOS watches the geofences ("Always", precise, and the device can). Otherwise they're
|
|
267
|
+
/// checked from location changes, which is coarser.
|
|
268
|
+
public let osWatchesGeofences: Bool
|
|
269
|
+
/// The workspace the key belongs to: "sandbox" (pk_test_) or "production" (pk_live_); nil
|
|
270
|
+
/// before the device has registered (or from a server that predates Sandbox).
|
|
271
|
+
public let environment: String?
|
|
272
|
+
/// In Sandbox, where this device stands as a test device; nil in Production.
|
|
273
|
+
public let testDevice: TestDevice?
|
|
274
|
+
|
|
275
|
+
/// A Sandbox test device: "pending" until someone approves it (in the dashboard, with
|
|
276
|
+
/// `code`, or with `Bubbl.registerTestDevice`), then "approved". Unused for 30 days, or
|
|
277
|
+
/// removed in the dashboard, it's pending again.
|
|
278
|
+
public struct TestDevice: Sendable, Equatable {
|
|
279
|
+
public let status: String
|
|
280
|
+
/// While pending: the short code the dashboard lists this device under (e.g. K7Q-2MX).
|
|
281
|
+
public let code: String?
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
public static func diagnostics() async -> Diagnostics {
|
|
286
|
+
await backend.diagnostics()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// `diagnostics()` for code that can't await; `completion` runs on the main thread.
|
|
290
|
+
public static func diagnostics(completion: @escaping @Sendable @MainActor (Diagnostics) -> Void) {
|
|
291
|
+
Task {
|
|
292
|
+
let result = await diagnostics()
|
|
293
|
+
await MainActor.run { completion(result) }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// MARK: - Internals
|
|
298
|
+
|
|
299
|
+
/// https only; http for local development (localhost, the Simulator's host, *.test).
|
|
300
|
+
static func isAllowedBaseUrl(_ url: String) -> Bool {
|
|
301
|
+
guard let components = URLComponents(string: url), let host = components.host?.lowercased(), !host.isEmpty else { return false }
|
|
302
|
+
switch components.scheme?.lowercased() {
|
|
303
|
+
case "https": return true
|
|
304
|
+
case "http": return host == "localhost" || host == "127.0.0.1" || host == "10.0.2.2" || host.hasSuffix(".test")
|
|
305
|
+
default: return false
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import os
|
|
3
|
+
#if canImport(UserNotifications)
|
|
4
|
+
import UserNotifications
|
|
5
|
+
#endif
|
|
6
|
+
#if !COCOAPODS
|
|
7
|
+
import BubblCore
|
|
8
|
+
#endif
|
|
9
|
+
|
|
10
|
+
/// What `Bubbl`'s calls do: the engine on iOS 17 and later (`SupportedBubbl`), nothing below it
|
|
11
|
+
/// (`UnsupportedBubbl`). Apps supporting older iOS versions can install Bubbl; it just does nothing
|
|
12
|
+
/// on those phones. The engine and everything it uses are `@available(iOS 17, *)`, so the compiler
|
|
13
|
+
/// proves that the one `#available` check in `Bubbl.backend` is the only way in.
|
|
14
|
+
protocol BubblBackend: Sendable {
|
|
15
|
+
var isSupported: Bool { get }
|
|
16
|
+
|
|
17
|
+
func start(apiKey: String, options: BubblOptions)
|
|
18
|
+
func start(credential: BubblCredential, options: BubblOptions)
|
|
19
|
+
func stop()
|
|
20
|
+
|
|
21
|
+
func setConsent(_ granted: Bool)
|
|
22
|
+
func optOut()
|
|
23
|
+
func deleteMyData()
|
|
24
|
+
func setLocationEnabled(_ enabled: Bool)
|
|
25
|
+
func registerTestDevice(_ code: String) async -> BubblTestDeviceResult
|
|
26
|
+
|
|
27
|
+
func setSegments(_ segments: [String])
|
|
28
|
+
func track(_ name: String, properties: [String: Any?])
|
|
29
|
+
|
|
30
|
+
func setNotificationListener(_ listener: (@MainActor @Sendable (BubblMessage) -> Bool)?)
|
|
31
|
+
func addEventListener(_ listener: @escaping @MainActor @Sendable (BubblEvent) -> Void) -> BubblEventToken
|
|
32
|
+
func removeEventListener(_ token: BubblEventToken)
|
|
33
|
+
func present(_ message: BubblMessage)
|
|
34
|
+
func report(_ call: String, _ record: @escaping @Sendable (NotificationEvents) async throws -> Void)
|
|
35
|
+
func submitSurvey(_ message: BubblMessage, answers: [String: Any?]) -> Bool
|
|
36
|
+
func openCta(_ message: BubblMessage)
|
|
37
|
+
|
|
38
|
+
func isBubblMessage(_ userInfo: [AnyHashable: Any]) -> Bool
|
|
39
|
+
#if os(iOS)
|
|
40
|
+
func setPushToken(_ deviceToken: Data)
|
|
41
|
+
func willPresent(_ notification: UNNotification, completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) -> Bool
|
|
42
|
+
func didReceive(_ response: UNNotificationResponse, completionHandler: @escaping () -> Void) -> Bool
|
|
43
|
+
#endif
|
|
44
|
+
|
|
45
|
+
func permissionStatus() -> BubblPermissionStatus?
|
|
46
|
+
func requestPermission(_ request: PermissionRequest, _ call: String) async -> BubblPermissionStatus?
|
|
47
|
+
func openSettings()
|
|
48
|
+
|
|
49
|
+
func diagnostics() async -> Bubbl.Diagnostics
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
extension Bubbl {
|
|
53
|
+
/// The one way into the engine.
|
|
54
|
+
static let backend: any BubblBackend = {
|
|
55
|
+
if #available(iOS 17, *) { return SupportedBubbl() }
|
|
56
|
+
return UnsupportedBubbl()
|
|
57
|
+
}()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Below iOS 17: every call returns at once and does nothing. No install, no network, no prompts,
|
|
61
|
+
/// no notification delegate, no background tasks, no location. `start` says so in the log, once
|
|
62
|
+
/// per call; permission calls report nil (as before `start`) and never prompt; pushes and taps are
|
|
63
|
+
/// left to the app.
|
|
64
|
+
struct UnsupportedBubbl: BubblBackend {
|
|
65
|
+
var isSupported: Bool { false }
|
|
66
|
+
|
|
67
|
+
func start(apiKey: String, options: BubblOptions) { said(options) }
|
|
68
|
+
func start(credential: BubblCredential, options: BubblOptions) { said(options) }
|
|
69
|
+
func stop() {}
|
|
70
|
+
|
|
71
|
+
func setConsent(_ granted: Bool) {}
|
|
72
|
+
func optOut() {}
|
|
73
|
+
func deleteMyData() {}
|
|
74
|
+
func setLocationEnabled(_ enabled: Bool) {}
|
|
75
|
+
func registerTestDevice(_ code: String) async -> BubblTestDeviceResult {
|
|
76
|
+
BubblTestDeviceResult(approved: false, message: "Bubbl needs iOS 17 or later")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func setSegments(_ segments: [String]) {}
|
|
80
|
+
func track(_ name: String, properties: [String: Any?]) {}
|
|
81
|
+
|
|
82
|
+
func setNotificationListener(_ listener: (@MainActor @Sendable (BubblMessage) -> Bool)?) {}
|
|
83
|
+
func addEventListener(_ listener: @escaping @MainActor @Sendable (BubblEvent) -> Void) -> BubblEventToken {
|
|
84
|
+
BubblEventToken(id: UUID())
|
|
85
|
+
}
|
|
86
|
+
func removeEventListener(_ token: BubblEventToken) {}
|
|
87
|
+
func present(_ message: BubblMessage) {}
|
|
88
|
+
func report(_ call: String, _ record: @escaping @Sendable (NotificationEvents) async throws -> Void) {}
|
|
89
|
+
func submitSurvey(_ message: BubblMessage, answers: [String: Any?]) -> Bool { false }
|
|
90
|
+
func openCta(_ message: BubblMessage) {}
|
|
91
|
+
|
|
92
|
+
func isBubblMessage(_ userInfo: [AnyHashable: Any]) -> Bool { false }
|
|
93
|
+
#if os(iOS)
|
|
94
|
+
func setPushToken(_ deviceToken: Data) {}
|
|
95
|
+
func willPresent(_ notification: UNNotification, completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) -> Bool { false }
|
|
96
|
+
func didReceive(_ response: UNNotificationResponse, completionHandler: @escaping () -> Void) -> Bool { false }
|
|
97
|
+
#endif
|
|
98
|
+
|
|
99
|
+
func permissionStatus() -> BubblPermissionStatus? { nil }
|
|
100
|
+
func requestPermission(_ request: PermissionRequest, _ call: String) async -> BubblPermissionStatus? { nil }
|
|
101
|
+
func openSettings() {}
|
|
102
|
+
|
|
103
|
+
func diagnostics() async -> Bubbl.Diagnostics {
|
|
104
|
+
Bubbl.Diagnostics(
|
|
105
|
+
started: false, supported: false, sdkVersion: Bubbl.sdkVersion, sdkSupported: true, registered: false, installId: nil,
|
|
106
|
+
active: false, pausedUntil: nil, consentRequired: false, consent: nil, locationEnabled: true, permissions: nil,
|
|
107
|
+
hasPushToken: false, geofences: 0, queuedEvents: 0, lastError: nil, credentialRejected: false, backgroundLocation: false,
|
|
108
|
+
transitionsDroppedWhileLocked: 0, backgroundRefreshAvailable: false, lowPowerMode: ProcessInfo.processInfo.isLowPowerModeEnabled,
|
|
109
|
+
osWatchesGeofences: false, environment: nil, testDevice: nil
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// The one line `start` writes: os_log, since the SDK's own log (os.Logger) is iOS 14 and later.
|
|
114
|
+
private func said(_ options: BubblOptions) {
|
|
115
|
+
guard options.logLevel != .none else { return }
|
|
116
|
+
os_log("Bubbl %{public}@ needs iOS 17 or later: it does nothing on this device", log: OSLog(subsystem: "tech.bubbl.sdk", category: "Bubbl"), type: .info, Bubbl.sdkVersion)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
#if !COCOAPODS
|
|
3
|
+
import BubblCore
|
|
4
|
+
#endif
|
|
5
|
+
|
|
6
|
+
/// A device credential issued outside the app (installs provisioned ahead of time, an app's own
|
|
7
|
+
/// pairing flow), for `Bubbl.start(credential:options:)` instead of an API key. The same as
|
|
8
|
+
/// Android's BubblCredential. Its secret is kept in the Keychain and never shown: not in
|
|
9
|
+
/// `description`, not by `dump` or the debugger.
|
|
10
|
+
public struct BubblCredential: Sendable, Equatable, CustomStringConvertible, CustomDebugStringConvertible, CustomReflectable {
|
|
11
|
+
public let keyId: String
|
|
12
|
+
public let secret: String
|
|
13
|
+
/// The install the credential was issued for (a pairing flow passes the install_id it paired
|
|
14
|
+
/// with).
|
|
15
|
+
public let installId: String
|
|
16
|
+
|
|
17
|
+
public init(keyId: String, secret: String, installId: String) {
|
|
18
|
+
self.keyId = keyId
|
|
19
|
+
self.secret = secret
|
|
20
|
+
self.installId = installId
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public var description: String { "BubblCredential(keyId: \(keyId), installId: \(installId), secret: hidden)" }
|
|
24
|
+
public var debugDescription: String { description }
|
|
25
|
+
public var customMirror: Mirror {
|
|
26
|
+
Mirror(self, children: ["keyId": keyId, "installId": installId, "secret": "hidden"], displayStyle: .struct)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var issued: IssuedCredential { IssuedCredential(keyId: keyId, secret: secret, installId: installId) }
|
|
30
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
#if !COCOAPODS
|
|
3
|
+
import BubblCore
|
|
4
|
+
#endif
|
|
5
|
+
|
|
6
|
+
/// A notification from Bubbl, as an app's own handler sees it (`Bubbl.setNotificationListener`):
|
|
7
|
+
/// enough to draw it itself, or to hand back to `Bubbl.present` to show Bubbl's screen. The same
|
|
8
|
+
/// as Android's BubblMessage.
|
|
9
|
+
public struct BubblMessage: Sendable, Hashable, CustomStringConvertible {
|
|
10
|
+
let notification: BubblNotification
|
|
11
|
+
|
|
12
|
+
init(_ notification: BubblNotification) {
|
|
13
|
+
self.notification = notification
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// A message back from its `json` (kept for an in-app inbox, say, then shown again with
|
|
17
|
+
/// `Bubbl.present`); nil when it isn't one.
|
|
18
|
+
public init?(json: String) {
|
|
19
|
+
guard let notification = BubblNotification(json: json) else { return nil }
|
|
20
|
+
self.init(notification)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Echo this on anything the app reports about it.
|
|
24
|
+
public var id: String { notification.campaignNotificationId }
|
|
25
|
+
public var headline: String { notification.headline }
|
|
26
|
+
public var body: String { notification.body }
|
|
27
|
+
public var isSurvey: Bool { notification.isSurvey }
|
|
28
|
+
/// From a Sandbox workspace: an app drawing it itself should mark it as such, as Bubbl's
|
|
29
|
+
/// screen does with its SANDBOX ribbon.
|
|
30
|
+
public var isSandbox: Bool { notification.sandbox }
|
|
31
|
+
|
|
32
|
+
/// image, video, audio, youtube, application, text or file; nil without media.
|
|
33
|
+
public var mediaType: String? { notification.media?.kind.rawValue }
|
|
34
|
+
public var mediaUrl: String? { notification.media?.url }
|
|
35
|
+
public var mediaThumbnailUrl: String? { notification.media?.pictureUrl }
|
|
36
|
+
|
|
37
|
+
public var ctaLabel: String? { notification.cta?.label }
|
|
38
|
+
public var ctaUrl: String? { notification.cta?.url }
|
|
39
|
+
|
|
40
|
+
/// A survey's questions in order (empty for a message), for an app drawing it itself.
|
|
41
|
+
public var questions: [BubblQuestion] { notification.questions.map(BubblQuestion.init) }
|
|
42
|
+
|
|
43
|
+
/// The whole notification as the device API sent it (JSON), e.g. to pass to a wrapper.
|
|
44
|
+
public var json: String { notification.jsonText }
|
|
45
|
+
|
|
46
|
+
public var description: String { "BubblMessage(\(id), \"\(headline)\")" }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// One question of a survey, and what `Bubbl.submitSurvey` takes as its answer, by `type`:
|
|
50
|
+
/// single choice a choice id; multiple choice a list of choice ids; rating a whole number 1–5;
|
|
51
|
+
/// boolean true/false; number a number; slider a number from 0 to 10; open-ended text (≤ 2000).
|
|
52
|
+
public struct BubblQuestion: Sendable, Hashable {
|
|
53
|
+
/// Android's BubblQuestion.Type (`Type` is taken in Swift).
|
|
54
|
+
public enum Kind: String, Sendable, CaseIterable {
|
|
55
|
+
case openEnded = "open_ended"
|
|
56
|
+
case singleChoice = "single_choice"
|
|
57
|
+
case multipleChoice = "multiple_choice"
|
|
58
|
+
case rating, boolean, number, slider
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public struct Choice: Sendable, Hashable {
|
|
62
|
+
public let id: String
|
|
63
|
+
public let text: String
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public let id: String
|
|
67
|
+
public let text: String
|
|
68
|
+
public let type: Kind
|
|
69
|
+
public let required: Bool
|
|
70
|
+
public let choices: [Choice]
|
|
71
|
+
|
|
72
|
+
init(_ question: BubblNotification.Question) {
|
|
73
|
+
id = question.id
|
|
74
|
+
text = question.text
|
|
75
|
+
type = Kind(rawValue: question.kind.rawValue) ?? .openEnded
|
|
76
|
+
required = question.required
|
|
77
|
+
choices = question.choices.map { Choice(id: $0.id, text: $0.text) }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// What Bubbl does, as it happens, for an app that wants to know (`Bubbl.addEventListener`): each
|
|
82
|
+
/// step of a notification's life, geofences entered and left, and errors. Delivered on the main
|
|
83
|
+
/// thread. They're for the app's own use (its analytics, its UI); Bubbl records its own.
|
|
84
|
+
///
|
|
85
|
+
/// New cases arrive in minor versions, so switch over it with a `default:` branch.
|
|
86
|
+
public enum BubblEvent: Sendable, Hashable {
|
|
87
|
+
/// A notification arrived (from a geofence, a push or a pull), before it's shown.
|
|
88
|
+
case notificationReceived(BubblMessage)
|
|
89
|
+
case notificationDisplayed(BubblMessage)
|
|
90
|
+
/// Opened from its system notification.
|
|
91
|
+
case notificationOpened(BubblMessage)
|
|
92
|
+
case notificationCtaClicked(BubblMessage)
|
|
93
|
+
case notificationDismissed(BubblMessage)
|
|
94
|
+
case mediaViewed(BubblMessage)
|
|
95
|
+
case mediaCompleted(BubblMessage)
|
|
96
|
+
case surveyStarted(BubblMessage)
|
|
97
|
+
case surveySubmitted(BubblMessage)
|
|
98
|
+
/// The device entered one of the workspace's locations.
|
|
99
|
+
case geofenceEntered(locationId: String)
|
|
100
|
+
case geofenceExited(locationId: String)
|
|
101
|
+
/// Something went wrong that Bubbl couldn't fix itself (also in diagnostics' lastError).
|
|
102
|
+
case error(message: String)
|
|
103
|
+
/// The server refused the credential Bubbl was started with (`Bubbl.start(credential:options:)`):
|
|
104
|
+
/// Bubbl has stopped until it's started with a new one.
|
|
105
|
+
case credentialRejected
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// What `Bubbl.addEventListener` returns, to remove that listener with (Swift closures have no
|
|
109
|
+
/// identity to remove them by).
|
|
110
|
+
public struct BubblEventToken: Hashable, Sendable {
|
|
111
|
+
let id: UUID
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// The app's event listeners, each told of every event on the main thread.
|
|
115
|
+
final class BubblEvents: @unchecked Sendable {
|
|
116
|
+
static let shared = BubblEvents()
|
|
117
|
+
|
|
118
|
+
private let lock = NSLock()
|
|
119
|
+
private var listeners: [UUID: @MainActor @Sendable (BubblEvent) -> Void] = [:]
|
|
120
|
+
|
|
121
|
+
func add(_ listener: @escaping @MainActor @Sendable (BubblEvent) -> Void) -> BubblEventToken {
|
|
122
|
+
let id = UUID()
|
|
123
|
+
lock.sync { listeners[id] = listener }
|
|
124
|
+
return BubblEventToken(id: id)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func remove(_ token: BubblEventToken) {
|
|
128
|
+
lock.sync { _ = listeners.removeValue(forKey: token.id) }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func emit(_ event: BubblEvent) {
|
|
132
|
+
let current = lock.sync { Array(listeners.values) }
|
|
133
|
+
guard !current.isEmpty else { return }
|
|
134
|
+
Task { @MainActor in current.forEach { $0(event) } }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// The event for a notification event the engine recorded (NotificationEvents' types).
|
|
138
|
+
static func event(_ type: String, _ message: BubblMessage) -> BubblEvent? {
|
|
139
|
+
switch type {
|
|
140
|
+
case "notification.displayed": .notificationDisplayed(message)
|
|
141
|
+
case "notification.opened": .notificationOpened(message)
|
|
142
|
+
case "notification.cta_clicked": .notificationCtaClicked(message)
|
|
143
|
+
case "notification.dismissed": .notificationDismissed(message)
|
|
144
|
+
case "media.viewed": .mediaViewed(message)
|
|
145
|
+
case "media.completed": .mediaCompleted(message)
|
|
146
|
+
case "notification.survey_started": .surveyStarted(message)
|
|
147
|
+
case "survey.submitted": .surveySubmitted(message)
|
|
148
|
+
default: nil
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
#if !COCOAPODS
|
|
3
|
+
import BubblCore
|
|
4
|
+
#endif
|
|
5
|
+
|
|
6
|
+
/// How `Bubbl.start` sets Bubbl up. The same options as Android's BubblOptions.
|
|
7
|
+
public struct BubblOptions: Sendable, Equatable {
|
|
8
|
+
/// How much Bubbl writes to the system log (subsystem "tech.bubbl.sdk").
|
|
9
|
+
public enum LogLevel: String, Sendable, Equatable, CaseIterable, Codable {
|
|
10
|
+
case none, error, warning, info, debug
|
|
11
|
+
|
|
12
|
+
var core: LogSeverity? {
|
|
13
|
+
switch self {
|
|
14
|
+
case .none: nil
|
|
15
|
+
case .error: .error
|
|
16
|
+
case .warning: .warning
|
|
17
|
+
case .info: .info
|
|
18
|
+
case .debug: .debug
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// The Bubbl API to talk to (https). Required for now: 5.0 pre-releases have no default until
|
|
24
|
+
/// the production host is settled.
|
|
25
|
+
public var baseUrl: String
|
|
26
|
+
/// True when the app asks its users first: Bubbl does nothing (no network, no location, no
|
|
27
|
+
/// notifications) until `Bubbl.setConsent(true)`.
|
|
28
|
+
public var requireConsent: Bool
|
|
29
|
+
public var logLevel: LogLevel
|
|
30
|
+
/// The device's segments, as `Bubbl.setSegments` would set them.
|
|
31
|
+
public var segments: [String]?
|
|
32
|
+
|
|
33
|
+
public init(baseUrl: String, requireConsent: Bool = false, logLevel: LogLevel = .warning, segments: [String]? = nil) {
|
|
34
|
+
self.baseUrl = baseUrl
|
|
35
|
+
self.requireConsent = requireConsent
|
|
36
|
+
self.logLevel = logLevel
|
|
37
|
+
self.segments = segments
|
|
38
|
+
}
|
|
39
|
+
}
|