@tryterra/react-native 0.0.0-stage → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -2
  3. package/TerraReactNative.podspec +48 -0
  4. package/android/build.gradle +87 -0
  5. package/android/src/bridgeCore/kotlin/README.md +1 -0
  6. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeBootstrap.kt +133 -0
  7. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeHost.kt +32 -0
  8. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Conversions.kt +557 -0
  9. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/LauncherSlots.kt +41 -0
  10. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/ReadConversions.kt +250 -0
  11. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Rejections.kt +150 -0
  12. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/StreamRegistry.kt +77 -0
  13. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridge.kt +86 -0
  14. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeOpsImpl.kt +300 -0
  15. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeStreamsImpl.kt +50 -0
  16. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TokenBroker.kt +198 -0
  17. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeErrors.kt +217 -0
  18. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeOps.kt +582 -0
  19. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeTypes.kt +1569 -0
  20. package/android/src/healthConnectOnly/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +11 -0
  21. package/android/src/main/AndroidManifest.xml +24 -0
  22. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraBridgeModule.kt +96 -0
  23. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraReactNativePackage.kt +26 -0
  24. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraStartup.kt +46 -0
  25. package/android/src/samsung/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +13 -0
  26. package/docs/availability.md +396 -0
  27. package/frameworks/TerraSDK.xcframework/Info.plist +48 -0
  28. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Info.plist +0 -0
  29. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.abi.json +82672 -0
  30. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.package.swiftinterface +2186 -0
  31. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +2186 -0
  32. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  33. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftinterface +2163 -0
  34. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
  35. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/TerraSDK +0 -0
  36. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
  37. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
  38. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6594 -0
  39. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Info.plist +0 -0
  40. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +82672 -0
  41. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.package.swiftinterface +2186 -0
  42. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +2186 -0
  43. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  44. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +2163 -0
  45. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +82672 -0
  46. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.package.swiftinterface +2186 -0
  47. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +2186 -0
  48. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  49. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +2163 -0
  50. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
  51. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/TerraSDK +0 -0
  52. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/_CodeSignature/CodeResources +223 -0
  53. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
  54. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
  55. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6595 -0
  56. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/x86_64/TerraSDK.yml +6513 -0
  57. package/ios/TerraBridgeCore/BridgeBootstrap.swift +89 -0
  58. package/ios/TerraBridgeCore/BridgeHost.swift +13 -0
  59. package/ios/TerraBridgeCore/Conversions+Reads.swift +259 -0
  60. package/ios/TerraBridgeCore/Conversions.swift +594 -0
  61. package/ios/TerraBridgeCore/Generated/BridgeErrors.swift +179 -0
  62. package/ios/TerraBridgeCore/Generated/BridgeOps.swift +967 -0
  63. package/ios/TerraBridgeCore/Generated/BridgeTypes.swift +2050 -0
  64. package/ios/TerraBridgeCore/Locked.swift +15 -0
  65. package/ios/TerraBridgeCore/README.md +1 -0
  66. package/ios/TerraBridgeCore/Rejections.swift +130 -0
  67. package/ios/TerraBridgeCore/RuntimeAttachment.swift +60 -0
  68. package/ios/TerraBridgeCore/StreamRegistry.swift +59 -0
  69. package/ios/TerraBridgeCore/TerraBridge.swift +122 -0
  70. package/ios/TerraBridgeCore/TerraBridgeOpsImpl.swift +318 -0
  71. package/ios/TerraBridgeCore/TerraBridgeStreamsImpl.swift +60 -0
  72. package/ios/TerraBridgeCore/TokenBroker.swift +147 -0
  73. package/ios/TerraBridgeModule.mm +79 -0
  74. package/ios/TerraBridgeModuleImpl.swift +49 -0
  75. package/ios/TerraReactNative.swift +19 -0
  76. package/lib/EndUser.d.ts +12 -0
  77. package/lib/EndUser.js +13 -0
  78. package/lib/NativeTerraBridge.d.ts +23 -0
  79. package/lib/NativeTerraBridge.js +13 -0
  80. package/lib/Terra.d.ts +223 -0
  81. package/lib/Terra.js +336 -0
  82. package/lib/TerraError.d.ts +42 -0
  83. package/lib/TerraError.js +95 -0
  84. package/lib/createTerra.d.ts +8 -0
  85. package/lib/createTerra.js +19 -0
  86. package/lib/generated/client.d.ts +128 -0
  87. package/lib/generated/client.js +256 -0
  88. package/lib/generated/codecs.d.ts +219 -0
  89. package/lib/generated/codecs.js +2066 -0
  90. package/lib/generated/errors.d.ts +13 -0
  91. package/lib/generated/errors.js +68 -0
  92. package/lib/generated/ops.d.ts +13 -0
  93. package/lib/generated/ops.js +161 -0
  94. package/lib/generated/streams.d.ts +23 -0
  95. package/lib/generated/streams.js +58 -0
  96. package/lib/generated/types.d.ts +794 -0
  97. package/lib/generated/types.js +224 -0
  98. package/lib/index.d.ts +29 -0
  99. package/lib/index.js +55 -0
  100. package/package.json +71 -4
  101. package/src/EndUser.ts +15 -0
  102. package/src/NativeTerraBridge.ts +27 -0
  103. package/src/Terra.ts +592 -0
  104. package/src/TerraError.ts +116 -0
  105. package/src/createTerra.ts +18 -0
  106. package/src/generated/client.ts +315 -0
  107. package/src/generated/codecs.ts +1989 -0
  108. package/src/generated/errors.ts +113 -0
  109. package/src/generated/ops.ts +233 -0
  110. package/src/generated/streams.ts +74 -0
  111. package/src/generated/types.ts +1222 -0
  112. package/src/index.ts +54 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Terra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,143 @@
