@tryterra/react-native 0.0.0-stage → 2.1.0
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/LICENSE +21 -0
- package/README.md +111 -2
- package/TerraReactNative.podspec +48 -0
- package/android/build.gradle +87 -0
- package/android/src/bridgeCore/kotlin/README.md +1 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeBootstrap.kt +133 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeHost.kt +32 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Conversions.kt +468 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/LauncherSlots.kt +41 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Rejections.kt +150 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/StreamRegistry.kt +77 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridge.kt +86 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeOpsImpl.kt +258 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeStreamsImpl.kt +52 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TokenBroker.kt +198 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeErrors.kt +217 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeOps.kt +493 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeTypes.kt +897 -0
- package/android/src/healthConnectOnly/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +11 -0
- package/android/src/main/AndroidManifest.xml +24 -0
- package/android/src/main/java/co/tryterra/sdk/reactnative/TerraBridgeModule.kt +96 -0
- package/android/src/main/java/co/tryterra/sdk/reactnative/TerraReactNativePackage.kt +26 -0
- package/android/src/main/java/co/tryterra/sdk/reactnative/TerraStartup.kt +46 -0
- package/android/src/samsung/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +13 -0
- package/docs/availability.md +246 -0
- package/frameworks/TerraSDK.xcframework/Info.plist +48 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Info.plist +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.abi.json +81513 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.package.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftinterface +2127 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6473 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Info.plist +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +81513 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.package.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +2127 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +81513 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.package.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +2127 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/_CodeSignature/CodeResources +223 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6474 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/x86_64/TerraSDK.yml +6392 -0
- package/ios/TerraBridgeCore/BridgeBootstrap.swift +89 -0
- package/ios/TerraBridgeCore/BridgeHost.swift +13 -0
- package/ios/TerraBridgeCore/Conversions.swift +467 -0
- package/ios/TerraBridgeCore/Generated/BridgeErrors.swift +179 -0
- package/ios/TerraBridgeCore/Generated/BridgeOps.swift +795 -0
- package/ios/TerraBridgeCore/Generated/BridgeTypes.swift +1317 -0
- package/ios/TerraBridgeCore/Locked.swift +15 -0
- package/ios/TerraBridgeCore/README.md +1 -0
- package/ios/TerraBridgeCore/Rejections.swift +130 -0
- package/ios/TerraBridgeCore/RuntimeAttachment.swift +60 -0
- package/ios/TerraBridgeCore/StreamRegistry.swift +59 -0
- package/ios/TerraBridgeCore/TerraBridge.swift +122 -0
- package/ios/TerraBridgeCore/TerraBridgeOpsImpl.swift +254 -0
- package/ios/TerraBridgeCore/TerraBridgeStreamsImpl.swift +60 -0
- package/ios/TerraBridgeCore/TokenBroker.swift +147 -0
- package/ios/TerraBridgeModule.mm +79 -0
- package/ios/TerraBridgeModuleImpl.swift +49 -0
- package/ios/TerraReactNative.swift +19 -0
- package/lib/EndUser.d.ts +12 -0
- package/lib/EndUser.js +13 -0
- package/lib/NativeTerraBridge.d.ts +23 -0
- package/lib/NativeTerraBridge.js +13 -0
- package/lib/Terra.d.ts +154 -0
- package/lib/Terra.js +280 -0
- package/lib/TerraError.d.ts +42 -0
- package/lib/TerraError.js +95 -0
- package/lib/createTerra.d.ts +8 -0
- package/lib/createTerra.js +19 -0
- package/lib/generated/client.d.ts +92 -0
- package/lib/generated/client.js +196 -0
- package/lib/generated/codecs.d.ts +135 -0
- package/lib/generated/codecs.js +1358 -0
- package/lib/generated/errors.d.ts +13 -0
- package/lib/generated/errors.js +68 -0
- package/lib/generated/ops.d.ts +13 -0
- package/lib/generated/ops.js +137 -0
- package/lib/generated/streams.d.ts +23 -0
- package/lib/generated/streams.js +58 -0
- package/lib/generated/types.d.ts +481 -0
- package/lib/generated/types.js +92 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.js +55 -0
- package/package.json +71 -4
- package/src/EndUser.ts +15 -0
- package/src/NativeTerraBridge.ts +27 -0
- package/src/Terra.ts +462 -0
- package/src/TerraError.ts +116 -0
- package/src/createTerra.ts +18 -0
- package/src/generated/client.ts +243 -0
- package/src/generated/codecs.ts +1287 -0
- package/src/generated/errors.ts +113 -0
- package/src/generated/ops.ts +197 -0
- package/src/generated/streams.ts +74 -0
- package/src/generated/types.ts +667 -0
- package/src/index.ts +52 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import TerraSDK
|
|
3
|
+
|
|
4
|
+
/// The hand-written half of streams: one source per stream, a fresh one per call.
|
|
5
|
+
///
|
|
6
|
+
/// Routing, element validation and framing are generated (`BridgeStreamDispatch`); this only
|
|
7
|
+
/// produces values, so a stream added to the schema stops this compiling until it has a source.
|
|
8
|
+
/// Each source is open when its method returns, so nothing after `subscribe` returns is missed.
|
|
9
|
+
struct TerraBridgeStreamsImpl: TerraBridgeStreams {
|
|
10
|
+
let terra: Terra
|
|
11
|
+
let audience: TokenAudience
|
|
12
|
+
let deviceZone: @Sendable () -> TimeZone
|
|
13
|
+
|
|
14
|
+
/// An event the bridge could not encode is relayed as an empty object, which the generated
|
|
15
|
+
/// envelope check refuses, so the subscription ends with an error instead of skipping it.
|
|
16
|
+
func diagnosticsEvents(_ params: Empty) async throws -> AsyncStream<TerraEvent> {
|
|
17
|
+
Self.relay(terra.diagnostics.events()) { (try? Conversions.event($0)) ?? BridgeJSON([:]) }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func bodyChanges(_ params: Empty) async throws -> AsyncStream<HealthChange> {
|
|
21
|
+
changes(await terra.body.changes())
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func dailyChanges(_ params: Empty) async throws -> AsyncStream<HealthChange> {
|
|
25
|
+
changes(await terra.daily.changes())
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func sleepChanges(_ params: Empty) async throws -> AsyncStream<HealthChange> {
|
|
29
|
+
changes(await terra.sleep.changes())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func activityChanges(_ params: Empty) async throws -> AsyncStream<HealthChange> {
|
|
33
|
+
changes(await terra.activity.changes())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func bridgeTokenRequest(_ params: Empty) async throws -> AsyncStream<TokenRequest> {
|
|
37
|
+
audience.listen()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// A category the surface excludes has no wire name; a namespace's stream never carries one.
|
|
41
|
+
private func changes(_ source: AsyncStream<TerraSDK.HealthChange>) -> AsyncStream<HealthChange> {
|
|
42
|
+
let zone = deviceZone
|
|
43
|
+
return Self.relay(source) { Conversions.change($0, zone: zone()) }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Forwards an SDK stream, converting each element, until either side goes away.
|
|
47
|
+
private static func relay<Element: Sendable, Wire: Sendable>(
|
|
48
|
+
_ source: AsyncStream<Element>, _ convert: @escaping @Sendable (Element) -> Wire?
|
|
49
|
+
) -> AsyncStream<Wire> {
|
|
50
|
+
AsyncStream { continuation in
|
|
51
|
+
let task = Task {
|
|
52
|
+
for await element in source {
|
|
53
|
+
if let wire = convert(element) { continuation.yield(wire) }
|
|
54
|
+
}
|
|
55
|
+
continuation.finish()
|
|
56
|
+
}
|
|
57
|
+
continuation.onTermination = { _ in task.cancel() }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import TerraSDK
|
|
3
|
+
|
|
4
|
+
/// The SDK's token provider, answered by a runtime on the other side of the bridge.
|
|
5
|
+
///
|
|
6
|
+
/// The client is built at launch, before any JavaScript exists, and captures its provider then. So
|
|
7
|
+
/// the provider is this broker, built first and handed to the client, and the runtime that is
|
|
8
|
+
/// listening now is whichever session last said it has a provider.
|
|
9
|
+
///
|
|
10
|
+
/// Safe because the SDK asks on one path only, `connect`, which only a live runtime can call. A
|
|
11
|
+
/// background wake never asks: uploads are signed by the device key.
|
|
12
|
+
actor TokenBroker {
|
|
13
|
+
static let defaultTimeoutMs = 30_000
|
|
14
|
+
|
|
15
|
+
private struct Pending {
|
|
16
|
+
let audience: ObjectIdentifier
|
|
17
|
+
let continuation: CheckedContinuation<String, any Error>
|
|
18
|
+
let timer: Task<Void, Never>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private weak var owner: TokenAudience?
|
|
22
|
+
private var timeoutMs = TokenBroker.defaultTimeoutMs
|
|
23
|
+
private var pending: [String: Pending] = [:]
|
|
24
|
+
|
|
25
|
+
/// What the client calls. No listener answers `tokenUnavailable` at once, which is what a
|
|
26
|
+
/// native app built without a provider gets.
|
|
27
|
+
func provide() async throws -> String {
|
|
28
|
+
guard let owner else { throw TerraError.tokenUnavailable }
|
|
29
|
+
let id = "tok-\(UUID().uuidString.lowercased())"
|
|
30
|
+
let budget = timeoutMs
|
|
31
|
+
return try await withTaskCancellationHandler {
|
|
32
|
+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<String, any Error>) in
|
|
33
|
+
if Task.isCancelled {
|
|
34
|
+
continuation.resume(throwing: CancellationError())
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
let timer = Task.detached { [weak self] in
|
|
38
|
+
try? await Task.sleep(nanoseconds: UInt64(min(max(budget, 0), 3_600_000)) * 1_000_000)
|
|
39
|
+
await self?.settle(id, .failure(BridgeError.tokenTimeout(timeoutMs: budget)))
|
|
40
|
+
}
|
|
41
|
+
pending[id] = Pending(audience: ObjectIdentifier(owner), continuation: continuation, timer: timer)
|
|
42
|
+
if !owner.deliver(TokenRequest(requestId: id)) {
|
|
43
|
+
settle(id, .failure(TerraError.tokenUnavailable))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} onCancel: {
|
|
47
|
+
Task { await self.settle(id, .failure(CancellationError())) }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Whether an ask made now would reach anyone.
|
|
52
|
+
var canAnswer: Bool { owner?.isListening ?? false }
|
|
53
|
+
|
|
54
|
+
/// The session that answers from now on, or none.
|
|
55
|
+
func setProviderPresent(_ present: Bool, timeoutMs: Int, audience: TokenAudience) {
|
|
56
|
+
guard !audience.isClosed else { return }
|
|
57
|
+
if present {
|
|
58
|
+
owner = audience
|
|
59
|
+
self.timeoutMs = timeoutMs
|
|
60
|
+
} else if owner === audience {
|
|
61
|
+
owner = nil
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// An unknown or settled id is ignored: a runtime that reloaded answers asks its predecessor
|
|
66
|
+
/// made, and failing that would turn a harmless race into a failed link.
|
|
67
|
+
func resolve(_ requestId: String, token: String) {
|
|
68
|
+
settle(requestId, .success(token))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func reject(_ requestId: String, message: String) {
|
|
72
|
+
settle(requestId, .failure(ProviderRefused(message: message)))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Fails what this session was asked, and stops routing to it. Only this session's: a stale
|
|
76
|
+
/// teardown arriving after its successor registered must not fail the successor's link.
|
|
77
|
+
func invalidate(_ audience: TokenAudience) {
|
|
78
|
+
if owner === audience { owner = nil }
|
|
79
|
+
let identity = ObjectIdentifier(audience)
|
|
80
|
+
for (id, ask) in pending where ask.audience == identity {
|
|
81
|
+
settle(id, .failure(ProviderRefused(message: "the host runtime went away")))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
var pendingCount: Int { pending.count }
|
|
86
|
+
|
|
87
|
+
private func settle(_ id: String, _ result: Result<String, any Error>) {
|
|
88
|
+
guard let ask = pending.removeValue(forKey: id) else { return }
|
|
89
|
+
ask.timer.cancel()
|
|
90
|
+
ask.continuation.resume(with: result)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// The provider's own failure. Its text never crosses the bridge; see `Rejections`.
|
|
94
|
+
struct ProviderRefused: Error, Sendable {
|
|
95
|
+
let message: String
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// The token-request subscriptions of one session.
|
|
100
|
+
final class TokenAudience: Sendable {
|
|
101
|
+
private struct State {
|
|
102
|
+
var listeners: [UUID: AsyncStream<TokenRequest>.Continuation] = [:]
|
|
103
|
+
var closed = false
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private let state = Locked(State())
|
|
107
|
+
|
|
108
|
+
var isClosed: Bool { state.withLock { $0.closed } }
|
|
109
|
+
|
|
110
|
+
var isListening: Bool { state.withLock { !$0.closed && !$0.listeners.isEmpty } }
|
|
111
|
+
|
|
112
|
+
func listen() -> AsyncStream<TokenRequest> {
|
|
113
|
+
let (stream, continuation) = AsyncStream<TokenRequest>.makeStream()
|
|
114
|
+
let key = UUID()
|
|
115
|
+
let accepted = state.withLock { state in
|
|
116
|
+
guard !state.closed else { return false }
|
|
117
|
+
state.listeners[key] = continuation
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
guard accepted else {
|
|
121
|
+
continuation.finish()
|
|
122
|
+
return stream
|
|
123
|
+
}
|
|
124
|
+
continuation.onTermination = { [weak self] _ in
|
|
125
|
+
self?.state.withLock { _ = $0.listeners.removeValue(forKey: key) }
|
|
126
|
+
}
|
|
127
|
+
return stream
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func deliver(_ request: TokenRequest) -> Bool {
|
|
131
|
+
let listeners = state.withLock { $0.closed ? [] : Array($0.listeners.values) }
|
|
132
|
+
var delivered = false
|
|
133
|
+
for listener in listeners {
|
|
134
|
+
if case .enqueued = listener.yield(request) { delivered = true }
|
|
135
|
+
}
|
|
136
|
+
return delivered
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func close() {
|
|
140
|
+
let listeners = state.withLock { state in
|
|
141
|
+
state.closed = true
|
|
142
|
+
defer { state.listeners = [:] }
|
|
143
|
+
return Array(state.listeners.values)
|
|
144
|
+
}
|
|
145
|
+
for listener in listeners { listener.finish() }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#import <React/RCTInvalidating.h>
|
|
2
|
+
#import <TerraReactNativeSpec/TerraReactNativeSpec.h>
|
|
3
|
+
|
|
4
|
+
// Declared here rather than in a header: a public header importing the C++ spec would be read by
|
|
5
|
+
// the Swift half of this module, which cannot import C++.
|
|
6
|
+
@interface TerraBridgeModule : NativeTerraBridgeSpecBase <NativeTerraBridgeSpec, RCTInvalidating>
|
|
7
|
+
@end
|
|
8
|
+
|
|
9
|
+
#if __has_include("TerraReactNative-Swift.h")
|
|
10
|
+
#import "TerraReactNative-Swift.h"
|
|
11
|
+
#else
|
|
12
|
+
#import <TerraReactNative/TerraReactNative-Swift.h>
|
|
13
|
+
#endif
|
|
14
|
+
|
|
15
|
+
static NSString *const TerraBridgeErrorDomain = @"co.tryterra.sdk.bridge";
|
|
16
|
+
|
|
17
|
+
@implementation TerraBridgeModule {
|
|
18
|
+
TerraBridgeModuleImpl *_impl;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
RCT_EXPORT_MODULE(TerraBridge)
|
|
22
|
+
|
|
23
|
+
- (instancetype)init
|
|
24
|
+
{
|
|
25
|
+
if (self = [super init]) {
|
|
26
|
+
__weak TerraBridgeModule *weakSelf = self;
|
|
27
|
+
_impl = [[TerraBridgeModuleImpl alloc] initWithEmit:^(NSString *subscriptionId, NSString *message) {
|
|
28
|
+
[weakSelf emitOnMessage:@{@"subscriptionId" : subscriptionId, @"message" : message}];
|
|
29
|
+
}];
|
|
30
|
+
}
|
|
31
|
+
return self;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static void (^TerraReject(RCTPromiseRejectBlock reject))(NSString *, NSString *, NSString *)
|
|
35
|
+
{
|
|
36
|
+
return ^(NSString *code, NSString *message, NSString *payload) {
|
|
37
|
+
reject(code, message, [NSError errorWithDomain:TerraBridgeErrorDomain code:0 userInfo:@{@"payload" : payload}]);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
- (void)invoke:(NSString *)op
|
|
42
|
+
payload:(NSString *)payload
|
|
43
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
44
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
45
|
+
{
|
|
46
|
+
[_impl invoke:op
|
|
47
|
+
payload:payload
|
|
48
|
+
resolve:^(NSString *response) { resolve(response); }
|
|
49
|
+
reject:TerraReject(reject)];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
- (void)subscribe:(NSString *)stream
|
|
53
|
+
params:(NSString *)params
|
|
54
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
55
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
56
|
+
{
|
|
57
|
+
[_impl subscribe:stream
|
|
58
|
+
params:params
|
|
59
|
+
resolve:^(NSString *subscriptionId) { resolve(subscriptionId); }
|
|
60
|
+
reject:TerraReject(reject)];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
- (void)unsubscribe:(NSString *)subscriptionId
|
|
64
|
+
{
|
|
65
|
+
[_impl unsubscribe:subscriptionId];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
- (void)invalidate
|
|
69
|
+
{
|
|
70
|
+
[_impl invalidate];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
74
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
75
|
+
{
|
|
76
|
+
return std::make_shared<facebook::react::NativeTerraBridgeSpecJSI>(params);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
@objc(TerraBridgeModuleImpl)
|
|
4
|
+
public final class TerraBridgeModuleImpl: NSObject, @unchecked Sendable {
|
|
5
|
+
public typealias Resolve = @Sendable (String) -> Void
|
|
6
|
+
/// `(code, message, payload JSON)`. The module rejects with the payload under `userInfo.payload`.
|
|
7
|
+
public typealias Reject = @Sendable (String, String, String) -> Void
|
|
8
|
+
|
|
9
|
+
private let attachment: RuntimeAttachment
|
|
10
|
+
|
|
11
|
+
@objc public init(emit: @escaping @Sendable (_ subscriptionId: String, _ message: String) -> Void) {
|
|
12
|
+
attachment = RuntimeAttachment(missingLine: TerraReactNative.missingLine, emit: emit)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// Nothing answers a runtime that has been invalidated: its promises belong to a JS context that is gone.
|
|
16
|
+
@objc public func invoke(_ op: String, payload: String, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
17
|
+
let attachment = attachment
|
|
18
|
+
Task {
|
|
19
|
+
do throws(BridgeRejection) {
|
|
20
|
+
guard let session = try attachment.session() else { return }
|
|
21
|
+
let answer = try await session.dispatch(op: op, payload: payload)
|
|
22
|
+
if !attachment.isInvalidated { resolve(answer) }
|
|
23
|
+
} catch {
|
|
24
|
+
if !attachment.isInvalidated { reject(error.code, error.payload.message, error.json) }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@objc public func subscribe(_ stream: String, params: String, resolve: @escaping Resolve, reject: @escaping Reject) {
|
|
30
|
+
let attachment = attachment
|
|
31
|
+
Task {
|
|
32
|
+
do throws(BridgeRejection) {
|
|
33
|
+
guard let session = try attachment.session() else { return }
|
|
34
|
+
let id = try await session.subscribe(stream: stream, params: params)
|
|
35
|
+
if !attachment.isInvalidated { resolve(id) }
|
|
36
|
+
} catch {
|
|
37
|
+
if !attachment.isInvalidated { reject(error.code, error.payload.message, error.json) }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@objc public func unsubscribe(_ subscriptionId: String) {
|
|
43
|
+
attachment.current()?.unsubscribe(id: subscriptionId)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@objc public func invalidate() {
|
|
47
|
+
attachment.invalidate()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
/// iOS raises if a background task is registered after launch finishes.
|
|
5
|
+
/// A HealthKit wake never starts a JavaScript runtime.
|
|
6
|
+
@objc(TerraReactNative)
|
|
7
|
+
public final class TerraReactNative: NSObject {
|
|
8
|
+
static let missingLine = "TerraReactNative.bootstrap() in application(_:didFinishLaunchingWithOptions:), with TerraDevID in Info.plist"
|
|
9
|
+
|
|
10
|
+
private static let log = Logger(subsystem: "co.tryterra.sdk", category: "TerraReactNative")
|
|
11
|
+
|
|
12
|
+
/// Builds the client, registers background sync, and parks both for JavaScript to attach to.
|
|
13
|
+
/// Safe to call more than once; the second call does nothing.
|
|
14
|
+
@objc public static func bootstrap() {
|
|
15
|
+
if BridgeBootstrap.bootstrap(bundle: .main) == nil {
|
|
16
|
+
log.error("Terra is not set up: Info.plist has no \(BridgeBootstrap.devIDKey, privacy: .public)")
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
package/lib/EndUser.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who a connect is for. Every connect names one: a second account on the same phone is a different
|
|
3
|
+
* person, and must not be handed the first one's link.
|
|
4
|
+
*/
|
|
5
|
+
import type { EndUser as WireEndUser } from "./generated/types";
|
|
6
|
+
export type EndUser = WireEndUser;
|
|
7
|
+
export declare const EndUser: {
|
|
8
|
+
/** The person your backend knows as `id`: the same value it mints their token with. */
|
|
9
|
+
readonly reference: (id: string) => EndUser;
|
|
10
|
+
/** Nobody your backend tracks. Every anonymous connect on one device is the same person. */
|
|
11
|
+
readonly anonymous: EndUser;
|
|
12
|
+
};
|
package/lib/EndUser.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Who a connect is for. Every connect names one: a second account on the same phone is a different
|
|
4
|
+
* person, and must not be handed the first one's link.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.EndUser = void 0;
|
|
8
|
+
exports.EndUser = {
|
|
9
|
+
/** The person your backend knows as `id`: the same value it mints their token with. */
|
|
10
|
+
reference: (id) => ({ kind: "reference", id }),
|
|
11
|
+
/** Nobody your backend tracks. Every anonymous connect on one device is the same person. */
|
|
12
|
+
anonymous: { kind: "anonymous" },
|
|
13
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TurboModule spec. React Native's Codegen reads this file at the host app's build and
|
|
3
|
+
* generates the native interfaces that `ios/TerraBridgeModule.mm` and
|
|
4
|
+
* `android/.../TerraBridgeModule.kt` implement.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately narrow: one op call, one stream subscription, one emitter. The op strings and their
|
|
7
|
+
* payloads are typed on both sides by `surface/gen`, so nothing here changes when the SDK grows an
|
|
8
|
+
* op.
|
|
9
|
+
*/
|
|
10
|
+
import type { CodegenTypes, TurboModule } from "react-native";
|
|
11
|
+
export type StreamMessageEvent = {
|
|
12
|
+
subscriptionId: string;
|
|
13
|
+
/** A `StreamMessage`, as the wire spells it. */
|
|
14
|
+
message: string;
|
|
15
|
+
};
|
|
16
|
+
export interface Spec extends TurboModule {
|
|
17
|
+
invoke(op: string, payload: string): Promise<string>;
|
|
18
|
+
subscribe(stream: string, params: string): Promise<string>;
|
|
19
|
+
unsubscribe(subscriptionId: string): void;
|
|
20
|
+
readonly onMessage: CodegenTypes.EventEmitter<StreamMessageEvent>;
|
|
21
|
+
}
|
|
22
|
+
declare const _default: Spec;
|
|
23
|
+
export default _default;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The TurboModule spec. React Native's Codegen reads this file at the host app's build and
|
|
4
|
+
* generates the native interfaces that `ios/TerraBridgeModule.mm` and
|
|
5
|
+
* `android/.../TerraBridgeModule.kt` implement.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately narrow: one op call, one stream subscription, one emitter. The op strings and their
|
|
8
|
+
* payloads are typed on both sides by `surface/gen`, so nothing here changes when the SDK grows an
|
|
9
|
+
* op.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const react_native_1 = require("react-native");
|
|
13
|
+
exports.default = react_native_1.TurboModuleRegistry.getEnforcing("TerraBridge");
|
package/lib/Terra.d.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client an app holds: namespaces over the generated ops, the token provider, and the streams.
|
|
3
|
+
*
|
|
4
|
+
* Hand-written, deliberately thin. Every method is one generated `GeneratedClient` call; what lives
|
|
5
|
+
* here is only what the generated layer cannot know - which runtime the app is on, how a JS
|
|
6
|
+
* function answers a native token request, and how one native event emitter fans out to many
|
|
7
|
+
* listeners. `facade.test.ts` drives every method and fails when an op in the schema has no member
|
|
8
|
+
* here, so this file cannot quietly fall behind the surface the way the 1.x wrapper did.
|
|
9
|
+
*/
|
|
10
|
+
import { TerraError } from "./TerraError";
|
|
11
|
+
import type { BodyEntry, BootstrapState, Connection, DataCategory, DiagnosticsState, EndUser, HealthAccess, HealthChange, HeartRateEntry, HeartRateSample, NutritionEntry, PermissionStatus, PlannedWorkout, WorkoutSchedulingStatus, SleepEntry, SleepSegment, Source, StepCount, StepEntry, SupportReport, TerraEvent, TimeRange, Workout, WorkoutEntry } from "./generated/types";
|
|
12
|
+
/** What the facade needs from the native module. `NativeTerraBridge` satisfies it; so does a double. */
|
|
13
|
+
export interface NativeBridge {
|
|
14
|
+
invoke(op: string, payload: string): Promise<string>;
|
|
15
|
+
subscribe(stream: string, params: string): Promise<string>;
|
|
16
|
+
unsubscribe(subscriptionId: string): void;
|
|
17
|
+
onMessage(listener: (event: {
|
|
18
|
+
subscriptionId: string;
|
|
19
|
+
message: string;
|
|
20
|
+
}) => void): {
|
|
21
|
+
remove(): void;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export type Platform = "ios" | "android";
|
|
25
|
+
/** Asked for a one-shot link token, and only while `connect` establishes a new link. */
|
|
26
|
+
export type TokenProvider = () => Promise<string>;
|
|
27
|
+
export interface TerraOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Called only when `connect` has to make a new link and was given no token. A restored link
|
|
30
|
+
* never asks, and neither does a background wake: uploads are signed by the device key.
|
|
31
|
+
*/
|
|
32
|
+
readonly tokenProvider?: TokenProvider;
|
|
33
|
+
/** How long the native side waits for `tokenProvider`. 30 seconds by default. */
|
|
34
|
+
readonly tokenTimeoutMs?: number;
|
|
35
|
+
}
|
|
36
|
+
export type Unsubscribe = () => void;
|
|
37
|
+
/** A stream's listener. `onError` receives the error a stream ended with; a stream that completes ends quietly. */
|
|
38
|
+
export interface StreamListener<T> {
|
|
39
|
+
(element: T): void;
|
|
40
|
+
}
|
|
41
|
+
export interface Permissions {
|
|
42
|
+
/**
|
|
43
|
+
* One sheet covering both directions and everything asked before. Android answers what this call
|
|
44
|
+
* obtained; HealthKit will not report read grants, so iOS answers `undefined`.
|
|
45
|
+
*
|
|
46
|
+
* `source` defaults to `health`. `samsungHealth` presents Samsung Health's own consent popup and
|
|
47
|
+
* is Android only.
|
|
48
|
+
*/
|
|
49
|
+
request(access: Partial<HealthAccess>, source?: Source): Promise<HealthAccess | undefined>;
|
|
50
|
+
status(category: DataCategory, source?: Source): Promise<PermissionStatus>;
|
|
51
|
+
writeStatus(category: DataCategory, source?: Source): Promise<PermissionStatus>;
|
|
52
|
+
requested(source?: Source): Promise<HealthAccess>;
|
|
53
|
+
/** Android only. */
|
|
54
|
+
readonly statuses?: (source?: Source) => Promise<Partial<Record<DataCategory, PermissionStatus>>>;
|
|
55
|
+
/** Android only. */
|
|
56
|
+
readonly backgroundReadStatus?: (source?: Source) => Promise<PermissionStatus>;
|
|
57
|
+
/** Android only. */
|
|
58
|
+
readonly historyReadStatus?: (source?: Source) => Promise<PermissionStatus>;
|
|
59
|
+
}
|
|
60
|
+
export interface Diagnostics {
|
|
61
|
+
state(source?: Source): Promise<DiagnosticsState>;
|
|
62
|
+
lastDelivery(source?: Source): Promise<Date | undefined>;
|
|
63
|
+
report(source?: Source): Promise<SupportReport>;
|
|
64
|
+
send(source?: Source): Promise<void>;
|
|
65
|
+
/** Everything the SDK does, as it happens. Never carries a health value. */
|
|
66
|
+
events(listener: StreamListener<TerraEvent>, onError?: (error: TerraError) => void): Unsubscribe;
|
|
67
|
+
}
|
|
68
|
+
export interface Background {
|
|
69
|
+
/** Whether the native bootstrap registered background sync at launch. False is a host setup bug. */
|
|
70
|
+
isRegistered(): Promise<boolean>;
|
|
71
|
+
/** Android only: drops the schedule. */
|
|
72
|
+
readonly cancel?: () => Promise<boolean>;
|
|
73
|
+
}
|
|
74
|
+
interface Changes {
|
|
75
|
+
/** Which days changed in the health store. Re-read through this namespace's accessor. */
|
|
76
|
+
changes(listener: StreamListener<HealthChange>, onError?: (error: TerraError) => void): Unsubscribe;
|
|
77
|
+
}
|
|
78
|
+
export interface BodyMetrics extends Changes {
|
|
79
|
+
heartRate(range: TimeRange): Promise<readonly HeartRateSample[]>;
|
|
80
|
+
logHeartRate(entries: readonly HeartRateEntry[]): Promise<readonly string[]>;
|
|
81
|
+
log(entries: readonly BodyEntry[]): Promise<readonly string[]>;
|
|
82
|
+
delete(id: string): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
export interface DailyMetrics extends Changes {
|
|
85
|
+
steps(range: TimeRange): Promise<readonly StepCount[]>;
|
|
86
|
+
log(entries: readonly StepEntry[]): Promise<readonly string[]>;
|
|
87
|
+
delete(id: string): Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
export interface SleepMetrics extends Changes {
|
|
90
|
+
segments(range: TimeRange): Promise<readonly SleepSegment[]>;
|
|
91
|
+
log(entries: readonly SleepEntry[]): Promise<readonly string[]>;
|
|
92
|
+
delete(id: string): Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
export interface ActivityMetrics extends Changes {
|
|
95
|
+
workouts(range: TimeRange): Promise<readonly Workout[]>;
|
|
96
|
+
log(entries: readonly WorkoutEntry[]): Promise<readonly string[]>;
|
|
97
|
+
delete(id: string): Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
/** Write-only on both platforms. */
|
|
100
|
+
export interface NutritionMetrics {
|
|
101
|
+
log(entries: readonly NutritionEntry[]): Promise<readonly string[]>;
|
|
102
|
+
delete(id: string): Promise<void>;
|
|
103
|
+
}
|
|
104
|
+
/** iOS only. */
|
|
105
|
+
export interface PlannedWorkouts {
|
|
106
|
+
planned(): Promise<readonly PlannedWorkout[]>;
|
|
107
|
+
markComplete(id: string, at?: Date): Promise<void>;
|
|
108
|
+
delete(id: string): Promise<void>;
|
|
109
|
+
/** Whether plans can be put on the person's Apple Watch. Never prompts. */
|
|
110
|
+
schedulingStatus(): Promise<WorkoutSchedulingStatus>;
|
|
111
|
+
/**
|
|
112
|
+
* Shows WorkoutKit's sheet if the person has not been asked. Once authorized, every sync run
|
|
113
|
+
* keeps the Watch schedule in step with Terra; nothing else needs calling.
|
|
114
|
+
*/
|
|
115
|
+
requestScheduling(): Promise<WorkoutSchedulingStatus>;
|
|
116
|
+
}
|
|
117
|
+
export declare class Terra {
|
|
118
|
+
readonly platform: Platform;
|
|
119
|
+
readonly permissions: Permissions;
|
|
120
|
+
readonly diagnostics: Diagnostics;
|
|
121
|
+
readonly background: Background;
|
|
122
|
+
readonly body: BodyMetrics;
|
|
123
|
+
readonly daily: DailyMetrics;
|
|
124
|
+
readonly sleep: SleepMetrics;
|
|
125
|
+
readonly activity: ActivityMetrics;
|
|
126
|
+
readonly nutrition: NutritionMetrics;
|
|
127
|
+
/** `undefined` on Android, which has no planned workouts. */
|
|
128
|
+
readonly plannedWorkouts: PlannedWorkouts | undefined;
|
|
129
|
+
private readonly ops;
|
|
130
|
+
private readonly streams;
|
|
131
|
+
/** Settles once the token provider, when there is one, can be asked. */
|
|
132
|
+
private readonly tokensReady;
|
|
133
|
+
constructor(native: NativeBridge, platform: Platform, options?: TerraOptions);
|
|
134
|
+
/**
|
|
135
|
+
* The native client this runtime is attached to. Rejects with `bridge.not_bootstrapped`, carrying
|
|
136
|
+
* the exact line to add in `details.line`, when the app's native launch never set Terra up.
|
|
137
|
+
*/
|
|
138
|
+
attach(): Promise<BootstrapState>;
|
|
139
|
+
/**
|
|
140
|
+
* Links `source` for the person `endUser` names, or restores the link already made for them.
|
|
141
|
+
*
|
|
142
|
+
* Every connect names its person: a second account on the same phone must not be handed the
|
|
143
|
+
* first one's link. With `token` the grant in hand is spent (a QR code, a deep link); without it
|
|
144
|
+
* the `tokenProvider` is asked, and only when a new link is actually needed.
|
|
145
|
+
*/
|
|
146
|
+
connect(source: Source, endUser: EndUser, options?: {
|
|
147
|
+
readonly token?: string;
|
|
148
|
+
}): Promise<Connection>;
|
|
149
|
+
connections(): Promise<readonly Connection[]>;
|
|
150
|
+
disconnect(source?: Source): Promise<void>;
|
|
151
|
+
/** Answers the native broker's token requests with `provider`, for the lifetime of this runtime. */
|
|
152
|
+
private answerTokens;
|
|
153
|
+
}
|
|
154
|
+
export {};
|