@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,39 @@
1
+ /// A lock for async code: one caller at a time runs the body, the rest wait their turn (in
2
+ /// order) without blocking a thread. An actor alone isn't enough, since an actor lets other
3
+ /// calls in whenever the running one awaits; registering the install must not interleave.
4
+ package actor AsyncMutex {
5
+ private var locked = false
6
+ private var waiters: [CheckedContinuation<Void, Never>] = []
7
+
8
+ package init() {}
9
+
10
+ /// Runs `body` holding the lock, and releases it however `body` ends.
11
+ package nonisolated func withLock<T: Sendable>(_ body: @Sendable () async throws -> T) async rethrows -> T {
12
+ await lock()
13
+ do {
14
+ let result = try await body()
15
+ await unlock()
16
+ return result
17
+ } catch {
18
+ await unlock()
19
+ throw error
20
+ }
21
+ }
22
+
23
+ private func lock() async {
24
+ guard locked else {
25
+ locked = true
26
+ return
27
+ }
28
+ await withCheckedContinuation { waiters.append($0) }
29
+ }
30
+
31
+ private func unlock() {
32
+ if waiters.isEmpty {
33
+ locked = false
34
+ } else {
35
+ // The lock passes straight to the next waiter: it stays held.
36
+ waiters.removeFirst().resume()
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,52 @@
1
+ import Foundation
2
+
3
+ /// How much the SDK says (BubblOptions.logLevel), most important first.
4
+ package enum LogSeverity: Int, Sendable, Comparable, CaseIterable {
5
+ case error, warning, info, debug
6
+
7
+ package static func < (lhs: LogSeverity, rhs: LogSeverity) -> Bool { lhs.rawValue < rhs.rawValue }
8
+ }
9
+
10
+ /// The SDK's log, as Android's BubblLog: one place the core reports whatever it drops or holds
11
+ /// back (a question type a newer dashboard added, a refused event, a batch the server rejected),
12
+ /// so a developer can see why. Silent until the iOS layer points it at os.Logger and sets the
13
+ /// level BubblOptions asks for; a message below that level is never even built.
14
+ ///
15
+ /// **What a message may say.** It can end up in Console and sysdiagnose for anyone holding the
16
+ /// device, so: ids, types, counts, codes and statuses only. Never coordinates or fixes, push
17
+ /// tokens, the credential or its secret, survey answers, or the app's own event properties.
18
+ package enum BubblLog {
19
+ private static let lock = NSLock()
20
+ nonisolated(unsafe) private static var sink: @Sendable (LogSeverity, String) -> Void = { _, _ in }
21
+ nonisolated(unsafe) private static var level: LogSeverity = .warning
22
+ nonisolated(unsafe) private static var lastErrorMessage: String?
23
+
24
+ /// Where messages go from now on (the iOS layer, or a test), and from which level up.
25
+ package static func setSink(level: LogSeverity = .debug, _ sink: @escaping @Sendable (LogSeverity, String) -> Void) {
26
+ lock.sync {
27
+ self.sink = sink
28
+ self.level = level
29
+ }
30
+ }
31
+
32
+ /// The level messages are kept from (BubblOptions.logLevel).
33
+ package static func setLevel(_ level: LogSeverity) {
34
+ lock.sync { self.level = level }
35
+ }
36
+
37
+ /// The last error logged, for diagnostics (kept whatever the level).
38
+ package static var lastError: String? { lock.sync { lastErrorMessage } }
39
+
40
+ package static func error(_ message: @autoclosure () -> String) { log(.error, message) }
41
+ package static func warning(_ message: @autoclosure () -> String) { log(.warning, message) }
42
+ package static func info(_ message: @autoclosure () -> String) { log(.info, message) }
43
+ package static func debug(_ message: @autoclosure () -> String) { log(.debug, message) }
44
+
45
+ private static func log(_ level: LogSeverity, _ message: () -> String) {
46
+ let (sink, wanted) = lock.sync { (self.sink, level <= self.level) }
47
+ guard wanted || level == .error else { return }
48
+ let text = message()
49
+ if level == .error { lock.sync { lastErrorMessage = text } }
50
+ if wanted { sink(level, text) }
51
+ }
52
+ }
@@ -0,0 +1,31 @@
1
+ import Foundation
2
+
3
+ /// JSON as the device API speaks it, read loosely: a body that isn't the expected shape is nil
4
+ /// rather than an error, since a proxy's error page or an empty 304 must not throw.
5
+ package enum JSON {
6
+ /// The object in `text`, or nil when it isn't a JSON object.
7
+ package static func object(_ text: String) -> [String: Any]? {
8
+ guard let data = text.data(using: .utf8) else { return nil }
9
+ return (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
10
+ }
11
+
12
+ /// A JSON number as Int64, however the platform's JSONSerialization boxed it (NSNumber on
13
+ /// Apple platforms, Swift numbers elsewhere); nil when it isn't a number.
14
+ package static func int64(_ value: Any?) -> Int64? {
15
+ switch value {
16
+ case let value as Int: Int64(value)
17
+ case let value as Int64: value
18
+ case let value as Double: Int64(exactly: value.rounded(.towardZero))
19
+ case let value as NSNumber: value.int64Value
20
+ default: nil
21
+ }
22
+ }
23
+
24
+ /// `value` (a dictionary or array of JSON values) as compact JSON text.
25
+ package static func string(_ value: Any) -> String {
26
+ guard JSONSerialization.isValidJSONObject(value),
27
+ let data = try? JSONSerialization.data(withJSONObject: value, options: [.sortedKeys])
28
+ else { return "{}" }
29
+ return String(decoding: data, as: UTF8.self)
30
+ }
31
+ }
@@ -0,0 +1,94 @@
1
+ /// Any JSON value, as Codable: for the parts of the device API that carry free-form JSON (an
2
+ /// event's `data`, a rejection's `errors`). JSONEncoder and JSONDecoder read and write it the same
3
+ /// way on every platform the core runs on, unlike JSONSerialization's boxed numbers.
4
+ package enum JSONValue: Sendable, Hashable, Codable {
5
+ case null
6
+ case bool(Bool)
7
+ case int(Int64)
8
+ case double(Double)
9
+ case string(String)
10
+ case array([JSONValue])
11
+ case object([String: JSONValue])
12
+
13
+ package init(from decoder: any Decoder) throws {
14
+ let container = try decoder.singleValueContainer()
15
+ if container.decodeNil() {
16
+ self = .null
17
+ } else if let value = try? container.decode(Bool.self) {
18
+ self = .bool(value)
19
+ } else if let value = try? container.decode(Int64.self) {
20
+ self = .int(value)
21
+ } else if let value = try? container.decode(Double.self) {
22
+ self = .double(value)
23
+ } else if let value = try? container.decode(String.self) {
24
+ self = .string(value)
25
+ } else if let value = try? container.decode([JSONValue].self) {
26
+ self = .array(value)
27
+ } else {
28
+ self = .object(try container.decode([String: JSONValue].self))
29
+ }
30
+ }
31
+
32
+ package func encode(to encoder: any Encoder) throws {
33
+ var container = encoder.singleValueContainer()
34
+ switch self {
35
+ case .null: try container.encodeNil()
36
+ case .bool(let value): try container.encode(value)
37
+ case .int(let value): try container.encode(value)
38
+ case .double(let value): try container.encode(value)
39
+ case .string(let value): try container.encode(value)
40
+ case .array(let value): try container.encode(value)
41
+ case .object(let value): try container.encode(value)
42
+ }
43
+ }
44
+
45
+ /// The value at `key`, when this is an object.
46
+ package subscript(key: String) -> JSONValue? {
47
+ if case .object(let object) = self { object[key] } else { nil }
48
+ }
49
+
50
+ package var stringValue: String? {
51
+ if case .string(let value) = self { value } else { nil }
52
+ }
53
+
54
+ /// A number as a whole Int64, never trapping: fractions are cut, and values beyond Int64
55
+ /// (1e30 from a server) are clamped. Nil for anything that isn't a finite number.
56
+ package var int64: Int64? {
57
+ switch self {
58
+ case .int(let value): return value
59
+ case .double(let value):
60
+ guard value.isFinite else { return nil }
61
+ let whole = value.rounded(.towardZero)
62
+ if let exact = Int64(exactly: whole) { return exact }
63
+ return whole > 0 ? .max : .min
64
+ default: return nil
65
+ }
66
+ }
67
+
68
+ /// int64, clamped to `range`: for a server number that sets a limit or an interval.
69
+ package func int64(in range: ClosedRange<Int64>) -> Int64? {
70
+ int64.map { min(max($0, range.lowerBound), range.upperBound) }
71
+ }
72
+
73
+ /// Whether every number in it is finite: JSON has no NaN or infinity, so JSONEncoder refuses
74
+ /// them (Android's JSONObject does too).
75
+ package var isEncodable: Bool {
76
+ switch self {
77
+ case .double(let value): value.isFinite
78
+ case .array(let values): values.allSatisfy(\.isEncodable)
79
+ case .object(let values): values.values.allSatisfy(\.isEncodable)
80
+ case .null, .bool, .int, .string: true
81
+ }
82
+ }
83
+ }
84
+
85
+ extension JSONValue: ExpressibleByStringLiteral, ExpressibleByIntegerLiteral, ExpressibleByBooleanLiteral,
86
+ ExpressibleByFloatLiteral, ExpressibleByDictionaryLiteral, ExpressibleByArrayLiteral, ExpressibleByNilLiteral {
87
+ package init(stringLiteral value: String) { self = .string(value) }
88
+ package init(integerLiteral value: Int64) { self = .int(value) }
89
+ package init(booleanLiteral value: Bool) { self = .bool(value) }
90
+ package init(floatLiteral value: Double) { self = .double(value) }
91
+ package init(dictionaryLiteral elements: (String, JSONValue)...) { self = .object(Dictionary(uniqueKeysWithValues: elements)) }
92
+ package init(arrayLiteral elements: JSONValue...) { self = .array(elements) }
93
+ package init(nilLiteral: ()) { self = .null }
94
+ }
@@ -0,0 +1,11 @@
1
+ import Foundation
2
+
3
+ extension NSLock {
4
+ /// Runs `body` holding the lock. (Foundation's own `withLock` isn't on every platform the
5
+ /// core builds for.)
6
+ package func sync<T>(_ body: () throws -> T) rethrows -> T {
7
+ lock()
8
+ defer { unlock() }
9
+ return try body()
10
+ }
11
+ }
@@ -0,0 +1,109 @@
1
+ import Foundation
2
+
3
+ /// A small piece of the engine's state kept between launches (what's been shown, the config, the
4
+ /// device's segments…). `load` is nil when nothing has been saved yet.
5
+ package protocol ValueStore<Value>: Sendable {
6
+ associatedtype Value: Codable & Sendable
7
+ func load() throws -> Value?
8
+ func save(_ value: Value) throws
9
+ func delete() throws
10
+ }
11
+
12
+ /// The file couldn't be read or written, so it did neither. Before the first unlock after a reboot
13
+ /// iOS keeps protected files closed; an unreadable file is never taken for "nothing saved".
14
+ package struct ValueStoreUnavailable: Error {
15
+ package let underlying: any Error
16
+ }
17
+
18
+ /// A value read once and then kept in memory, for state that's asked for often. Only a successful
19
+ /// read is kept: one that failed (the file locked) is tried again next time.
20
+ package final class Cached<Value: Sendable>: @unchecked Sendable {
21
+ private let lock = NSLock()
22
+ private var loaded = false
23
+ private var stored: Value?
24
+
25
+ package init() {}
26
+
27
+ /// The kept value, or `load`'s result (kept) the first time.
28
+ package func value(_ load: () throws -> Value?) rethrows -> Value? {
29
+ try lock.sync {
30
+ if loaded { return stored }
31
+ let value = try load()
32
+ stored = value
33
+ loaded = true
34
+ return value
35
+ }
36
+ }
37
+
38
+ package func set(_ value: Value?) {
39
+ lock.sync {
40
+ stored = value
41
+ loaded = true
42
+ }
43
+ }
44
+ }
45
+
46
+ /// A ValueStore in memory: for tests.
47
+ package final class InMemoryValueStore<Value: Codable & Sendable>: ValueStore, @unchecked Sendable {
48
+ private let lock = NSLock()
49
+ private var value: Value?
50
+
51
+ package init(_ value: Value? = nil) {
52
+ self.value = value
53
+ }
54
+
55
+ package func load() -> Value? { lock.sync { value } }
56
+ package func save(_ value: Value) { lock.sync { self.value = value } }
57
+ package func delete() { lock.sync { value = nil } }
58
+ }
59
+
60
+ /// A ValueStore as one JSON file, written atomically (on iOS, with file protection). A missing
61
+ /// file is nothing saved, and so is one that reads but doesn't decode (from an older build, or
62
+ /// corrupt); one that can't be read at all throws ValueStoreUnavailable.
63
+ package final class FileValueStore<Value: Codable & Sendable>: ValueStore, @unchecked Sendable {
64
+ private let lock = NSLock()
65
+ private let url: URL
66
+ private let writeOptions: Data.WritingOptions
67
+
68
+ /// - Parameter writeOptions: added to `.atomic`; on iOS, the file protection class.
69
+ package init(url: URL, writeOptions: Data.WritingOptions = []) {
70
+ self.url = url
71
+ self.writeOptions = writeOptions.union(.atomic)
72
+ }
73
+
74
+ package func load() throws -> Value? {
75
+ try lock.sync {
76
+ guard FileManager.default.fileExists(atPath: url.path) else { return nil }
77
+ let data: Data
78
+ do {
79
+ data = try Data(contentsOf: url)
80
+ } catch {
81
+ throw ValueStoreUnavailable(underlying: error)
82
+ }
83
+ return try? JSONDecoder().decode(Value.self, from: data)
84
+ }
85
+ }
86
+
87
+ package func save(_ value: Value) throws {
88
+ try lock.sync {
89
+ let data = try JSONEncoder().encode(value)
90
+ do {
91
+ try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
92
+ try data.write(to: url, options: writeOptions)
93
+ } catch {
94
+ throw ValueStoreUnavailable(underlying: error)
95
+ }
96
+ }
97
+ }
98
+
99
+ package func delete() throws {
100
+ try lock.sync {
101
+ guard FileManager.default.fileExists(atPath: url.path) else { return }
102
+ do {
103
+ try FileManager.default.removeItem(at: url)
104
+ } catch {
105
+ throw ValueStoreUnavailable(underlying: error)
106
+ }
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,45 @@
1
+ #import "BubblLaunch.h"
2
+ #import <Foundation/Foundation.h>
3
+ #if TARGET_OS_IOS
4
+ #import <UIKit/UIKit.h>
5
+ #endif
6
+
7
+ // Bubbl's own start at launch, with no app code (as Android's App Startup). Swift has no +load, so
8
+ // this Objective-C class registers, before main(), for the end of every launch. There it asks the
9
+ // Swift side (BubblLaunchHook, found by name, since this target can't import the one that depends
10
+ // on it) to start Bubbl again if an earlier launch started it and it wasn't stopped. So an app that
11
+ // iOS launches for a geofence, a push or a tap has Bubbl running before a wrapper's JavaScript or
12
+ // Dart could call start.
13
+ @interface BubblLaunchLoader : NSObject
14
+ @end
15
+
16
+ @implementation BubblLaunchLoader
17
+
18
+ + (void)load {
19
+ #if TARGET_OS_IOS
20
+ // Below iOS 17 Bubbl does nothing (Bubbl.isSupported): no background task, no start at launch.
21
+ if (@available(iOS 17.0, *)) {} else { return; }
22
+
23
+ // Background refresh's handler must be registered before launch finishes: now, before main().
24
+ Class hook = NSClassFromString(@"BubblLaunchHook");
25
+ SEL registerTasks = NSSelectorFromString(@"registerBackgroundTasks");
26
+ if ([hook respondsToSelector:registerTasks]) {
27
+ ((void (*)(id, SEL))[hook methodForSelector:registerTasks])(hook, registerTasks);
28
+ }
29
+
30
+ [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification
31
+ object:nil
32
+ queue:nil
33
+ usingBlock:^(NSNotification *notification) {
34
+ Class hook = NSClassFromString(@"BubblLaunchHook");
35
+ SEL launched = NSSelectorFromString(@"applicationDidFinishLaunching");
36
+ if ([hook respondsToSelector:launched]) {
37
+ ((void (*)(id, SEL))[hook methodForSelector:launched])(hook, launched);
38
+ }
39
+ }];
40
+ #endif
41
+ }
42
+
43
+ @end
44
+
45
+ void BubblLaunchLinked(void) {}
@@ -0,0 +1,5 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ /// Called by the Swift side so the linker keeps this target's object file, and with it the class
4
+ /// whose +load watches for the end of launch (a static library's unreferenced files are dropped).
5
+ FOUNDATION_EXPORT void BubblLaunchLinked(void);