@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.
Files changed (176) hide show
  1. package/BubblReactNativeSdk.podspec +29 -9
  2. package/CHANGELOG.md +52 -1
  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 +79 -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 +320 -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 +169 -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
@@ -0,0 +1,288 @@
1
+ #if os(iOS)
2
+ import Foundation
3
+ import ObjectiveC
4
+ import UIKit
5
+ import UserNotifications
6
+ #if !COCOAPODS
7
+ import BubblCore
8
+ #endif
9
+
10
+ /// Bubbl's part in the app's push handling, with no app code (as Firebase does it):
11
+ /// - the device token: the app delegate's application(_:didRegisterForRemoteNotifications…) is
12
+ /// extended (given one, if the app has none) to hand Bubbl the token as well;
13
+ /// - the notification center's delegate: whichever the app sets (its own, Firebase's,
14
+ /// flutter_local_notifications'…) is extended so Bubbl answers for Bubbl's pushes and passes
15
+ /// everything else on unchanged; when there's none by the end of launch, Bubbl sets its own.
16
+ ///
17
+ /// BubblAutoIntegrationEnabled = NO in Info.plist turns all of it off: the app then calls
18
+ /// Bubbl.setPushToken, Bubbl.willPresent and Bubbl.didReceive from its own code.
19
+ @available(iOS 17, *)
20
+ enum PushIntegration {
21
+ private static let lock = NSLock()
22
+ nonisolated(unsafe) private static var asked = false
23
+ @MainActor private static var installed = false
24
+
25
+ /// Off with BubblAutoIntegrationEnabled = NO (a Boolean, or "NO" / "false").
26
+ static var enabled: Bool {
27
+ switch Bundle.main.object(forInfoDictionaryKey: "BubblAutoIntegrationEnabled") {
28
+ case let flag as Bool: return flag
29
+ case let text as String: return !["no", "false", "0"].contains(text.lowercased())
30
+ default: return true
31
+ }
32
+ }
33
+
34
+ /// Installs once launch has finished: apps and their plugins set their notification delegate
35
+ /// during launch only when there's none yet, so Bubbl's own must not be there first. Taps that
36
+ /// launched the app are delivered after launch, so they still reach it. At the end of launch
37
+ /// (`launchFinished`: Bubbl starting itself) it installs now; started after launch (a
38
+ /// wrapper's JavaScript or Dart), at the next turn of the main loop.
39
+ static func installWhenLaunched(launchFinished: Bool = false) {
40
+ guard enabled else {
41
+ BubblLog.info("Bubbl's push integration is off (BubblAutoIntegrationEnabled): the app hands Bubbl the token and its pushes")
42
+ return
43
+ }
44
+ let first: Bool = lock.sync {
45
+ defer { asked = true }
46
+ return !asked
47
+ }
48
+ guard first else { return }
49
+
50
+ if launchFinished, Thread.isMainThread {
51
+ MainActor.assumeIsolated { install() }
52
+ return
53
+ }
54
+ _ = NotificationCenter.default.addObserver(forName: UIApplication.didFinishLaunchingNotification, object: nil, queue: .main) { _ in
55
+ MainActor.assumeIsolated { install() }
56
+ }
57
+ DispatchQueue.main.async {
58
+ MainActor.assumeIsolated { install() }
59
+ }
60
+ }
61
+
62
+ @MainActor
63
+ private static func install() {
64
+ guard !installed else { return }
65
+ installed = true
66
+ AppDelegateHooks.install()
67
+ NotificationDelegateHooks.install()
68
+ registerIfAllowed()
69
+ }
70
+
71
+ /// Asks iOS for the device token once the user allows notifications: a token for a device that
72
+ /// can't show them would only make pushes that go nowhere look delivered. (Asking for the
73
+ /// permission through Bubbl registers as well.)
74
+ /// (Not main-actor code: iOS answers on a queue of its own, and a main-actor closure run there
75
+ /// is a Swift runtime trap.)
76
+ static func registerIfAllowed() {
77
+ Task {
78
+ let status = await PermissionFlow.notificationAuthorization()
79
+ guard status == .authorized || status == .provisional || status == .ephemeral else { return }
80
+ await MainActor.run { UIApplication.shared.registerForRemoteNotifications() }
81
+ }
82
+ }
83
+ }
84
+
85
+ // MARK: - The app delegate's token callbacks
86
+
87
+ @available(iOS 17, *)
88
+ enum AppDelegateHooks {
89
+ private typealias DidRegister = @convention(c) (AnyObject, Selector, UIApplication, NSData) -> Void
90
+ private typealias DidFail = @convention(c) (AnyObject, Selector, UIApplication, NSError) -> Void
91
+
92
+ @MainActor
93
+ static func install() {
94
+ guard let delegate = UIApplication.shared.delegate, let cls = object_getClass(delegate) else {
95
+ BubblLog.warning("The app has no delegate, so Bubbl can't get the device token: call Bubbl.setPushToken from the app")
96
+ return
97
+ }
98
+
99
+ let didRegister = #selector(UIApplicationDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:))
100
+ Swizzle.extend(cls, didRegister, types: "v@:@@") { original in
101
+ let block: @convention(block) (AnyObject, UIApplication, NSData) -> Void = { this, application, token in
102
+ EngineHost.shared.pushTokenReceived(token as Data)
103
+ if let original {
104
+ unsafeBitCast(original, to: DidRegister.self)(this, didRegister, application, token)
105
+ } else if let target = Swizzle.forwardee(this, didRegister) {
106
+ // SwiftUI's app delegate hands the app's UIApplicationDelegateAdaptor its
107
+ // messages; now that Bubbl gave it the method, pass it on the same way.
108
+ _ = target.perform(didRegister, with: application, with: token)
109
+ }
110
+ }
111
+ return block
112
+ }
113
+
114
+ let didFail = #selector(UIApplicationDelegate.application(_:didFailToRegisterForRemoteNotificationsWithError:))
115
+ Swizzle.extend(cls, didFail, types: "v@:@@") { original in
116
+ let block: @convention(block) (AnyObject, UIApplication, NSError) -> Void = { this, application, error in
117
+ BubblLog.warning("iOS couldn't give the app a device token (\(error.domain) \(error.code)): pushes can't reach this device")
118
+ if let original {
119
+ unsafeBitCast(original, to: DidFail.self)(this, didFail, application, error)
120
+ } else if let target = Swizzle.forwardee(this, didFail) {
121
+ _ = target.perform(didFail, with: application, with: error)
122
+ }
123
+ }
124
+ return block
125
+ }
126
+ }
127
+ }
128
+
129
+ // MARK: - The notification center's delegate
130
+
131
+ @available(iOS 17, *)
132
+ enum NotificationDelegateHooks {
133
+ private typealias SetDelegate = @convention(c) (AnyObject, Selector, AnyObject?) -> Void
134
+ private typealias WillPresent = @convention(c) (AnyObject, Selector, UNUserNotificationCenter, UNNotification, @escaping @convention(block) (UNNotificationPresentationOptions) -> Void) -> Void
135
+ private typealias DidReceive = @convention(c) (AnyObject, Selector, UNUserNotificationCenter, UNNotificationResponse, @escaping @convention(block) () -> Void) -> Void
136
+
137
+ private static let lock = NSLock()
138
+ nonisolated(unsafe) private static var hooked: Set<ObjectIdentifier> = []
139
+ /// Bubbl's own delegate when the app has none, kept alive here (the center holds it weakly).
140
+ nonisolated(unsafe) private static var own: BubblNotificationCenterDelegate?
141
+
142
+ @MainActor
143
+ static func install() {
144
+ hookSetDelegate()
145
+ let center = UNUserNotificationCenter.current()
146
+ if let delegate = center.delegate {
147
+ hook(object_getClass(delegate))
148
+ } else {
149
+ let delegate = BubblNotificationCenterDelegate()
150
+ lock.sync { own = delegate }
151
+ center.delegate = delegate
152
+ }
153
+ }
154
+
155
+ /// Bubbl answers for a push arriving in front when it's Bubbl's; false when it isn't (then the
156
+ /// app's own handling decides).
157
+ static func willPresent(_ notification: UNNotification, _ completion: @escaping (UNNotificationPresentationOptions) -> Void) -> Bool {
158
+ let data = EngineHost.pushData(notification.request.content.userInfo)
159
+ guard PushMessage.isBubbl(data) else { return false }
160
+ arrived(data, completion)
161
+ return true
162
+ }
163
+
164
+ /// Bubbl answers for a tap on its push's notification; false when it isn't Bubbl's.
165
+ static func didReceive(_ response: UNNotificationResponse, _ completion: @escaping () -> Void) -> Bool {
166
+ let data = EngineHost.pushData(response.notification.request.content.userInfo)
167
+ guard PushMessage.isBubbl(data) else { return false }
168
+ opened(data, tapped: response.actionIdentifier == UNNotificationDefaultActionIdentifier, completion)
169
+ return true
170
+ }
171
+
172
+ // iOS's completion handlers are called on the main thread, whatever thread the delegate was
173
+ // called on and wherever the work in between ran: UIKit's for a notification response updates
174
+ // the app's snapshot and state restoration, and asserts it's on the main thread (a tap on a
175
+ // campaign push crashed TestFlight build 6 at launch when it wasn't).
176
+
177
+ /// A Bubbl push arriving in front: how to present it, handed to `completion` on the main thread.
178
+ static func arrived(_ data: [String: JSONValue], _ completion: @escaping (UNNotificationPresentationOptions) -> Void) {
179
+ let done = Handed(completion)
180
+ Task { @MainActor in
181
+ let options = await EngineHost.shared.presentation(forArriving: data)
182
+ done.value(options)
183
+ }
184
+ }
185
+
186
+ /// A Bubbl push's notification answered (`tapped`: opened), then `completion` on the main thread.
187
+ static func opened(_ data: [String: JSONValue], tapped: Bool, _ completion: @escaping () -> Void) {
188
+ let done = Handed(completion)
189
+ Task { @MainActor in
190
+ if tapped { await EngineHost.shared.pushOpened(data) }
191
+ done.value()
192
+ }
193
+ }
194
+
195
+ /// A delegate the app sets later (after Bubbl's, or replacing another) is extended too.
196
+ private static func hookSetDelegate() {
197
+ let selector = #selector(setter: UNUserNotificationCenter.delegate)
198
+ guard let method = class_getInstanceMethod(UNUserNotificationCenter.self, selector) else { return }
199
+ let original = unsafeBitCast(method_getImplementation(method), to: SetDelegate.self)
200
+ let block: @convention(block) (AnyObject, AnyObject?) -> Void = { center, delegate in
201
+ original(center, selector, delegate)
202
+ if let delegate, !(delegate is BubblNotificationCenterDelegate) { hook(object_getClass(delegate)) }
203
+ }
204
+ method_setImplementation(method, imp_implementationWithBlock(block))
205
+ }
206
+
207
+ private static func hook(_ cls: AnyClass?) {
208
+ guard let cls, lock.sync({ hooked.insert(ObjectIdentifier(cls)).inserted }) else { return }
209
+
210
+ let willPresentSelector = #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:withCompletionHandler:))
211
+ Swizzle.extend(cls, willPresentSelector, types: "v@:@@@?") { original in
212
+ let block: @convention(block) (AnyObject, UNUserNotificationCenter, UNNotification, @escaping @convention(block) (UNNotificationPresentationOptions) -> Void) -> Void = { this, center, notification, completion in
213
+ if willPresent(notification, completion) { return }
214
+ if let original {
215
+ unsafeBitCast(original, to: WillPresent.self)(this, willPresentSelector, center, notification, completion)
216
+ } else {
217
+ // The app's delegate doesn't handle pushes in front: iOS's default, not shown.
218
+ completion([])
219
+ }
220
+ }
221
+ return block
222
+ }
223
+
224
+ let didReceiveSelector = #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:didReceive:withCompletionHandler:))
225
+ Swizzle.extend(cls, didReceiveSelector, types: "v@:@@@?") { original in
226
+ let block: @convention(block) (AnyObject, UNUserNotificationCenter, UNNotificationResponse, @escaping @convention(block) () -> Void) -> Void = { this, center, response, completion in
227
+ if didReceive(response, completion) { return }
228
+ if let original {
229
+ unsafeBitCast(original, to: DidReceive.self)(this, didReceiveSelector, center, response, completion)
230
+ } else {
231
+ completion()
232
+ }
233
+ }
234
+ return block
235
+ }
236
+ }
237
+ }
238
+
239
+ /// The notification center's delegate when the app has none: Bubbl's pushes are Bubbl's to
240
+ /// handle; others are left as iOS would (not shown in front, a tap just opens the app).
241
+ /// (The async forms: they match the protocol however its completion handlers are annotated.)
242
+ /// On the main actor, so iOS's completion handlers (which Swift calls when these return) run on
243
+ /// the main thread: nonisolated, they returned on a background thread (see `opened`).
244
+ @available(iOS 17, *)
245
+ @MainActor
246
+ final class BubblNotificationCenterDelegate: NSObject, @preconcurrency UNUserNotificationCenterDelegate {
247
+ func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
248
+ let data = EngineHost.pushData(notification.request.content.userInfo)
249
+ guard PushMessage.isBubbl(data) else { return [] }
250
+ return await EngineHost.shared.presentation(forArriving: data)
251
+ }
252
+
253
+ func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
254
+ let data = EngineHost.pushData(response.notification.request.content.userInfo)
255
+ guard PushMessage.isBubbl(data), response.actionIdentifier == UNNotificationDefaultActionIdentifier else { return }
256
+ await EngineHost.shared.pushOpened(data)
257
+ }
258
+ }
259
+
260
+ // MARK: - Extending a class's method
261
+
262
+ @available(iOS 17, *)
263
+ enum Swizzle {
264
+ /// Gives `cls` its own `selector`, implemented by the block `make` returns for the original
265
+ /// implementation: the class's own or a superclass's, nil when it had none. A superclass's
266
+ /// implementation is left as it was, so other subclasses aren't touched.
267
+ static func extend(_ cls: AnyClass, _ selector: Selector, types: String, _ make: (IMP?) -> Any) {
268
+ let inherited = class_getInstanceMethod(cls, selector)
269
+ let replacement = imp_implementationWithBlock(make(inherited.map(method_getImplementation)))
270
+ let added = if let inherited, let encoding = method_getTypeEncoding(inherited) {
271
+ class_addMethod(cls, selector, replacement, encoding)
272
+ } else {
273
+ class_addMethod(cls, selector, replacement, types)
274
+ }
275
+ // The class defines it itself: replace it there (the original is that one).
276
+ if !added, let own = class_getInstanceMethod(cls, selector) { method_setImplementation(own, replacement) }
277
+ }
278
+
279
+ /// Where an object sends a message it doesn't implement itself (SwiftUI's app delegate sends
280
+ /// the app's UIApplicationDelegateAdaptor its messages this way), if anywhere else.
281
+ static func forwardee(_ object: AnyObject, _ selector: Selector) -> NSObject? {
282
+ guard let target = (object as? NSObject)?.forwardingTarget(for: selector) as? NSObject,
283
+ target !== object, target.responds(to: selector)
284
+ else { return nil }
285
+ return target
286
+ }
287
+ }
288
+ #endif
@@ -0,0 +1,69 @@
1
+ import Foundation
2
+ #if canImport(FoundationNetworking)
3
+ import FoundationNetworking
4
+ #endif
5
+ #if !COCOAPODS
6
+ import BubblCore
7
+ #endif
8
+
9
+ /// The engine's way to the network: an ephemeral URLSession of its own.
10
+ /// - No URL cache: a 304 for the engine's own If-None-Match reaches it as a 304, not as a
11
+ /// cached 200.
12
+ /// - No redirects followed: the request is signed for its own URL, and a redirect would carry
13
+ /// the signing headers to another host.
14
+ /// - Explicit timeouts, so work never hangs past what a background task allows.
15
+ @available(iOS 17, *)
16
+ final class URLSessionHttpClient: HttpClient, @unchecked Sendable {
17
+ private let session: URLSession
18
+
19
+ /// - Parameter protocolClasses: for tests, URLProtocols that answer instead of the network.
20
+ init(requestTimeout: TimeInterval = 20, resourceTimeout: TimeInterval = 40, protocolClasses: [AnyClass]? = nil) {
21
+ let configuration = URLSessionConfiguration.ephemeral
22
+ if let protocolClasses { configuration.protocolClasses = protocolClasses }
23
+ configuration.urlCache = nil
24
+ configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
25
+ configuration.httpCookieStorage = nil
26
+ configuration.httpShouldSetCookies = false
27
+ configuration.timeoutIntervalForRequest = requestTimeout
28
+ configuration.timeoutIntervalForResource = resourceTimeout
29
+ configuration.waitsForConnectivity = false
30
+ session = URLSession(configuration: configuration, delegate: RedirectRefuser(), delegateQueue: nil)
31
+ }
32
+
33
+ deinit {
34
+ session.finishTasksAndInvalidate()
35
+ }
36
+
37
+ func execute(_ request: HttpRequest) async throws -> HttpResponse {
38
+ guard let url = URL(string: request.url) else { throw URLError(.badURL) }
39
+ var urlRequest = URLRequest(url: url)
40
+ urlRequest.httpMethod = request.method
41
+ request.headers.forEach { urlRequest.setValue($0.value, forHTTPHeaderField: $0.key) }
42
+ urlRequest.httpBody = request.body.map { Data($0.utf8) }
43
+
44
+ let data: Data
45
+ let response: URLResponse
46
+ do {
47
+ (data, response) = try await session.data(for: urlRequest)
48
+ } catch let error as URLError where error.code == .appTransportSecurityRequiresSecureConnection {
49
+ BubblLog.warning("An http:// baseUrl needs NSAllowsLocalNetworking under NSAppTransportSecurity in the app's Info.plist")
50
+ throw error
51
+ }
52
+ guard let http = response as? HTTPURLResponse else { throw URLError(.badServerResponse) }
53
+
54
+ var headers: [String: [String]] = [:]
55
+ for (name, value) in http.allHeaderFields {
56
+ if let name = name as? String { headers[name] = [String(describing: value)] }
57
+ }
58
+ return HttpResponse(status: http.statusCode, body: String(decoding: data, as: UTF8.self), headers: headers)
59
+ }
60
+ }
61
+
62
+ /// Returns a redirect as the response itself (a 3xx the engine treats like any other status)
63
+ /// instead of following it.
64
+ @available(iOS 17, *)
65
+ private final class RedirectRefuser: NSObject, URLSessionTaskDelegate, @unchecked Sendable {
66
+ func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest) async -> URLRequest? {
67
+ nil
68
+ }
69
+ }
@@ -0,0 +1,76 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <!--
4
+ Bubbl's privacy manifest, which Xcode folds into the app's privacy report. Bubbl doesn't track
5
+ (no data is combined with other companies' for advertising, no tracking domains) and uses none of
6
+ Apple's "required reason" APIs. What it collects, for the app's workspace only, is keyed to an
7
+ install id it makes itself, never to a person: the app declares more if it links Bubbl's data to
8
+ its own accounts.
9
+ -->
10
+ <plist version="1.0">
11
+ <dict>
12
+ <key>NSPrivacyTracking</key>
13
+ <false/>
14
+ <key>NSPrivacyTrackingDomains</key>
15
+ <array/>
16
+ <key>NSPrivacyAccessedAPITypes</key>
17
+ <array/>
18
+ <key>NSPrivacyCollectedDataTypes</key>
19
+ <array>
20
+ <!-- Where the device is when it enters or leaves one of the workspace's places. -->
21
+ <dict>
22
+ <key>NSPrivacyCollectedDataType</key>
23
+ <string>NSPrivacyCollectedDataTypePreciseLocation</string>
24
+ <key>NSPrivacyCollectedDataTypeLinked</key>
25
+ <false/>
26
+ <key>NSPrivacyCollectedDataTypeTracking</key>
27
+ <false/>
28
+ <key>NSPrivacyCollectedDataTypePurposes</key>
29
+ <array>
30
+ <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
31
+ <string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
32
+ </array>
33
+ </dict>
34
+ <!-- The install id Bubbl makes, and the device's push token. -->
35
+ <dict>
36
+ <key>NSPrivacyCollectedDataType</key>
37
+ <string>NSPrivacyCollectedDataTypeDeviceID</string>
38
+ <key>NSPrivacyCollectedDataTypeLinked</key>
39
+ <false/>
40
+ <key>NSPrivacyCollectedDataTypeTracking</key>
41
+ <false/>
42
+ <key>NSPrivacyCollectedDataTypePurposes</key>
43
+ <array>
44
+ <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
45
+ </array>
46
+ </dict>
47
+ <!-- Notifications shown, opened and acted on; app opens; the app's own events. -->
48
+ <dict>
49
+ <key>NSPrivacyCollectedDataType</key>
50
+ <string>NSPrivacyCollectedDataTypeProductInteraction</string>
51
+ <key>NSPrivacyCollectedDataTypeLinked</key>
52
+ <false/>
53
+ <key>NSPrivacyCollectedDataTypeTracking</key>
54
+ <false/>
55
+ <key>NSPrivacyCollectedDataTypePurposes</key>
56
+ <array>
57
+ <string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
58
+ </array>
59
+ </dict>
60
+ <!-- Survey answers. -->
61
+ <dict>
62
+ <key>NSPrivacyCollectedDataType</key>
63
+ <string>NSPrivacyCollectedDataTypeOtherUserContent</string>
64
+ <key>NSPrivacyCollectedDataTypeLinked</key>
65
+ <false/>
66
+ <key>NSPrivacyCollectedDataTypeTracking</key>
67
+ <false/>
68
+ <key>NSPrivacyCollectedDataTypePurposes</key>
69
+ <array>
70
+ <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
71
+ <string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
72
+ </array>
73
+ </dict>
74
+ </array>
75
+ </dict>
76
+ </plist>
package/jest/index.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Bubbl for an app's Jest tests, where there's no native module:
3
+ *
4
+ * jest.mock('@bubblsdk/react-native-sdk', () => require('@bubblsdk/react-native-sdk/jest'));
5
+ *
6
+ * Every function is a jest.fn doing what Bubbl does before start (nothing; permissions null,
7
+ * submitSurvey false), so tests can assert the calls. isBubblMessage is the real one.
8
+ */
9
+ 'use strict';
10
+
11
+ const fn = (implementation) => jest.fn(implementation);
12
+ const resolved = (value) => fn(() => Promise.resolve(value));
13
+
14
+ const Bubbl = {
15
+ isSupported: true,
16
+ start: fn(),
17
+ startWithCredential: fn(),
18
+ stop: fn(),
19
+ setConsent: fn(),
20
+ optOut: fn(),
21
+ deleteMyData: fn(),
22
+ setLocationEnabled: fn(),
23
+ permissions: {
24
+ status: resolved(null),
25
+ requestNotifications: resolved(null),
26
+ requestLocation: resolved(null),
27
+ openSettings: fn(),
28
+ },
29
+ setSegments: fn(),
30
+ track: fn(),
31
+ addEventListener: fn(() => ({ remove: fn() })),
32
+ setNotificationListener: fn(),
33
+ present: fn(),
34
+ reportDisplayed: fn(),
35
+ reportOpened: fn(),
36
+ reportCtaClicked: fn(),
37
+ reportDismissed: fn(),
38
+ reportMediaViewed: fn(),
39
+ reportMediaCompleted: fn(),
40
+ reportSurveyStarted: fn(),
41
+ submitSurvey: resolved(false),
42
+ openCta: fn(),
43
+ isBubblMessage: fn((data) => data != null && data.bubbl === '1'),
44
+ registerTestDevice: resolved({ approved: true }),
45
+ diagnostics: resolved({
46
+ started: false,
47
+ sdkVersion: require('../package.json').version,
48
+ supported: true,
49
+ sdkSupported: true,
50
+ registered: false,
51
+ installId: null,
52
+ active: false,
53
+ pausedUntil: null,
54
+ consentRequired: false,
55
+ consent: null,
56
+ locationEnabled: true,
57
+ permissions: null,
58
+ hasPushToken: false,
59
+ geofences: 0,
60
+ queuedEvents: 0,
61
+ lastError: null,
62
+ credentialRejected: false,
63
+ environment: null,
64
+ testDevice: null,
65
+ backgroundLocation: false,
66
+ transitionsDroppedWhileLocked: 0,
67
+ backgroundRestricted: null,
68
+ batteryOptimized: null,
69
+ backgroundRefreshAvailable: null,
70
+ lowPowerMode: null,
71
+ osWatchesGeofences: null,
72
+ }),
73
+ };
74
+
75
+ module.exports = { __esModule: true, Bubbl, default: Bubbl };
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "default", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _NativeBubblSdk.default;
10
+ }
11
+ });
12
+ var _NativeBubblSdk = _interopRequireDefault(require("./NativeBubblSdk.js"));
13
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
+ //# sourceMappingURL=BubblModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_NativeBubblSdk","_interopRequireDefault","require","e","__esModule","default"],"sourceRoot":"../../src","sources":["BubblModule.ts"],"mappings":";;;;;;;;;;;AACA,IAAAA,eAAA,GAAAC,sBAAA,CAAAC,OAAA;AAA2C,SAAAD,uBAAAE,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAE,OAAA,EAAAF,CAAA","ignoreList":[]}
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ /**
8
+ * No native module on the web, and react-native-web has no TurboModuleRegistry to ask for one, so
9
+ * NativeBubblSdk.ts isn't loaded at all: Bubbl's calls do nothing there.
10
+ */
11
+ const BubblModule = null;
12
+ var _default = exports.default = BubblModule;
13
+ //# sourceMappingURL=BubblModule.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["BubblModule","_default","exports","default"],"sourceRoot":"../../src","sources":["BubblModule.web.ts"],"mappings":";;;;;;AAEA;AACA;AACA;AACA;AACA,MAAMA,WAAwB,GAAG,IAAI;AAAC,IAAAC,QAAA,GAAAC,OAAA,CAAAC,OAAA,GAEvBH,WAAW","ignoreList":[]}
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _reactNative = require("react-native");
8
+ /**
9
+ * The native module under the facade (src/index.ts): the Bubbl SDK on each platform, called with
10
+ * JSON strings rather than mapped objects (the same convention as the Flutter wrapper), so the
11
+ * bridge stays small and the native types stay the source of truth.
12
+ *
13
+ * - Messages cross as their contract Notification JSON (BubblMessage.json) going in, and as the
14
+ * public message object (fields worked out natively, plus json) coming out.
15
+ * - Events and notifications for the app's listener arrive as JSON on one event, "BubblSdkEvent".
16
+ *
17
+ * Codegen reads this file (package.json codegenConfig); keep it to the types codegen knows.
18
+ */
19
+ var _default = exports.default = _reactNative.TurboModuleRegistry.get('BubblSdk');
20
+ //# sourceMappingURL=NativeBubblSdk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_reactNative","require","_default","exports","default","TurboModuleRegistry","get"],"sourceRoot":"../../src","sources":["NativeBubblSdk.ts"],"mappings":";;;;;;AACA,IAAAA,YAAA,GAAAC,OAAA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAVA,IAAAC,QAAA,GAAAC,OAAA,CAAAC,OAAA,GA+DeC,gCAAmB,CAACC,GAAG,CAAO,UAAU,CAAC","ignoreList":[]}