@bubblsdk/react-native-sdk 4.1.7 → 5.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/BubblReactNativeSdk.podspec +29 -9
  2. package/CHANGELOG.md +47 -0
  3. package/LICENSE +202 -1
  4. package/README.md +172 -83
  5. package/android/build.gradle +33 -30
  6. package/android/src/main/AndroidManifest.xml +1 -0
  7. package/android/src/main/java/tech/bubbl/reactnative/BubblJson.kt +135 -0
  8. package/android/src/main/java/tech/bubbl/reactnative/BubblSdkModule.kt +277 -0
  9. package/android/src/main/java/tech/bubbl/reactnative/BubblSdkPackage.kt +26 -0
  10. package/app.plugin.js +2 -0
  11. package/ios/BubblPush.swift +44 -0
  12. package/ios/BubblSdk.h +7 -0
  13. package/ios/BubblSdk.mm +252 -0
  14. package/ios/BubblSdkImpl.swift +335 -0
  15. package/ios-extension/BubblReactNativeNotificationService.podspec +44 -0
  16. package/ios-extension/Sources/BubblCore/API/ApiResponse.swift +90 -0
  17. package/ios-extension/Sources/BubblCore/API/CredentialStore.swift +116 -0
  18. package/ios-extension/Sources/BubblCore/API/DeviceApiClient.swift +239 -0
  19. package/ios-extension/Sources/BubblCore/API/ErrorActions.swift +68 -0
  20. package/ios-extension/Sources/BubblCore/API/Http.swift +39 -0
  21. package/ios-extension/Sources/BubblCore/API/ServerClock.swift +69 -0
  22. package/ios-extension/Sources/BubblCore/BubblVersion.swift +5 -0
  23. package/ios-extension/Sources/BubblCore/Device/ConfigSync.swift +216 -0
  24. package/ios-extension/Sources/BubblCore/Device/DeviceSync.swift +160 -0
  25. package/ios-extension/Sources/BubblCore/Device/ProvisioningProfile.swift +24 -0
  26. package/ios-extension/Sources/BubblCore/Engine/EngineCore.swift +523 -0
  27. package/ios-extension/Sources/BubblCore/Engine/Geofencing.swift +170 -0
  28. package/ios-extension/Sources/BubblCore/Engine/Permissions.swift +90 -0
  29. package/ios-extension/Sources/BubblCore/Engine/Privacy.swift +64 -0
  30. package/ios-extension/Sources/BubblCore/Engine/PrivacyControls.swift +184 -0
  31. package/ios-extension/Sources/BubblCore/Engine/Pushes.swift +61 -0
  32. package/ios-extension/Sources/BubblCore/Engine/Sandbox.swift +149 -0
  33. package/ios-extension/Sources/BubblCore/Events/EventQueue.swift +207 -0
  34. package/ios-extension/Sources/BubblCore/Events/EventStore.swift +159 -0
  35. package/ios-extension/Sources/BubblCore/Geofence/Geofence.swift +160 -0
  36. package/ios-extension/Sources/BubblCore/Geofence/GeofenceEngine.swift +488 -0
  37. package/ios-extension/Sources/BubblCore/Geofence/GeofenceState.swift +103 -0
  38. package/ios-extension/Sources/BubblCore/Geofence/RegionBudget.swift +62 -0
  39. package/ios-extension/Sources/BubblCore/Notification/BubblNotification.swift +137 -0
  40. package/ios-extension/Sources/BubblCore/Notification/NotificationEvents.swift +160 -0
  41. package/ios-extension/Sources/BubblCore/Notification/Push.swift +134 -0
  42. package/ios-extension/Sources/BubblCore/Notification/SurveyForm.swift +185 -0
  43. package/ios-extension/Sources/BubblCore/RequestSigner.swift +56 -0
  44. package/ios-extension/Sources/BubblCore/Support/AsyncMutex.swift +39 -0
  45. package/ios-extension/Sources/BubblCore/Support/BubblLog.swift +52 -0
  46. package/ios-extension/Sources/BubblCore/Support/JSON.swift +31 -0
  47. package/ios-extension/Sources/BubblCore/Support/JSONValue.swift +94 -0
  48. package/ios-extension/Sources/BubblCore/Support/Locking.swift +11 -0
  49. package/ios-extension/Sources/BubblCore/Support/ValueStore.swift +109 -0
  50. package/ios-extension/Sources/BubblNotificationService/BubblNotificationService.swift +124 -0
  51. package/ios-sdk/BubblCore/API/ApiResponse.swift +90 -0
  52. package/ios-sdk/BubblCore/API/CredentialStore.swift +116 -0
  53. package/ios-sdk/BubblCore/API/DeviceApiClient.swift +239 -0
  54. package/ios-sdk/BubblCore/API/ErrorActions.swift +68 -0
  55. package/ios-sdk/BubblCore/API/Http.swift +39 -0
  56. package/ios-sdk/BubblCore/API/ServerClock.swift +69 -0
  57. package/ios-sdk/BubblCore/BubblVersion.swift +5 -0
  58. package/ios-sdk/BubblCore/Device/ConfigSync.swift +216 -0
  59. package/ios-sdk/BubblCore/Device/DeviceSync.swift +160 -0
  60. package/ios-sdk/BubblCore/Device/ProvisioningProfile.swift +24 -0
  61. package/ios-sdk/BubblCore/Engine/EngineCore.swift +523 -0
  62. package/ios-sdk/BubblCore/Engine/Geofencing.swift +170 -0
  63. package/ios-sdk/BubblCore/Engine/Permissions.swift +90 -0
  64. package/ios-sdk/BubblCore/Engine/Privacy.swift +64 -0
  65. package/ios-sdk/BubblCore/Engine/PrivacyControls.swift +184 -0
  66. package/ios-sdk/BubblCore/Engine/Pushes.swift +61 -0
  67. package/ios-sdk/BubblCore/Engine/Sandbox.swift +149 -0
  68. package/ios-sdk/BubblCore/Events/EventQueue.swift +207 -0
  69. package/ios-sdk/BubblCore/Events/EventStore.swift +159 -0
  70. package/ios-sdk/BubblCore/Geofence/Geofence.swift +160 -0
  71. package/ios-sdk/BubblCore/Geofence/GeofenceEngine.swift +488 -0
  72. package/ios-sdk/BubblCore/Geofence/GeofenceState.swift +103 -0
  73. package/ios-sdk/BubblCore/Geofence/RegionBudget.swift +62 -0
  74. package/ios-sdk/BubblCore/Notification/BubblNotification.swift +137 -0
  75. package/ios-sdk/BubblCore/Notification/NotificationEvents.swift +160 -0
  76. package/ios-sdk/BubblCore/Notification/Push.swift +134 -0
  77. package/ios-sdk/BubblCore/Notification/SurveyForm.swift +185 -0
  78. package/ios-sdk/BubblCore/RequestSigner.swift +56 -0
  79. package/ios-sdk/BubblCore/Support/AsyncMutex.swift +39 -0
  80. package/ios-sdk/BubblCore/Support/BubblLog.swift +52 -0
  81. package/ios-sdk/BubblCore/Support/JSON.swift +31 -0
  82. package/ios-sdk/BubblCore/Support/JSONValue.swift +94 -0
  83. package/ios-sdk/BubblCore/Support/Locking.swift +11 -0
  84. package/ios-sdk/BubblCore/Support/ValueStore.swift +109 -0
  85. package/ios-sdk/BubblLaunch/BubblLaunch.m +45 -0
  86. package/ios-sdk/BubblLaunch/include/BubblLaunch.h +5 -0
  87. package/ios-sdk/BubblSDK/Bubbl.swift +308 -0
  88. package/ios-sdk/BubblSDK/BubblBackend.swift +118 -0
  89. package/ios-sdk/BubblSDK/BubblCredential.swift +30 -0
  90. package/ios-sdk/BubblSDK/BubblMessage.swift +151 -0
  91. package/ios-sdk/BubblSDK/BubblOptions.swift +39 -0
  92. package/ios-sdk/BubblSDK/BubblPermissions.swift +77 -0
  93. package/ios-sdk/BubblSDK/BubblTestDeviceResult.swift +10 -0
  94. package/ios-sdk/BubblSDK/Engine/BackgroundRefresh.swift +92 -0
  95. package/ios-sdk/BubblSDK/Engine/EngineHost+Geofences.swift +177 -0
  96. package/ios-sdk/BubblSDK/Engine/EngineHost+Privacy.swift +65 -0
  97. package/ios-sdk/BubblSDK/Engine/EngineHost+Push.swift +154 -0
  98. package/ios-sdk/BubblSDK/Engine/EngineHost.swift +356 -0
  99. package/ios-sdk/BubblSDK/Engine/SupportedBubbl.swift +318 -0
  100. package/ios-sdk/BubblSDK/Engine/SystemLog.swift +27 -0
  101. package/ios-sdk/BubblSDK/Engine/WorkRunner.swift +107 -0
  102. package/ios-sdk/BubblSDK/Notification/Look.swift +78 -0
  103. package/ios-sdk/BubblSDK/Notification/NotificationMedia.swift +329 -0
  104. package/ios-sdk/BubblSDK/Notification/NotificationModel.swift +172 -0
  105. package/ios-sdk/BubblSDK/Notification/NotificationScreen.swift +428 -0
  106. package/ios-sdk/BubblSDK/Notification/ScreenPresenter.swift +148 -0
  107. package/ios-sdk/BubblSDK/Permissions/PermissionFlow.swift +167 -0
  108. package/ios-sdk/BubblSDK/Permissions/PrivacyView.swift +110 -0
  109. package/ios-sdk/BubblSDK/Platform/AppleDevicePlatform.swift +95 -0
  110. package/ios-sdk/BubblSDK/Platform/KeychainCredentialStore.swift +114 -0
  111. package/ios-sdk/BubblSDK/Platform/LocationService.swift +418 -0
  112. package/ios-sdk/BubblSDK/Platform/PushIntegration.swift +288 -0
  113. package/ios-sdk/BubblSDK/Platform/URLSessionHttpClient.swift +69 -0
  114. package/ios-sdk/BubblSDK/PrivacyInfo.xcprivacy +76 -0
  115. package/jest/index.js +75 -0
  116. package/lib/commonjs/BubblModule.js +14 -0
  117. package/lib/commonjs/BubblModule.js.map +1 -0
  118. package/lib/commonjs/BubblModule.web.js +13 -0
  119. package/lib/commonjs/BubblModule.web.js.map +1 -0
  120. package/lib/commonjs/NativeBubblSdk.js +20 -0
  121. package/lib/commonjs/NativeBubblSdk.js.map +1 -0
  122. package/lib/commonjs/index.js +336 -0
  123. package/lib/commonjs/index.js.map +1 -0
  124. package/lib/commonjs/package.json +1 -0
  125. package/lib/commonjs/types.js +2 -0
  126. package/lib/commonjs/types.js.map +1 -0
  127. package/lib/module/BubblModule.js +5 -0
  128. package/lib/module/BubblModule.js.map +1 -0
  129. package/lib/module/BubblModule.web.js +9 -0
  130. package/lib/module/BubblModule.web.js.map +1 -0
  131. package/lib/module/NativeBubblSdk.js +18 -0
  132. package/lib/module/NativeBubblSdk.js.map +1 -0
  133. package/lib/module/index.js +331 -0
  134. package/lib/module/index.js.map +1 -0
  135. package/lib/module/package.json +1 -0
  136. package/lib/module/types.js +2 -0
  137. package/lib/module/types.js.map +1 -0
  138. package/lib/typescript/commonjs/package.json +1 -0
  139. package/lib/typescript/commonjs/src/BubblModule.d.ts +3 -0
  140. package/lib/typescript/commonjs/src/BubblModule.d.ts.map +1 -0
  141. package/lib/typescript/commonjs/src/BubblModule.web.d.ts +8 -0
  142. package/lib/typescript/commonjs/src/BubblModule.web.d.ts.map +1 -0
  143. package/lib/typescript/commonjs/src/NativeBubblSdk.d.ts +59 -0
  144. package/lib/typescript/commonjs/src/NativeBubblSdk.d.ts.map +1 -0
  145. package/lib/typescript/commonjs/src/index.d.ts +111 -0
  146. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  147. package/lib/typescript/commonjs/src/types.d.ts +192 -0
  148. package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
  149. package/lib/typescript/module/package.json +1 -0
  150. package/lib/typescript/module/src/BubblModule.d.ts +3 -0
  151. package/lib/typescript/module/src/BubblModule.d.ts.map +1 -0
  152. package/lib/typescript/module/src/BubblModule.web.d.ts +8 -0
  153. package/lib/typescript/module/src/BubblModule.web.d.ts.map +1 -0
  154. package/lib/typescript/module/src/NativeBubblSdk.d.ts +59 -0
  155. package/lib/typescript/module/src/NativeBubblSdk.d.ts.map +1 -0
  156. package/lib/typescript/module/src/index.d.ts +111 -0
  157. package/lib/typescript/module/src/index.d.ts.map +1 -0
  158. package/lib/typescript/module/src/types.d.ts +192 -0
  159. package/lib/typescript/module/src/types.d.ts.map +1 -0
  160. package/package.json +105 -24
  161. package/plugin/build/imageNotifications.d.ts +35 -0
  162. package/plugin/build/imageNotifications.js +198 -0
  163. package/plugin/build/index.d.ts +84 -0
  164. package/plugin/build/index.js +176 -0
  165. package/src/BubblModule.ts +2 -0
  166. package/src/BubblModule.web.ts +9 -0
  167. package/src/NativeBubblSdk.ts +67 -0
  168. package/src/index.ts +338 -282
  169. package/src/types.ts +182 -0
  170. package/android/src/main/kotlin/tech/bubbl/reactnative/BubblSdkModule.kt +0 -680
  171. package/android/src/main/kotlin/tech/bubbl/reactnative/BubblSdkNotificationIntents.kt +0 -32
  172. package/android/src/main/kotlin/tech/bubbl/reactnative/BubblSdkPackage.kt +0 -14
  173. package/ios/BubblSdk.swift +0 -802
  174. package/ios/BubblSdkBridge.m +0 -86
  175. package/react-native.config.js +0 -11
  176. package/src/specs/NativeBubblSdk.ts +0 -54
