@bubblsdk/react-native-sdk 4.1.7 → 5.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BubblReactNativeSdk.podspec +29 -9
- package/CHANGELOG.md +52 -1
- package/LICENSE +202 -1
- package/README.md +172 -83
- package/android/build.gradle +33 -30
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/tech/bubbl/reactnative/BubblJson.kt +135 -0
- package/android/src/main/java/tech/bubbl/reactnative/BubblSdkModule.kt +277 -0
- package/android/src/main/java/tech/bubbl/reactnative/BubblSdkPackage.kt +26 -0
- package/app.plugin.js +2 -0
- package/ios/BubblPush.swift +44 -0
- package/ios/BubblSdk.h +7 -0
- package/ios/BubblSdk.mm +252 -0
- package/ios/BubblSdkImpl.swift +335 -0
- package/ios-extension/BubblReactNativeNotificationService.podspec +44 -0
- package/ios-extension/Sources/BubblCore/API/ApiResponse.swift +90 -0
- package/ios-extension/Sources/BubblCore/API/CredentialStore.swift +116 -0
- package/ios-extension/Sources/BubblCore/API/DeviceApiClient.swift +239 -0
- package/ios-extension/Sources/BubblCore/API/ErrorActions.swift +68 -0
- package/ios-extension/Sources/BubblCore/API/Http.swift +39 -0
- package/ios-extension/Sources/BubblCore/API/ServerClock.swift +69 -0
- package/ios-extension/Sources/BubblCore/BubblVersion.swift +5 -0
- package/ios-extension/Sources/BubblCore/Device/ConfigSync.swift +216 -0
- package/ios-extension/Sources/BubblCore/Device/DeviceSync.swift +160 -0
- package/ios-extension/Sources/BubblCore/Device/ProvisioningProfile.swift +24 -0
- package/ios-extension/Sources/BubblCore/Engine/EngineCore.swift +523 -0
- package/ios-extension/Sources/BubblCore/Engine/Geofencing.swift +170 -0
- package/ios-extension/Sources/BubblCore/Engine/Permissions.swift +90 -0
- package/ios-extension/Sources/BubblCore/Engine/Privacy.swift +64 -0
- package/ios-extension/Sources/BubblCore/Engine/PrivacyControls.swift +184 -0
- package/ios-extension/Sources/BubblCore/Engine/Pushes.swift +61 -0
- package/ios-extension/Sources/BubblCore/Engine/Sandbox.swift +149 -0
- package/ios-extension/Sources/BubblCore/Events/EventQueue.swift +207 -0
- package/ios-extension/Sources/BubblCore/Events/EventStore.swift +159 -0
- package/ios-extension/Sources/BubblCore/Geofence/Geofence.swift +160 -0
- package/ios-extension/Sources/BubblCore/Geofence/GeofenceEngine.swift +488 -0
- package/ios-extension/Sources/BubblCore/Geofence/GeofenceState.swift +103 -0
- package/ios-extension/Sources/BubblCore/Geofence/RegionBudget.swift +62 -0
- package/ios-extension/Sources/BubblCore/Notification/BubblNotification.swift +137 -0
- package/ios-extension/Sources/BubblCore/Notification/NotificationEvents.swift +160 -0
- package/ios-extension/Sources/BubblCore/Notification/Push.swift +134 -0
- package/ios-extension/Sources/BubblCore/Notification/SurveyForm.swift +185 -0
- package/ios-extension/Sources/BubblCore/RequestSigner.swift +56 -0
- package/ios-extension/Sources/BubblCore/Support/AsyncMutex.swift +39 -0
- package/ios-extension/Sources/BubblCore/Support/BubblLog.swift +52 -0
- package/ios-extension/Sources/BubblCore/Support/JSON.swift +31 -0
- package/ios-extension/Sources/BubblCore/Support/JSONValue.swift +94 -0
- package/ios-extension/Sources/BubblCore/Support/Locking.swift +11 -0
- package/ios-extension/Sources/BubblCore/Support/ValueStore.swift +109 -0
- package/ios-extension/Sources/BubblNotificationService/BubblNotificationService.swift +124 -0
- package/ios-sdk/BubblCore/API/ApiResponse.swift +90 -0
- package/ios-sdk/BubblCore/API/CredentialStore.swift +116 -0
- package/ios-sdk/BubblCore/API/DeviceApiClient.swift +239 -0
- package/ios-sdk/BubblCore/API/ErrorActions.swift +68 -0
- package/ios-sdk/BubblCore/API/Http.swift +39 -0
- package/ios-sdk/BubblCore/API/ServerClock.swift +69 -0
- package/ios-sdk/BubblCore/BubblVersion.swift +5 -0
- package/ios-sdk/BubblCore/Device/ConfigSync.swift +216 -0
- package/ios-sdk/BubblCore/Device/DeviceSync.swift +160 -0
- package/ios-sdk/BubblCore/Device/ProvisioningProfile.swift +24 -0
- package/ios-sdk/BubblCore/Engine/EngineCore.swift +523 -0
- package/ios-sdk/BubblCore/Engine/Geofencing.swift +170 -0
- package/ios-sdk/BubblCore/Engine/Permissions.swift +90 -0
- package/ios-sdk/BubblCore/Engine/Privacy.swift +64 -0
- package/ios-sdk/BubblCore/Engine/PrivacyControls.swift +184 -0
- package/ios-sdk/BubblCore/Engine/Pushes.swift +61 -0
- package/ios-sdk/BubblCore/Engine/Sandbox.swift +149 -0
- package/ios-sdk/BubblCore/Events/EventQueue.swift +207 -0
- package/ios-sdk/BubblCore/Events/EventStore.swift +159 -0
- package/ios-sdk/BubblCore/Geofence/Geofence.swift +160 -0
- package/ios-sdk/BubblCore/Geofence/GeofenceEngine.swift +488 -0
- package/ios-sdk/BubblCore/Geofence/GeofenceState.swift +103 -0
- package/ios-sdk/BubblCore/Geofence/RegionBudget.swift +62 -0
- package/ios-sdk/BubblCore/Notification/BubblNotification.swift +137 -0
- package/ios-sdk/BubblCore/Notification/NotificationEvents.swift +160 -0
- package/ios-sdk/BubblCore/Notification/Push.swift +134 -0
- package/ios-sdk/BubblCore/Notification/SurveyForm.swift +185 -0
- package/ios-sdk/BubblCore/RequestSigner.swift +56 -0
- package/ios-sdk/BubblCore/Support/AsyncMutex.swift +39 -0
- package/ios-sdk/BubblCore/Support/BubblLog.swift +52 -0
- package/ios-sdk/BubblCore/Support/JSON.swift +31 -0
- package/ios-sdk/BubblCore/Support/JSONValue.swift +94 -0
- package/ios-sdk/BubblCore/Support/Locking.swift +11 -0
- package/ios-sdk/BubblCore/Support/ValueStore.swift +109 -0
- package/ios-sdk/BubblLaunch/BubblLaunch.m +45 -0
- package/ios-sdk/BubblLaunch/include/BubblLaunch.h +5 -0
- package/ios-sdk/BubblSDK/Bubbl.swift +308 -0
- package/ios-sdk/BubblSDK/BubblBackend.swift +118 -0
- package/ios-sdk/BubblSDK/BubblCredential.swift +30 -0
- package/ios-sdk/BubblSDK/BubblMessage.swift +151 -0
- package/ios-sdk/BubblSDK/BubblOptions.swift +39 -0
- package/ios-sdk/BubblSDK/BubblPermissions.swift +79 -0
- package/ios-sdk/BubblSDK/BubblTestDeviceResult.swift +10 -0
- package/ios-sdk/BubblSDK/Engine/BackgroundRefresh.swift +92 -0
- package/ios-sdk/BubblSDK/Engine/EngineHost+Geofences.swift +177 -0
- package/ios-sdk/BubblSDK/Engine/EngineHost+Privacy.swift +65 -0
- package/ios-sdk/BubblSDK/Engine/EngineHost+Push.swift +154 -0
- package/ios-sdk/BubblSDK/Engine/EngineHost.swift +356 -0
- package/ios-sdk/BubblSDK/Engine/SupportedBubbl.swift +320 -0
- package/ios-sdk/BubblSDK/Engine/SystemLog.swift +27 -0
- package/ios-sdk/BubblSDK/Engine/WorkRunner.swift +107 -0
- package/ios-sdk/BubblSDK/Notification/Look.swift +78 -0
- package/ios-sdk/BubblSDK/Notification/NotificationMedia.swift +329 -0
- package/ios-sdk/BubblSDK/Notification/NotificationModel.swift +172 -0
- package/ios-sdk/BubblSDK/Notification/NotificationScreen.swift +428 -0
- package/ios-sdk/BubblSDK/Notification/ScreenPresenter.swift +148 -0
- package/ios-sdk/BubblSDK/Permissions/PermissionFlow.swift +169 -0
- package/ios-sdk/BubblSDK/Permissions/PrivacyView.swift +110 -0
- package/ios-sdk/BubblSDK/Platform/AppleDevicePlatform.swift +95 -0
- package/ios-sdk/BubblSDK/Platform/KeychainCredentialStore.swift +114 -0
- package/ios-sdk/BubblSDK/Platform/LocationService.swift +418 -0
- package/ios-sdk/BubblSDK/Platform/PushIntegration.swift +288 -0
- package/ios-sdk/BubblSDK/Platform/URLSessionHttpClient.swift +69 -0
- package/ios-sdk/BubblSDK/PrivacyInfo.xcprivacy +76 -0
- package/jest/index.js +75 -0
- package/lib/commonjs/BubblModule.js +14 -0
- package/lib/commonjs/BubblModule.js.map +1 -0
- package/lib/commonjs/BubblModule.web.js +13 -0
- package/lib/commonjs/BubblModule.web.js.map +1 -0
- package/lib/commonjs/NativeBubblSdk.js +20 -0
- package/lib/commonjs/NativeBubblSdk.js.map +1 -0
- package/lib/commonjs/index.js +336 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/BubblModule.js +5 -0
- package/lib/module/BubblModule.js.map +1 -0
- package/lib/module/BubblModule.web.js +9 -0
- package/lib/module/BubblModule.web.js.map +1 -0
- package/lib/module/NativeBubblSdk.js +18 -0
- package/lib/module/NativeBubblSdk.js.map +1 -0
- package/lib/module/index.js +331 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/BubblModule.d.ts +3 -0
- package/lib/typescript/commonjs/src/BubblModule.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/BubblModule.web.d.ts +8 -0
- package/lib/typescript/commonjs/src/BubblModule.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/NativeBubblSdk.d.ts +59 -0
- package/lib/typescript/commonjs/src/NativeBubblSdk.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +111 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/types.d.ts +192 -0
- package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/BubblModule.d.ts +3 -0
- package/lib/typescript/module/src/BubblModule.d.ts.map +1 -0
- package/lib/typescript/module/src/BubblModule.web.d.ts +8 -0
- package/lib/typescript/module/src/BubblModule.web.d.ts.map +1 -0
- package/lib/typescript/module/src/NativeBubblSdk.d.ts +59 -0
- package/lib/typescript/module/src/NativeBubblSdk.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +111 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/lib/typescript/module/src/types.d.ts +192 -0
- package/lib/typescript/module/src/types.d.ts.map +1 -0
- package/package.json +105 -24
- package/plugin/build/imageNotifications.d.ts +35 -0
- package/plugin/build/imageNotifications.js +198 -0
- package/plugin/build/index.d.ts +84 -0
- package/plugin/build/index.js +176 -0
- package/src/BubblModule.ts +2 -0
- package/src/BubblModule.web.ts +9 -0
- package/src/NativeBubblSdk.ts +67 -0
- package/src/index.ts +338 -282
- package/src/types.ts +182 -0
- package/android/src/main/kotlin/tech/bubbl/reactnative/BubblSdkModule.kt +0 -680
- package/android/src/main/kotlin/tech/bubbl/reactnative/BubblSdkNotificationIntents.kt +0 -32
- package/android/src/main/kotlin/tech/bubbl/reactnative/BubblSdkPackage.kt +0 -14
- package/ios/BubblSdk.swift +0 -802
- package/ios/BubblSdkBridge.m +0 -86
- package/react-native.config.js +0 -11
- package/src/specs/NativeBubblSdk.ts +0 -54
|
@@ -0,0 +1,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);
|