1
- # Temporary Holding Version
1
+ # @tryterra/react-native
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Terra's health SDK for React Native: Apple Health on iOS, Health Connect (and optionally Samsung
4
+ Health) on Android. A TurboModule over the native SDKs in this repository. React Native 0.81 or
5
+ newer, New Architecture only.
6
+
7
+ ```ts
8
+ import { createTerra, EndUser } from "@tryterra/react-native";
9
+
10
+ const terra = createTerra({ tokenProvider: () => api.terraToken() });
11
+
12
+ await terra.connect("health", EndUser.reference(user.id));
13
+ await terra.permissions.request({ read: ["body"] });
14
+ const hr = await terra.body.heartRate({ kind: "lastHours", hours: 1 });
15
+ ```
16
+
17
+ There is no sync call: once connected, the SDK uploads on its own, in the background too. Every
18
+ rejection is a `TerraError` with a stable `code` (see `docs/availability.md` for the full list and
19
+ what exists on which platform).
20
+
21
+ ## Local reads
22
+
23
+ Every read goes straight to the device's store and never touches the network:
24
+
25
+ | Member | Answers | Platforms |
26
+ |---|---|---|
27
+ | `body.heartRate(range)` | `HeartRateSample[]` | both |
28
+ | `body.restingHeartRate(range)` | `RestingHeartRateSample[]`, the store's own daily figure | both; Android grants it under `daily` |
29
+ | `body.readings(range)` | `BodyReading[]`, each `kind` carrying its unit | both; `bmi` and wrist temperature iOS only |
30
+ | `daily.steps(range)` | `StepCount[]` | both |
31
+ | `daily.totals(range)` | `DayTotal[]`, one per metric per whole local day | both; `appleExerciseTime` iOS only |
32
+ | `daily.intraday(range)` | `IntradaySample[]` | both; `basalEnergy` iOS only |
33
+ | `sleep.segments(range)` | `SleepSegment[]` | both |
34
+ | `activity.workouts(range)` | `Workout[]`, with the writer's `title` where it set one | both |
35
+ | `activity.routes(range)` | `WorkoutRoute[]`, each naming its workout | both |
36
+ | `activity.intensity(range)` | `WorkoutIntensitySample[]` | both; no `workoutId` on Android |
37
+ | `nutrition.quantities(range)` | `DietaryQuantity[]`, one per nutrient, not grouped into meals | both |
38
+ | `nutrition.hydration(range)` | `HydrationSample[]`, in millilitres | both |
39
+ | `menstruation.flow(range)` | `MenstruationFlowDay[]`, one per logged day | both |
40
+ | `menstruation.events(range)` | `MenstrualEvent[]`: ovulation tests, cervical mucus, intermenstrual bleeding | both |
41
+ | `athlete?.profile()` | `AthleteProfile`, or `undefined` where the store has none | iOS; `terra.athlete` is `undefined` on Android |
42
+
43
+ Ask for the category first: `permissions.request({ read: ["nutrition", "menstruation"] })`, plus
44
+ `"athlete"` on iOS, without which HealthKit hides the characteristics and `profile()` answers
45
+ `notSet` and nothing else. A read rejects with `not_connected` before `connect`, and with
46
+ `permission_denied` naming the category in `details.category` when the grant was refused.
47
+
48
+ `terra.availability(source?)` answers before any of them, with no connection: `available`, or why
49
+ not - `unsupportedDevice` on iOS, and on Android the reason Health Connect gives, such as
50
+ `providerAppMissing` when it needs installing.
51
+
52
+ ## Native setup
53
+
54
+ The client is built natively at launch, before any JavaScript runs, because background delivery
55
+ needs it then. Both platforms read the dev id from native configuration.
56
+
57
+ ### iOS
58
+
59
+ 1. `Info.plist`: `TerraDevID`, `NSHealthShareUsageDescription`, `NSHealthUpdateUsageDescription`,
60
+ `UIBackgroundModes` with `fetch`, and `BGTaskSchedulerPermittedIdentifiers` with
61
+ `co.tryterra.sdk.refresh`.
62
+ 2. Entitlements: `com.apple.developer.healthkit`, `com.apple.developer.healthkit.access` (an empty
63
+ array) and `com.apple.developer.healthkit.background-delivery`.
64
+ 3. One line, first thing in `application(_:didFinishLaunchingWithOptions:)`:
65
+
66
+ ```swift
67
+ import TerraReactNative
68
+ TerraReactNative.bootstrap()
69
+ ```
70
+
71
+ Without it every call rejects with `bridge.not_bootstrapped`, and `details.line` names the line.
72
+
73
+ ### Android
74
+
75
+ 1. `minSdkVersion` 28, `compileSdkVersion` 36.
76
+ 2. In `<application>`: `<meta-data android:name="co.tryterra.sdk.DEV_ID" android:value="..." />`.
77
+ Nothing else in code: an `androidx.startup` initializer bootstraps before `Application.onCreate`.
78
+ An app that removed WorkManager's default initializer, to configure WorkManager itself, adds
79
+ `TerraReactNative.bootstrap(this)` at the end of `Application.onCreate`.
80
+ 3. One `<uses-permission android:name="android.permission.health.*">` per Health Connect type your
81
+ `permissions.request` reads, plus the permissions-rationale activity filters. `example/` has a
82
+ complete manifest.
83
+
84
+ ### Samsung Health (Android, optional)
85
+
86
+ 1. Get Samsung's partner approval and download `samsung-health-data-api.aar` (Health Data SDK
87
+ 1.1.0) from Samsung. Samsung's licence forbids anyone else redistributing it.
88
+ 2. Add it to your app module: `implementation(files("libs/samsung-health-data-api.aar"))`.
89
+ 3. Set `terraSamsungHealth=true` in `android/gradle.properties`.
90
+
91
+ Then `terra.connect("samsungHealth", ...)`, `terra.permissions.request({...}, "samsungHealth")` and
92
+ `terra.diagnostics.state("samsungHealth")` reach Samsung Health directly. Reads, writes and change
93
+ streams are Health Connect's. Without step 3, `samsungHealth` rejects with
94
+ `health_data_unavailable`.
95
+
96
+ ## Developing in this repository
97
+
98
+ ```
99
+ npm ci && npx tsc --noEmit && npx jest # the JS lane (surface.yml)
100
+ npm run build # lib/, the types the example compiles against
101
+ cd example && npm ci && npx tsc --noEmit # the example's typecheck (react-native.yml)
102
+ npm run pods # then: npm run ios / npm run android
103
+ ```
104
+
105
+ `npm run pods` is a plain `pod install` (CocoaPods from Homebrew or `gem install`). The Podfile
106
+ takes React Native's core prebuilt, as CI does; built from source it does not compile under Xcode 26
107
+ and rewrites `Podfile.lock`.
108
+
109
+ `example/` builds against this checkout's SDKs, not the published ones: its Podfile sets
110
+ `TERRA_IOS_SDK_FROM_SOURCE=1` and points `TerraiOS` at `ios/TerraiOS.podspec`, and
111
+ `npm run android` first publishes `android/` into `android/build/published`, the only repository
112
+ the example takes `co.tryterra:terra-android` from.
113
+ `xcrun simctl launch booted co.tryterra.reactnative.example -terraSmoke` (or
114
+ `adb shell am start -a android.intent.action.VIEW -d terraexample://smoke`) runs its smoke test.
115
+
116
+ How the native halves ship, and why, is `docs/distribution.md`.
117
+
118
+ ## Releasing
119
+
120
+ The package pins one version of each native SDK in `package.json`'s `terraNative`, and
121
+ `packaging.test.ts` holds those pins equal to `sdkVersion` in `ios/Sources/TerraSDK/Version.swift`
122
+ and `VERSION_NAME` in `android/gradle.properties`. The tarball carries the released
123
+ `TerraSDK.xcframework` (`prepack` downloads it from the `tryterra/TerraiOS` release and checks it
124
+ against the checksum that tag's `Package.swift` pins), and a customer's Gradle sync resolves the
125
+ Android SDK from Maven Central, so the wrapper is released last:
126
+
127
+ 1. Release iOS (`v<version>`, then the `tryterra/TerraiOS` steps in the root README) and Android
128
+ (`android-v<version>`, then release the staged bundle from the Central Portal).
129
+ 2. Push `react-native-v<version>`. `.github/workflows/react-native-release.yml` checks the tag
130
+ against `version`, refuses while either native pin is missing from the `tryterra/TerraiOS`
131
+ release or Central, packs, and publishes with `npm publish --access public` (a pre-release under
132
+ the `next` dist-tag).
133
+
134
+ The workflow publishes through npm trusted publishing, with no npm secret in this repository. It
135
+ works once the package's settings on npmjs.com list `tryterra/terra-mobile` and
136
+ `react-native-release.yml` as a trusted publisher; npm allows that only after the package exists, so
137
+ the first version is published by hand with `npm publish --access public` from `react-native/`. A
138
+ version already on npm is skipped.
139
+
140
+ `npm pack --dry-run` shows the tarball. `prepack` builds `lib/`, copies hybrid-common into
141
+ `ios/TerraBridgeCore` and `android/src/bridgeCore`, and fetches `frameworks/TerraSDK.xcframework`
142
+ (`npm run fetch-sdk`), none of which a checkout carries. The xcframework is most of the tarball's
143
+ 12.6 MB.
@@ -0,0 +1,48 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ # hybrid-common's Swift, copied in by scripts/vendor-bridge-core.js: from `prepack` for the npm
6
+ # tarball, and from the example app's Podfile in this repository. A podspec cannot reach a source
7
+ # file outside its own directory, so without the copy this pod would compile a module with no
8
+ # bridge in it and fail at link time far from the cause.
9
+ bridge_core = File.join(__dir__, "ios", "TerraBridgeCore")
10
+ unless File.directory?(bridge_core)
11
+ raise "TerraReactNative: #{bridge_core} is missing. In a terra-mobile checkout run " \
12
+ "`npm run vendor` in react-native/; an installed package always carries it."
13
+ end
14
+
15
+ Pod::Spec.new do |s|
16
+ s.name = "TerraReactNative"
17
+ s.version = package["version"]
18
+ s.summary = package["description"]
19
+ s.homepage = "https://github.com/tryterra/terra-mobile"
20
+ s.license = package["license"]
21
+ s.authors = "Terra"
22
+ # RN 0.81's own floor; TerraSDK's is 15.0.
23
+ s.platforms = { :ios => "15.1" }
24
+ s.source = { :git => "https://github.com/tryterra/terra-mobile.git", :tag => "v#{s.version}" }
25
+
26
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
27
+ # hybrid-common is written in the Swift 6 language mode, and its concurrency checking is part of
28
+ # what it promises.
29
+ s.swift_version = "6.0"
30
+
31
+ # CocoaPods trunk takes no new releases; React Native's `spm_dependency` links dynamic
32
+ # binary targets without embedding them in the app.
33
+ if ENV["TERRA_IOS_SDK_FROM_SOURCE"] == "1"
34
+ s.dependency "TerraiOS", package["terraNative"]["ios"]
35
+ else
36
+ sdk = File.join(__dir__, "frameworks", "TerraSDK.xcframework")
37
+ unless File.directory?(sdk)
38
+ raise "TerraReactNative: #{sdk} is missing. In a terra-mobile checkout run " \
39
+ "`npm run fetch-sdk` in react-native/, or set TERRA_IOS_SDK_FROM_SOURCE=1; an installed " \
40
+ "package always carries it."
41
+ end
42
+ s.vendored_frameworks = "frameworks/TerraSDK.xcframework"
43
+ end
44
+
45
+ s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES" }
46
+
47
+ install_modules_dependencies(s)
48
+ end
@@ -0,0 +1,87 @@
1
+ import groovy.json.JsonSlurper
2
+
3
+ buildscript {
4
+ // The host's Kotlin, so the serialization plugin matches the compiler that runs it.
5
+ ext.terraKotlinVersion = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : "2.1.20"
6
+ repositories {
7
+ google()
8
+ mavenCentral()
9
+ }
10
+ dependencies {
11
+ // hybrid-common's generated DTOs are @Serializable. A host's root build has the Kotlin plugin
12
+ // already; the serialization plugin it has no reason to carry.
13
+ classpath "org.jetbrains.kotlin:kotlin-serialization:${terraKotlinVersion}"
14
+ }
15
+ }
16
+
17
+ apply plugin: "com.android.library"
18
+ apply plugin: "kotlin-android"
19
+ apply plugin: "org.jetbrains.kotlin.plugin.serialization"
20
+ apply plugin: "com.facebook.react"
21
+
22
+ def packageJson = new JsonSlurper().parse(file("../package.json"))
23
+ // The native SDK, exactly the version this package was released against. In this repository the
24
+ // example's settings.gradle includes ../android as a build, which substitutes the project for it.
25
+ def terraAndroidVersion = packageJson.terraNative.android
26
+
27
+ // hybrid-common compiled in place inside a terra-mobile checkout, so this build can never compile a
28
+ // stale copy; inside an npm tarball, the copy `prepack` made (scripts/vendor-bridge-core.js).
29
+ // Canonical, because a `file:..` install reaches this project through a node_modules symlink, and
30
+ // the checkout is only one level up from where the link points.
31
+ def bridgeCoreInRepo = new File(projectDir.canonicalFile, "../../hybrid-common/android/src/main/kotlin").canonicalFile
32
+ def bridgeCore = bridgeCoreInRepo.exists() ? bridgeCoreInRepo : file("src/bridgeCore/kotlin")
33
+ if (!bridgeCore.exists()) {
34
+ throw new GradleException("@tryterra/react-native: hybrid-common sources are missing at ${bridgeCore}")
35
+ }
36
+
37
+ // Samsung Health is opt-in, because the host has to supply Samsung's SDK itself: Samsung's licence
38
+ // forbids redistributing it, so no artifact of ours can carry it (see
39
+ // android/terra-android-samsung/libs/README.md). A host sets `terraSamsungHealth=true` in its
40
+ // gradle.properties and adds `samsung-health-data-api.aar` to its app; this module then depends on
41
+ // the add-on and registers its store at launch.
42
+ def samsungHealth = (rootProject.findProperty("terraSamsungHealth") ?: "false").toString().toBoolean()
43
+
44
+ def safeExtGet(prop, fallback) {
45
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
46
+ }
47
+
48
+ android {
49
+ namespace "co.tryterra.sdk.reactnative"
50
+ compileSdkVersion safeExtGet("compileSdkVersion", 36)
51
+
52
+ defaultConfig {
53
+ // The SDK's floor, above React Native's own 24.
54
+ minSdkVersion Math.max(28, safeExtGet("minSdkVersion", 28) as int)
55
+ }
56
+
57
+ compileOptions {
58
+ sourceCompatibility JavaVersion.VERSION_17
59
+ targetCompatibility JavaVersion.VERSION_17
60
+ }
61
+
62
+ kotlinOptions {
63
+ jvmTarget = "17"
64
+ }
65
+
66
+ sourceSets {
67
+ main {
68
+ java.srcDirs += [bridgeCore.path, samsungHealth ? "src/samsung/kotlin" : "src/healthConnectOnly/kotlin"]
69
+ }
70
+ }
71
+ }
72
+
73
+ repositories {
74
+ google()
75
+ mavenCentral()
76
+ }
77
+
78
+ dependencies {
79
+ implementation "com.facebook.react:react-android"
80
+ api "co.tryterra:terra-android:${terraAndroidVersion}"
81
+ if (samsungHealth) {
82
+ implementation "co.tryterra:terra-android-samsung:${terraAndroidVersion}"
83
+ }
84
+ implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0"
85
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1"
86
+ implementation "androidx.startup:startup-runtime:1.2.0"
87
+ }
@@ -0,0 +1 @@
1
+ Copied from hybrid-common by scripts/vendor-bridge-core.js. Do not edit; change hybrid-common.
@@ -0,0 +1,133 @@
1
+ package co.tryterra.sdk.bridge
2
+
3
+ import android.app.Activity
4
+ import android.app.Application
5
+ import android.content.Context
6
+ import android.content.pm.PackageManager
7
+ import android.os.Bundle
8
+ import android.util.Log
9
+ import androidx.activity.result.ActivityResultCaller
10
+ import co.tryterra.sdk.SDK_VERSION
11
+ import co.tryterra.sdk.Source
12
+ import co.tryterra.sdk.StoreRegistration
13
+ import co.tryterra.sdk.Terra
14
+ import co.tryterra.sdk.TerraError
15
+ import co.tryterra.sdk.background.BackgroundSync
16
+ import co.tryterra.sdk.bridge.generated.BridgeError
17
+ import co.tryterra.sdk.permissions.PermissionLauncher
18
+ import java.util.concurrent.atomic.AtomicReference
19
+ import kotlinx.coroutines.CoroutineScope
20
+
21
+ public object BridgeBootstrap {
22
+
23
+ /** The manifest meta-data carrying the developer id. */
24
+ public const val DEV_ID_KEY: String = "co.tryterra.sdk.DEV_ID"
25
+
26
+ private const val TAG = "TerraBridge"
27
+
28
+ /** What bootstrap parked: the client, and the broker the client was built with. */
29
+ public class Parked internal constructor(
30
+ public val terra: Terra,
31
+ internal val tokens: TokenBroker,
32
+ internal val host: BridgeHost,
33
+ )
34
+
35
+ private val parked = AtomicReference<Parked?>(null)
36
+
37
+ /** What bootstrap parked, or null when it has not run. */
38
+ public val current: Parked? get() = parked.get()
39
+
40
+ @Synchronized
41
+ public fun bootstrap(context: Context, stores: StoreRegistration.() -> Unit = {}): Parked? {
42
+ parked.get()?.let { return it }
43
+ val app = context.applicationContext as Application
44
+ val meta = metaData(app)
45
+ val devId = meta?.getString(DEV_ID_KEY)?.trim()?.takeIf { it.isNotEmpty() }
46
+ if (devId == null) {
47
+ Log.e(TAG, "no <meta-data android:name=\"$DEV_ID_KEY\"> in the app manifest, so Terra is not set up")
48
+ return null
49
+ }
50
+
51
+ val tokens = TokenBroker()
52
+ val terra = Terra(app, devId, tokenProvider = tokens::provide, stores = stores)
53
+ val launchers = Launchers(terra, servedSources(terra))
54
+ app.registerActivityLifecycleCallbacks(launchers)
55
+ val registered = BackgroundSync.register(app, terra)
56
+ val result = Parked(terra, tokens, LaunchHost(app, terra, launchers, devId, registered))
57
+ parked.set(result)
58
+ return result
59
+ }
60
+
61
+ public fun attach(scope: CoroutineScope, missingLine: String, emit: (String, String) -> Unit): TerraBridge {
62
+ val current = parked.get()
63
+ ?: throw TerraBridge.BridgeRejection(Rejections.of(BridgeError.NotBootstrapped(missingLine)))
64
+ return TerraBridge(current.terra, current.tokens, current.host, scope, emit = emit)
65
+ }
66
+
67
+ private fun metaData(app: Application): Bundle? = try {
68
+ app.packageManager.getApplicationInfo(app.packageName, PackageManager.GET_META_DATA).metaData
69
+ } catch (missing: PackageManager.NameNotFoundException) {
70
+ null
71
+ }
72
+
73
+ /** The sources [terra] has a store for, asked of the client itself rather than tracked beside it. */
74
+ internal fun servedSources(terra: Terra): Set<Source> = Source.entries.filterTo(LinkedHashSet()) { source ->
75
+ try {
76
+ terra.on(source)
77
+ true
78
+ } catch (missing: TerraError.HealthDataUnavailable) {
79
+ false
80
+ }
81
+ }
82
+
83
+ internal class Launchers(
84
+ private val terra: Terra,
85
+ private val sources: Set<Source>,
86
+ ) : Application.ActivityLifecycleCallbacks {
87
+ private val slots = LauncherSlots<Activity, PermissionLauncher>()
88
+
89
+ fun current(source: Source): PermissionLauncher? = slots.current(source)
90
+
91
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
92
+ if (activity !is ActivityResultCaller) return
93
+ val made = sources.mapNotNull { source ->
94
+ try {
95
+ source to terra.on(source).permissions.launcher(activity)
96
+ } catch (refused: RuntimeException) {
97
+ Log.w(TAG, "no permission sheet for $source in ${activity.javaClass.simpleName}", refused)
98
+ null
99
+ }
100
+ }.toMap()
101
+ slots.created(activity, made)
102
+ }
103
+
104
+ override fun onActivityDestroyed(activity: Activity) {
105
+ slots.destroyed(activity)?.values?.forEach { it.unregister() }
106
+ }
107
+
108
+ override fun onActivityStarted(activity: Activity) = slots.started(activity)
109
+ override fun onActivityStopped(activity: Activity) = slots.stopped(activity)
110
+ override fun onActivityResumed(activity: Activity) {}
111
+ override fun onActivityPaused(activity: Activity) {}
112
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
113
+ }
114
+
115
+ private class LaunchHost(
116
+ private val app: Application,
117
+ private val terra: Terra,
118
+ private val launchers: Launchers,
119
+ override val devId: String,
120
+ private val registered: Boolean,
121
+ ) : BridgeHost {
122
+ override fun permissionLauncher(source: Source): PermissionLauncher? = launchers.current(source)
123
+
124
+ override suspend fun backgroundRegistered(): Boolean =
125
+ registered && BackgroundSync.isScheduled(app, Terra.DEFAULT_STORAGE_DOMAIN) != false
126
+
127
+ override suspend fun cancelBackgroundSync(): Boolean = BackgroundSync.cancel(app, terra)
128
+
129
+ override val platform: String = "android"
130
+
131
+ override val sdkVersion: String = SDK_VERSION
132
+ }
133
+ }
@@ -0,0 +1,32 @@
1
+ package co.tryterra.sdk.bridge
2
+
3
+ import co.tryterra.sdk.Source
4
+ import co.tryterra.sdk.permissions.PermissionLauncher
5
+
6
+ /**
7
+ * The parts of an op that are not the SDK: the permission sheet's launcher, and the scheduler.
8
+ *
9
+ * A seam rather than a `Context` parameter, for the reason `HealthStore` is one in the SDK itself:
10
+ * everything behind it needs a device, and everything in front of it can then be decided on a plain
11
+ * JVM. `TerraBridgeOpsImpl` holds no `Context`, so the whole dispatch is testable on `ubuntu-latest`
12
+ * with no emulator - which is the lane that catches the disease this wrapper exists to cure.
13
+ */
14
+ public interface BridgeHost {
15
+
16
+ /** The launcher the current activity registered for [source]'s permission sheet, or null when none has. */
17
+ public fun permissionLauncher(source: Source): PermissionLauncher?
18
+
19
+ /** Whether the scheduler holds this client's periodic work. False means bootstrap did not run. */
20
+ public suspend fun backgroundRegistered(): Boolean
21
+
22
+ /** Drops the schedule. Android only; there is no iOS counterpart. */
23
+ public suspend fun cancelBackgroundSync(): Boolean
24
+
25
+ /** What `client.attach` reports, so a host can tell a misconfiguration from a bug. */
26
+ public val platform: String
27
+
28
+ public val sdkVersion: String
29
+
30
+ /** The dev id the native bootstrap was configured with, or null when it did not run. */
31
+ public val devId: String?
32
+ }