@@ -0,0 +1,252 @@
1
+ #import "BubblSdk.h"
2
+
3
+ // Before the Swift header: BubblPush's signatures use UserNotifications types.
4
+ #import <UserNotifications/UserNotifications.h>
5
+
6
+ #if __has_include(<BubblReactNativeSdk/BubblReactNativeSdk-Swift.h>)
7
+ #import <BubblReactNativeSdk/BubblReactNativeSdk-Swift.h>
8
+ #else
9
+ #import "BubblReactNativeSdk-Swift.h"
10
+ #endif
11
+
12
+ static NSString *const BubblSdkEvent = @"BubblSdkEvent";
13
+
14
+ @implementation BubblSdk {
15
+ BubblSdkImpl *_impl;
16
+ BOOL _hasListeners;
17
+ }
18
+
19
+ RCT_EXPORT_MODULE(BubblSdk)
20
+
21
+ - (instancetype)init
22
+ {
23
+ if (self = [super init]) {
24
+ __weak BubblSdk *weakSelf = self;
25
+ _impl = [[BubblSdkImpl alloc] initWithEmit:^BOOL(NSString *json) {
26
+ return [weakSelf emit:json];
27
+ }];
28
+ }
29
+ return self;
30
+ }
31
+
32
+ + (BOOL)requiresMainQueueSetup
33
+ {
34
+ return NO;
35
+ }
36
+
37
+ - (NSArray<NSString *> *)supportedEvents
38
+ {
39
+ return @[ BubblSdkEvent ];
40
+ }
41
+
42
+ - (void)startObserving
43
+ {
44
+ _hasListeners = YES;
45
+ }
46
+
47
+ - (void)stopObserving
48
+ {
49
+ _hasListeners = NO;
50
+ }
51
+
52
+ /// Whether JS got it: with no JS listener it can't (a notification then stays Bubbl's to show).
53
+ - (BOOL)emit:(NSString *)json
54
+ {
55
+ if (!_hasListeners) {
56
+ return NO;
57
+ }
58
+ [self sendEventWithName:BubblSdkEvent body:json];
59
+ return YES;
60
+ }
61
+
62
+ - (void)invalidate
63
+ {
64
+ [_impl invalidate];
65
+ [super invalidate];
66
+ }
67
+
68
+ // MARK: - Starting, consent and privacy
69
+
70
+ - (NSNumber *)isSupported
71
+ {
72
+ return @([_impl isSupported]);
73
+ }
74
+
75
+ - (void)start:(NSString *)apiKey optionsJson:(NSString *)optionsJson
76
+ {
77
+ [_impl startWithApiKey:apiKey optionsJson:optionsJson];
78
+ }
79
+
80
+ - (void)startWithCredential:(NSString *)keyId
81
+ secret:(NSString *)secret
82
+ installId:(NSString *)installId
83
+ optionsJson:(NSString *)optionsJson
84
+ {
85
+ [_impl startWithCredentialWithKeyId:keyId secret:secret installId:installId optionsJson:optionsJson];
86
+ }
87
+
88
+ - (void)stop
89
+ {
90
+ [_impl stop];
91
+ }
92
+
93
+ - (void)setConsent:(BOOL)granted
94
+ {
95
+ [_impl setConsent:granted];
96
+ }
97
+
98
+ - (void)optOut
99
+ {
100
+ [_impl optOut];
101
+ }
102
+
103
+ - (void)deleteMyData
104
+ {
105
+ [_impl deleteMyData];
106
+ }
107
+
108
+ - (void)setLocationEnabled:(BOOL)enabled
109
+ {
110
+ [_impl setLocationEnabled:enabled];
111
+ }
112
+
113
+ // MARK: - Permissions (JSON, or null before start)
114
+
115
+ - (void)permissionStatus:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
116
+ {
117
+ resolve([_impl permissionStatus]);
118
+ }
119
+
120
+ - (void)requestNotifications:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
121
+ {
122
+ [_impl requestNotifications:^(NSString *json) {
123
+ resolve(json);
124
+ }];
125
+ }
126
+
127
+ - (void)requestLocation:(BOOL)always resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
128
+ {
129
+ [_impl requestLocationWithAlways:always completion:^(NSString *json) {
130
+ resolve(json);
131
+ }];
132
+ }
133
+
134
+ - (void)openSettings
135
+ {
136
+ [_impl openSettings];
137
+ }
138
+
139
+ // MARK: - Targeting and events
140
+
141
+ - (void)setSegments:(NSArray *)segments
142
+ {
143
+ NSMutableArray<NSString *> *names = [NSMutableArray array];
144
+ for (id segment in segments) {
145
+ if ([segment isKindOfClass:[NSString class]] && [segment length] > 0) {
146
+ [names addObject:segment];
147
+ }
148
+ }
149
+ [_impl setSegments:names];
150
+ }
151
+
152
+ - (void)track:(NSString *)name propertiesJson:(NSString *)propertiesJson
153
+ {
154
+ [_impl trackWithName:name propertiesJson:propertiesJson];
155
+ }
156
+
157
+ // MARK: - Notifications
158
+
159
+ - (void)setNotificationListenerEnabled:(BOOL)enabled
160
+ {
161
+ [_impl setNotificationListenerEnabled:enabled];
162
+ }
163
+
164
+ - (void)claimNotification:(NSString *)token
165
+ {
166
+ [_impl claimNotification:token];
167
+ }
168
+
169
+ - (void)finishNotification:(NSString *)token appShowsIt:(BOOL)appShowsIt
170
+ {
171
+ [_impl finishNotification:token appShowsIt:appShowsIt];
172
+ }
173
+
174
+ - (void)present:(NSString *)messageJson
175
+ {
176
+ [_impl present:messageJson];
177
+ }
178
+
179
+ - (void)reportDisplayed:(NSString *)messageJson
180
+ {
181
+ [_impl reportDisplayed:messageJson];
182
+ }
183
+
184
+ - (void)reportOpened:(NSString *)messageJson
185
+ {
186
+ [_impl reportOpened:messageJson];
187
+ }
188
+
189
+ - (void)reportCtaClicked:(NSString *)messageJson
190
+ {
191
+ [_impl reportCtaClicked:messageJson];
192
+ }
193
+
194
+ - (void)reportDismissed:(NSString *)messageJson
195
+ {
196
+ [_impl reportDismissed:messageJson];
197
+ }
198
+
199
+ - (void)reportMediaViewed:(NSString *)messageJson
200
+ {
201
+ [_impl reportMediaViewed:messageJson];
202
+ }
203
+
204
+ - (void)reportMediaCompleted:(NSString *)messageJson durationSeconds:(double)durationSeconds
205
+ {
206
+ [_impl reportMediaCompleted:messageJson durationSeconds:durationSeconds];
207
+ }
208
+
209
+ - (void)reportSurveyStarted:(NSString *)messageJson
210
+ {
211
+ [_impl reportSurveyStarted:messageJson];
212
+ }
213
+
214
+ - (void)submitSurvey:(NSString *)messageJson
215
+ answersJson:(NSString *)answersJson
216
+ resolve:(RCTPromiseResolveBlock)resolve
217
+ reject:(RCTPromiseRejectBlock)reject
218
+ {
219
+ resolve(@([_impl submitSurvey:messageJson answersJson:answersJson]));
220
+ }
221
+
222
+ - (void)openCta:(NSString *)messageJson
223
+ {
224
+ [_impl openCta:messageJson];
225
+ }
226
+
227
+ // MARK: - Sandbox
228
+
229
+ - (void)registerTestDevice:(NSString *)code resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
230
+ {
231
+ [_impl registerTestDevice:code completion:^(NSString *json) {
232
+ resolve(json);
233
+ }];
234
+ }
235
+
236
+ // MARK: - Diagnostics
237
+
238
+ - (void)diagnostics:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
239
+ {
240
+ [_impl diagnostics:^(NSString *json) {
241
+ resolve(json);
242
+ }];
243
+ }
244
+
245
+ // MARK: - TurboModule
246
+
247
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params
248
+ {
249
+ return std::make_shared<facebook::react::NativeBubblSdkSpecJSI>(params);
250
+ }
251
+
252
+ @end
@@ -0,0 +1,335 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ /// The Swift side of the React Native module (BubblSdk.mm calls it): each function passes straight
5
+ /// to the iOS SDK (`Bubbl`, built into this pod), with JSON across the bridge in the same shapes as
6
+ /// the Android module and the Flutter wrapper:
7
+ /// - a message goes to JS as the public BubblMessage fields plus its contract JSON, and comes back
8
+ /// as that JSON alone (`BubblMessage(json:)`);
9
+ /// - events go as {"type": "notification.received", ...}, the SDK's own event names;
10
+ /// - options, properties, answers and diagnostics are JSON objects.
11
+ ///
12
+ /// The notification listener: while JS has one and the app is active, this takes the notification
13
+ /// over and hands it to JS ("notification.handle"), which shows it itself or calls present().
14
+ /// Otherwise Bubbl shows it as usual.
15
+ @objc(BubblSdkImpl)
16
+ public final class BubblSdkImpl: NSObject {
17
+ /// Sends JSON to JS on the module's one event (BubblSdkEvent); false when JS isn't listening.
18
+ private let emit: (String) -> Bool
19
+ private var eventToken: BubblEventToken?
20
+
21
+ /// How long JS has to claim a notification handed to it before Bubbl shows it.
22
+ static let claimTimeout: TimeInterval = 5
23
+
24
+ /// The instance whose JS set the app's notification listener (main thread only).
25
+ private static weak var owner: BubblSdkImpl?
26
+
27
+ /// Notifications handed to JS and not yet settled, by token (main thread only): JS claims one
28
+ /// when it arrives and finishes it once the app's listener has decided.
29
+ private var handedOver: [String: (message: BubblMessage, timeout: DispatchWorkItem)] = [:]
30
+ private var nextToken = 0
31
+
32
+ @objc public init(emit: @escaping (String) -> Bool) {
33
+ self.emit = emit
34
+ super.init()
35
+ eventToken = Bubbl.addEventListener { [weak self] event in
36
+ guard let self, let json = Self.event(event).flatMap(Self.json) else { return }
37
+ _ = self.emit(json)
38
+ }
39
+ }
40
+
41
+ /// The module is going away (a JS reload): stop listening, and show whatever JS was handed and
42
+ /// hadn't settled. The app's listener is cleared only if this instance set it.
43
+ @objc public func invalidate() {
44
+ if let eventToken { Bubbl.removeEventListener(eventToken) }
45
+ eventToken = nil
46
+ DispatchQueue.main.async { [self] in
47
+ for handover in handedOver.values {
48
+ handover.timeout.cancel()
49
+ Bubbl.present(handover.message)
50
+ }
51
+ handedOver.removeAll()
52
+ if Self.owner === self {
53
+ Self.owner = nil
54
+ Bubbl.setNotificationListener(nil)
55
+ }
56
+ }
57
+ }
58
+
59
+ // MARK: - Starting, consent and privacy
60
+
61
+ /// Bubbl works on this device (iOS 17 and later); below, every call does nothing.
62
+ @objc public var isSupported: Bool { Bubbl.isSupported }
63
+
64
+ /// A mistake (an empty key, an http:// URL) is logged by the SDK, never thrown.
65
+ @objc public func start(apiKey: String, optionsJson: String) {
66
+ Bubbl.start(apiKey: apiKey, options: Self.options(Self.object(optionsJson)))
67
+ }
68
+
69
+ /// The secret goes to the SDK and nowhere else: it arrives as its own argument (never inside
70
+ /// JSON), and the SDK logs an incomplete credential without it (BubblCredential's description
71
+ /// hides it).
72
+ @objc public func startWithCredential(keyId: String, secret: String, installId: String, optionsJson: String) {
73
+ Bubbl.start(
74
+ credential: BubblCredential(keyId: keyId, secret: secret, installId: installId),
75
+ options: Self.options(Self.object(optionsJson))
76
+ )
77
+ }
78
+
79
+ @objc public func stop() { Bubbl.stop() }
80
+ @objc public func setConsent(_ granted: Bool) { Bubbl.setConsent(granted) }
81
+ @objc public func optOut() { Bubbl.optOut() }
82
+ @objc public func deleteMyData() { Bubbl.deleteMyData() }
83
+ @objc public func setLocationEnabled(_ enabled: Bool) { Bubbl.setLocationEnabled(enabled) }
84
+
85
+ // MARK: - Targeting and events
86
+
87
+ @objc public func setSegments(_ segments: [String]) { Bubbl.setSegments(segments) }
88
+
89
+ @objc public func track(name: String, propertiesJson: String) {
90
+ Bubbl.track(name, properties: Self.plain(Self.object(propertiesJson)))
91
+ }
92
+
93
+ // MARK: - Notifications
94
+
95
+ @objc public func setNotificationListenerEnabled(_ enabled: Bool) {
96
+ DispatchQueue.main.async { [self] in
97
+ guard enabled else {
98
+ if Self.owner === self {
99
+ Self.owner = nil
100
+ Bubbl.setNotificationListener(nil)
101
+ }
102
+ return
103
+ }
104
+ Self.owner = self
105
+ Bubbl.setNotificationListener { @MainActor [weak self] (message: BubblMessage) -> Bool in
106
+ self?.handOver(message) ?? false
107
+ }
108
+ }
109
+ }
110
+
111
+ /// JS has the notification: the timeout that would show it is off.
112
+ @objc public func claimNotification(_ token: String) {
113
+ DispatchQueue.main.async { [self] in handedOver[token]?.timeout.cancel() }
114
+ }
115
+
116
+ /// The app's listener decided: Bubbl shows the notification unless the app does.
117
+ @objc public func finishNotification(_ token: String, appShowsIt: Bool) {
118
+ DispatchQueue.main.async { [self] in
119
+ guard let handover = handedOver.removeValue(forKey: token) else { return }
120
+ handover.timeout.cancel()
121
+ if !appShowsIt { Bubbl.present(handover.message) }
122
+ }
123
+ }
124
+
125
+ @objc public func present(_ json: String) { withMessage(json, "present") { Bubbl.present($0) } }
126
+ @objc public func reportDisplayed(_ json: String) { withMessage(json, "reportDisplayed") { Bubbl.reportDisplayed($0) } }
127
+ @objc public func reportOpened(_ json: String) { withMessage(json, "reportOpened") { Bubbl.reportOpened($0) } }
128
+ @objc public func reportCtaClicked(_ json: String) { withMessage(json, "reportCtaClicked") { Bubbl.reportCtaClicked($0) } }
129
+ @objc public func reportDismissed(_ json: String) { withMessage(json, "reportDismissed") { Bubbl.reportDismissed($0) } }
130
+ @objc public func reportMediaViewed(_ json: String) { withMessage(json, "reportMediaViewed") { Bubbl.reportMediaViewed($0) } }
131
+ @objc public func reportSurveyStarted(_ json: String) { withMessage(json, "reportSurveyStarted") { Bubbl.reportSurveyStarted($0) } }
132
+ @objc public func openCta(_ json: String) { withMessage(json, "openCta") { Bubbl.openCta($0) } }
133
+
134
+ @objc public func reportMediaCompleted(_ json: String, durationSeconds: Double) {
135
+ withMessage(json, "reportMediaCompleted") { Bubbl.reportMediaCompleted($0, durationSeconds: durationSeconds) }
136
+ }
137
+
138
+ @objc public func submitSurvey(_ json: String, answersJson: String) -> Bool {
139
+ guard let message = BubblMessage(json: json) else {
140
+ NSLog("[Bubbl] Bubbl.submitSurvey: that isn't a Bubbl message's json")
141
+ return false
142
+ }
143
+ return Bubbl.submitSurvey(message, answers: Self.plain(Self.object(answersJson)))
144
+ }
145
+
146
+ // MARK: - Permissions
147
+
148
+ /// Where the device stands, as JSON; nil before start.
149
+ @objc public func permissionStatus() -> String? {
150
+ Bubbl.permissions.status().flatMap { Self.json(Self.permissions($0)) }
151
+ }
152
+
153
+ /// The privacy view if the dashboard says so, then the system prompt; the status after, on the
154
+ /// main thread (nil before start).
155
+ @objc public func requestNotifications(_ completion: @escaping (String?) -> Void) {
156
+ Bubbl.permissions.requestNotifications { status in
157
+ completion(status.flatMap { Self.json(Self.permissions($0)) })
158
+ }
159
+ }
160
+
161
+ /// The same for location; with `always`, the second step to "Always" too.
162
+ @objc public func requestLocation(always: Bool, completion: @escaping (String?) -> Void) {
163
+ Bubbl.permissions.requestLocation(always: always) { status in
164
+ completion(status.flatMap { Self.json(Self.permissions($0)) })
165
+ }
166
+ }
167
+
168
+ @objc public func openSettings() { Bubbl.permissions.openSettings() }
169
+
170
+ // MARK: - Sandbox
171
+
172
+ /// Approve this device as a Sandbox test device with the dashboard's code: {approved, message}
173
+ /// as JSON, on the main thread.
174
+ @objc public func registerTestDevice(_ code: String, completion: @escaping (String) -> Void) {
175
+ Bubbl.registerTestDevice(code) { result in
176
+ completion(Self.json(["approved": result.approved, "message": result.message ?? NSNull()]) ?? "{\"approved\":false}")
177
+ }
178
+ }
179
+
180
+ // MARK: - Diagnostics
181
+
182
+ /// Bubbl.Diagnostics as JSON, on the main thread.
183
+ @objc public func diagnostics(_ completion: @escaping (String) -> Void) {
184
+ Bubbl.diagnostics { diagnostics in completion(Self.json(Self.diagnostics(diagnostics)) ?? "{}") }
185
+ }
186
+
187
+ // MARK: -
188
+
189
+ private func withMessage(_ json: String, _ call: String, _ action: (BubblMessage) -> Void) {
190
+ guard let message = BubblMessage(json: json) else {
191
+ NSLog("[Bubbl] Bubbl.%@: that isn't a Bubbl message's json", call)
192
+ return
193
+ }
194
+ action(message)
195
+ }
196
+
197
+ /// The SDK asks, on the main thread, whether the app shows `message` itself: yes while the app
198
+ /// is active and JS is listening, and JS decides; no otherwise. Handed over, it's JS's to settle;
199
+ /// if JS doesn't claim it in time (reloading, stuck), Bubbl shows it: it's never lost.
200
+ @MainActor
201
+ private func handOver(_ message: BubblMessage) -> Bool {
202
+ nextToken += 1
203
+ let token = String(nextToken)
204
+ guard UIApplication.shared.applicationState == .active,
205
+ let json = Self.json(["type": "notification.handle", "token": token, "message": Self.message(message)])
206
+ else { return false }
207
+ let timeout = DispatchWorkItem { [weak self] in
208
+ if let handover = self?.handedOver.removeValue(forKey: token) { Bubbl.present(handover.message) }
209
+ }
210
+ handedOver[token] = (message, timeout)
211
+ guard emit(json) else {
212
+ handedOver[token] = nil
213
+ return false
214
+ }
215
+ DispatchQueue.main.asyncAfter(deadline: .now() + Self.claimTimeout, execute: timeout)
216
+ return true
217
+ }
218
+
219
+ // MARK: - JSON (the same shapes as android/…/BubblJson.kt)
220
+
221
+ static func object(_ text: String) -> [String: Any] {
222
+ guard let data = text.data(using: .utf8),
223
+ let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
224
+ else { return [:] }
225
+ return object
226
+ }
227
+
228
+ /// JSON nulls as nil, for the SDK's `[String: Any?]` parameters.
229
+ static func plain(_ object: [String: Any]) -> [String: Any?] {
230
+ object.mapValues { value -> Any? in value is NSNull ? nil : value }
231
+ }
232
+
233
+ static func json(_ object: [String: Any]) -> String? {
234
+ guard let data = try? JSONSerialization.data(withJSONObject: object) else { return nil }
235
+ return String(data: data, encoding: .utf8)
236
+ }
237
+
238
+ static func options(_ json: [String: Any]) -> BubblOptions {
239
+ let name = json["logLevel"] as? String
240
+ return BubblOptions(
241
+ baseUrl: json["baseUrl"] as? String ?? "",
242
+ requireConsent: json["requireConsent"] as? Bool ?? false,
243
+ logLevel: BubblOptions.LogLevel.allCases.first { "\($0)" == name } ?? .warning,
244
+ segments: json["segments"] as? [String]
245
+ )
246
+ }
247
+
248
+ /// The public BubblMessage for JS: its fields as the SDK works them out, and its JSON.
249
+ static func message(_ message: BubblMessage) -> [String: Any] {
250
+ [
251
+ "id": message.id,
252
+ "headline": message.headline,
253
+ "body": message.body,
254
+ "isSurvey": message.isSurvey,
255
+ "isSandbox": message.isSandbox,
256
+ "mediaType": message.mediaType ?? NSNull(),
257
+ "mediaUrl": message.mediaUrl ?? NSNull(),
258
+ "mediaThumbnailUrl": message.mediaThumbnailUrl ?? NSNull(),
259
+ "ctaLabel": message.ctaLabel ?? NSNull(),
260
+ "ctaUrl": message.ctaUrl ?? NSNull(),
261
+ "questions": message.questions.map { question -> [String: Any] in
262
+ [
263
+ "id": question.id,
264
+ "text": question.text,
265
+ // The case name (openEnded, singleChoice…), as the TS type names them.
266
+ "type": "\(question.type)",
267
+ "required": question.required,
268
+ "choices": question.choices.map { ["id": $0.id, "text": $0.text] },
269
+ ]
270
+ },
271
+ "json": message.json,
272
+ ]
273
+ }
274
+
275
+ static func event(_ event: BubblEvent) -> [String: Any]? {
276
+ func with(_ type: String, _ message: BubblMessage) -> [String: Any] { ["type": type, "message": Self.message(message)] }
277
+ switch event {
278
+ case .notificationReceived(let message): return with("notification.received", message)
279
+ case .notificationDisplayed(let message): return with("notification.displayed", message)
280
+ case .notificationOpened(let message): return with("notification.opened", message)
281
+ case .notificationCtaClicked(let message): return with("notification.cta_clicked", message)
282
+ case .notificationDismissed(let message): return with("notification.dismissed", message)
283
+ case .mediaViewed(let message): return with("media.viewed", message)
284
+ case .mediaCompleted(let message): return with("media.completed", message)
285
+ case .surveyStarted(let message): return with("survey.started", message)
286
+ case .surveySubmitted(let message): return with("survey.submitted", message)
287
+ case .geofenceEntered(let locationId): return ["type": "geofence.entered", "locationId": locationId]
288
+ case .geofenceExited(let locationId): return ["type": "geofence.exited", "locationId": locationId]
289
+ case .error(let message): return ["type": "error", "message": message]
290
+ case .credentialRejected: return ["type": "credential.rejected"]
291
+ }
292
+ }
293
+
294
+ /// {notifications, location, preciseLocation}, with the case names (notDetermined, whenInUse…)
295
+ /// the TS type and the Android module use.
296
+ static func permissions(_ status: BubblPermissionStatus) -> [String: Any] {
297
+ [
298
+ "notifications": "\(status.notifications)",
299
+ "location": "\(status.location)",
300
+ "preciseLocation": status.preciseLocation,
301
+ ]
302
+ }
303
+
304
+ /// The same fields as the Android module sends; Android's own extras are null here.
305
+ static func diagnostics(_ diagnostics: Bubbl.Diagnostics) -> [String: Any] {
306
+ [
307
+ "started": diagnostics.started,
308
+ "sdkVersion": diagnostics.sdkVersion,
309
+ "supported": diagnostics.supported,
310
+ "sdkSupported": diagnostics.sdkSupported,
311
+ "registered": diagnostics.registered,
312
+ "installId": diagnostics.installId ?? NSNull(),
313
+ "active": diagnostics.active,
314
+ "pausedUntil": diagnostics.pausedUntil.map { NSNumber(value: $0) } ?? NSNull(),
315
+ "consentRequired": diagnostics.consentRequired,
316
+ "consent": diagnostics.consent.map { NSNumber(value: $0) } ?? NSNull(),
317
+ "locationEnabled": diagnostics.locationEnabled,
318
+ "permissions": diagnostics.permissions.map(Self.permissions) ?? NSNull(),
319
+ "hasPushToken": diagnostics.hasPushToken,
320
+ "geofences": diagnostics.geofences,
321
+ "queuedEvents": diagnostics.queuedEvents,
322
+ "lastError": diagnostics.lastError ?? NSNull(),
323
+ "credentialRejected": diagnostics.credentialRejected,
324
+ "environment": diagnostics.environment ?? NSNull(),
325
+ "testDevice": diagnostics.testDevice.map { ["status": $0.status, "code": $0.code ?? NSNull()] as [String: Any] } ?? NSNull(),
326
+ "backgroundLocation": diagnostics.backgroundLocation,
327
+ "transitionsDroppedWhileLocked": diagnostics.transitionsDroppedWhileLocked,
328
+ "backgroundRestricted": NSNull(),
329
+ "batteryOptimized": NSNull(),
330
+ "backgroundRefreshAvailable": diagnostics.backgroundRefreshAvailable,
331
+ "lowPowerMode": diagnostics.lowPowerMode,
332
+ "osWatchesGeofences": diagnostics.osWatchesGeofences,
333
+ ]
334
+ }
335
+ }
@@ -0,0 +1,44 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ # Bubbl's pictures on pushes, for the app's Notification Service Extension target (optional): the
6
+ # iOS SDK's BubblNotificationService and the plain-Swift core it reads pushes with, copied into
7
+ # Sources/ by scripts/sync-ios-sources.mjs. No React Native in it: an extension is its own small
8
+ # process. In the Podfile, for the extension's target:
9
+ #
10
+ # target 'NotificationService' do
11
+ # pod 'BubblReactNativeNotificationService', :path => '../node_modules/@bubblsdk/react-native-sdk/ios-extension'
12
+ # end
13
+ #
14
+ # and the extension's principal class:
15
+ #
16
+ # import BubblNotificationService
17
+ # class NotificationService: BubblNotificationService {}
18
+ #
19
+ # Its own folder (not the package root), so React Native's autolinking never adds it to the app.
20
+ Pod::Spec.new do |s|
21
+ s.name = 'BubblReactNativeNotificationService'
22
+ # Imported as the iOS SDK's own product is: `import BubblNotificationService`.
23
+ s.module_name = 'BubblNotificationService'
24
+ s.version = package['version']
25
+ s.summary = "Pictures on Bubbl's pushes, for a React Native app's Notification Service Extension."
26
+ s.homepage = 'https://bubbl.tech'
27
+ s.license = { :type => 'Apache-2.0' }
28
+ s.author = { 'Bubbl' => 'engineering@bubbl.tech' }
29
+ s.source = { :git => 'https://github.com/bubbl-platform/renewed-sdk.git', :tag => "v#{s.version}" }
30
+
31
+ # The service shows pictures on iOS 17 and up and passes pushes through untouched below.
32
+ s.platforms = { :ios => defined?(min_ios_version_supported) ? min_ios_version_supported : '13.0' }
33
+ s.swift_version = '5.9'
34
+ s.source_files = 'Sources/**/*.swift'
35
+ s.frameworks = 'UserNotifications'
36
+ # iOS 14 and up (used only on 17 and up): weak, so an older iOS still loads the extension.
37
+ s.weak_frameworks = 'UniformTypeIdentifiers'
38
+ s.pod_target_xcconfig = {
39
+ 'DEFINES_MODULE' => 'YES',
40
+ 'APPLICATION_EXTENSION_API_ONLY' => 'YES',
41
+ # The core's `package` declarations, built here into one module with the service.
42
+ 'OTHER_SWIFT_FLAGS' => '$(inherited) -package-name BubblReactNativeNotificationService',
43
+ }
44
+ end