@appeeky/expo-healthkit 0.1.0 → 0.1.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.
package/.releaserc.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"branches": ["main"],
|
|
3
|
+
"plugins": [
|
|
4
|
+
"@semantic-release/commit-analyzer",
|
|
5
|
+
"@semantic-release/release-notes-generator",
|
|
6
|
+
[
|
|
7
|
+
"@semantic-release/changelog",
|
|
8
|
+
{
|
|
9
|
+
"changelogFile": "CHANGELOG.md"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"@semantic-release/npm",
|
|
13
|
+
"@semantic-release/github",
|
|
14
|
+
[
|
|
15
|
+
"@semantic-release/git",
|
|
16
|
+
{
|
|
17
|
+
"assets": ["CHANGELOG.md", "package.json", "package-lock.json"],
|
|
18
|
+
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
]
|
|
22
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [0.1.1](https://github.com/appeeky/expo-healthkit/compare/v0.1.0...v0.1.1) (2026-09-05)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* use local datetime for Health Connect daily statistics ([d01e776](https://github.com/appeeky/expo-healthkit/commit/d01e776398fb7f45b2b4c0401593a871f03bb700))
|
|
7
|
+
|
|
1
8
|
# Changelog
|
|
2
9
|
|
|
3
10
|
## 0.1.0
|
|
@@ -22,5 +29,5 @@
|
|
|
22
29
|
- Published as [`@appeeky/expo-healthkit`](https://www.npmjs.com/package/@appeeky/expo-healthkit)
|
|
23
30
|
- npm-ready `exports`, `.npmignore`, and `prepublishOnly` build
|
|
24
31
|
- App Store privacy manifest (`PrivacyInfo.xcprivacy`)
|
|
25
|
-
- GitHub Actions for lint, typecheck, tests with coverage thresholds, and
|
|
32
|
+
- GitHub Actions for lint, typecheck, tests with coverage thresholds, pack contents, and automated semver releases to npm
|
|
26
33
|
- [PolyForm Shield 1.0.0](./LICENSE) (source-available; apps may use the SDK, competing SDKs may not)
|
package/README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
<img width="1024" height="725" alt="image" src="https://github.com/user-attachments/assets/fadcd831-4187-42b1-b03b-3228ab2d8f32" />
|
|
2
|
+
|
|
2
3
|
|
|
3
4
|
# @appeeky/expo-healthkit
|
|
4
5
|
|
|
@@ -395,6 +396,21 @@ Copy [`.cursor/skills/expo-healthkit/`](https://github.com/appeeky/expo-healthki
|
|
|
395
396
|
|
|
396
397
|
The skill tells the agent to pass Apple identifier strings through one query/save API on iOS and Android, request every type in `toRead`, and rebuild native code after Swift, Kotlin, or plugin changes.
|
|
397
398
|
|
|
399
|
+
## 🚀 Releases
|
|
400
|
+
|
|
401
|
+
Versions follow [semver](https://semver.org/). Pushing to `main` with a [Conventional Commit](https://www.conventionalcommits.org/) runs `.github/workflows/release.yml`: bump `package.json`, update `CHANGELOG.md`, GitHub Release, and `npm publish`.
|
|
402
|
+
|
|
403
|
+
| Commit | npm bump |
|
|
404
|
+
| --- | --- |
|
|
405
|
+
| `fix: …` | patch (`0.1.0` → `0.1.1`) |
|
|
406
|
+
| `feat: …` | minor (`0.1.0` → `0.2.0`) |
|
|
407
|
+
| `feat!: …` or `BREAKING CHANGE:` in the body | major (`0.1.0` → `1.0.0`) |
|
|
408
|
+
| `docs:`, `chore:`, `ci:`, `test:` | no release |
|
|
409
|
+
|
|
410
|
+
Squash-merge PRs so the merge commit (or PR title) starts with `fix:` / `feat:`. `chore:` and `ci:` do not publish.
|
|
411
|
+
|
|
412
|
+
npm publishes from GitHub Actions via [trusted publishing](https://docs.npmjs.com/trusted-publishers) (no `NPM_TOKEN`). One-time setup: package Settings → Trusted Publisher → GitHub Actions, repository `appeeky/expo-healthkit`, workflow filename `release.yml`, and allow **`npm publish`** (not only staged publish).
|
|
413
|
+
|
|
398
414
|
## 🤝 Contributing
|
|
399
415
|
|
|
400
416
|
Issues and pull requests are welcome. Contributions are licensed under [PolyForm Shield 1.0.0](./LICENSE).
|
|
@@ -438,7 +454,7 @@ npx expo run:android
|
|
|
438
454
|
|
|
439
455
|
### Docs
|
|
440
456
|
|
|
441
|
-
If the public API or Android mapping changes, update `README.md
|
|
457
|
+
If the public API or Android mapping changes, update `README.md` and `.cursor/skills/expo-healthkit/` (`SKILL.md`, `reference.md`, `examples.md`) in the same PR. `CHANGELOG.md` is generated on release from conventional commits.
|
|
442
458
|
|
|
443
459
|
## 🔒 Privacy
|
|
444
460
|
|
|
@@ -450,4 +466,4 @@ Health data stays on-device unless your app uploads it. Request only the types y
|
|
|
450
466
|
|
|
451
467
|
You **can** install and use this SDK inside your own Expo / React Native apps, including commercial products.
|
|
452
468
|
|
|
453
|
-
You **cannot** copy this SDK and sell, publish, or otherwise offer a competing HealthKit / Health Connect library based on it.
|
|
469
|
+
You **cannot** copy this SDK and sell, publish, or otherwise offer a competing HealthKit / Health Connect library based on it.
|
|
@@ -47,6 +47,7 @@ import androidx.health.connect.client.units.Temperature
|
|
|
47
47
|
import androidx.health.connect.client.units.Volume
|
|
48
48
|
import java.time.Duration
|
|
49
49
|
import java.time.Instant
|
|
50
|
+
import java.time.LocalDateTime
|
|
50
51
|
import java.time.Period
|
|
51
52
|
import java.time.ZoneId
|
|
52
53
|
import java.time.ZoneOffset
|
|
@@ -126,7 +127,7 @@ internal class HealthConnectService(
|
|
|
126
127
|
val samples = sessions.flatMap { session -> sleepSamples(session) }
|
|
127
128
|
applyLimit(if (ascending) samples else samples.reversed(), limit)
|
|
128
129
|
}
|
|
129
|
-
else -> throw
|
|
130
|
+
else -> throw HealthConnectUnsupportedException(type)
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
|
|
@@ -205,7 +206,7 @@ internal class HealthConnectService(
|
|
|
205
206
|
correlation(record.metadata, type, record.startTime, record.endTime, objects)
|
|
206
207
|
}
|
|
207
208
|
}
|
|
208
|
-
else ->
|
|
209
|
+
else -> unmappedType(type)
|
|
209
210
|
}
|
|
210
211
|
}
|
|
211
212
|
|
|
@@ -255,7 +256,7 @@ internal class HealthConnectService(
|
|
|
255
256
|
else -> Period.ofDays(1)
|
|
256
257
|
}
|
|
257
258
|
client().aggregateGroupByPeriod(
|
|
258
|
-
AggregateGroupByPeriodRequest(metrics,
|
|
259
|
+
AggregateGroupByPeriodRequest(metrics, localTimeFilter(options), period)
|
|
259
260
|
).map { result ->
|
|
260
261
|
val start = result.startTime.atZone(ZoneId.systemDefault()).toInstant()
|
|
261
262
|
val end = result.endTime.atZone(ZoneId.systemDefault()).toInstant()
|
|
@@ -268,7 +269,7 @@ internal class HealthConnectService(
|
|
|
268
269
|
assertAvailable()
|
|
269
270
|
val type = requiredString(options, "type")
|
|
270
271
|
val unit = options["unit"] as? String ?: ""
|
|
271
|
-
val recordClass = HealthConnectMapping.recordClass(type) ?:
|
|
272
|
+
val recordClass = HealthConnectMapping.recordClass(type) ?: unmappedType(type)
|
|
272
273
|
val anchor = options["anchor"] as? String
|
|
273
274
|
val client = client()
|
|
274
275
|
if (anchor.isNullOrBlank()) {
|
|
@@ -433,7 +434,7 @@ internal class HealthConnectService(
|
|
|
433
434
|
val range = ensureRange(start, end)
|
|
434
435
|
HealthConnectNutrition.build(range.first, range.second, zoneOffset(range.first), zoneOffset(range.second), metadata(), values)
|
|
435
436
|
}
|
|
436
|
-
else ->
|
|
437
|
+
else -> unmappedType(type)
|
|
437
438
|
}
|
|
438
439
|
return client().insertRecords(listOf(record)).recordIdsList.first()
|
|
439
440
|
}
|
|
@@ -441,7 +442,7 @@ internal class HealthConnectService(
|
|
|
441
442
|
suspend fun deleteObjects(options: Map<String, Any?>): Int {
|
|
442
443
|
assertAvailable()
|
|
443
444
|
val type = options["type"] as? String ?: throw MissingDeleteTypeException()
|
|
444
|
-
val recordClass = HealthConnectMapping.recordClass(type) ?:
|
|
445
|
+
val recordClass = HealthConnectMapping.recordClass(type) ?: unmappedType(type)
|
|
445
446
|
val uuid = options["uuid"] as? String
|
|
446
447
|
val client = client()
|
|
447
448
|
if (!uuid.isNullOrBlank()) {
|
|
@@ -485,7 +486,7 @@ internal class HealthConnectService(
|
|
|
485
486
|
}
|
|
486
487
|
HealthConnectMapping.BASAL_ENERGY -> emptyList()
|
|
487
488
|
else -> {
|
|
488
|
-
val recordClass = HealthConnectMapping.recordClass(type) ?:
|
|
489
|
+
val recordClass = HealthConnectMapping.recordClass(type) ?: unmappedType(type)
|
|
489
490
|
readAll(recordClass, filter, ascending, 0).flatMap { record ->
|
|
490
491
|
quantitySamplesFromRecord(type, unit, record)
|
|
491
492
|
}
|
|
@@ -1002,6 +1003,19 @@ internal class HealthConnectService(
|
|
|
1002
1003
|
}
|
|
1003
1004
|
}
|
|
1004
1005
|
|
|
1006
|
+
private fun localTimeFilter(options: Map<String, Any?>): TimeRangeFilter {
|
|
1007
|
+
val zone = ZoneId.systemDefault()
|
|
1008
|
+
val from = (options["from"] as? String)?.let { parseInstant(it).atZone(zone).toLocalDateTime() }
|
|
1009
|
+
val to = (options["to"] as? String)?.let { parseInstant(it).atZone(zone).toLocalDateTime() }
|
|
1010
|
+
return when {
|
|
1011
|
+
from != null && to != null ->
|
|
1012
|
+
TimeRangeFilter.between(from, if (to.isAfter(from)) to else from.plusSeconds(1))
|
|
1013
|
+
from != null -> TimeRangeFilter.after(from)
|
|
1014
|
+
to != null -> TimeRangeFilter.before(to)
|
|
1015
|
+
else -> TimeRangeFilter.after(LocalDateTime.ofInstant(Instant.EPOCH, zone))
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1005
1019
|
private fun filterStart(options: Map<String, Any?>): Instant =
|
|
1006
1020
|
(options["from"] as? String)?.let { parseInstant(it) } ?: Instant.EPOCH
|
|
1007
1021
|
|
|
@@ -1026,6 +1040,13 @@ internal class HealthConnectService(
|
|
|
1026
1040
|
return start to end
|
|
1027
1041
|
}
|
|
1028
1042
|
|
|
1043
|
+
private fun unmappedType(type: String): Nothing {
|
|
1044
|
+
if (type.startsWith("HK")) {
|
|
1045
|
+
throw HealthConnectUnsupportedException(type)
|
|
1046
|
+
}
|
|
1047
|
+
throw InvalidIdentifierException(type)
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1029
1050
|
private fun requiredString(options: Map<String, Any?>, key: String): String =
|
|
1030
1051
|
options[key] as? String ?: throw InvalidIdentifierException(key)
|
|
1031
1052
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appeeky/expo-healthkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Apple HealthKit and Android Health Connect for Expo. One identifier-based JavaScript API on iOS and Android.",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"test:coverage": "node internal/module_scripts/test.js --coverage --watchAll=false",
|
|
24
24
|
"prepare": "node internal/module_scripts/prepare.js",
|
|
25
25
|
"prepublishOnly": "node internal/module_scripts/prepare.js",
|
|
26
|
+
"release": "semantic-release",
|
|
26
27
|
"open:ios": "node internal/module_scripts/open-ios.js",
|
|
27
28
|
"open:android": "node internal/module_scripts/open-android.js"
|
|
28
29
|
},
|
|
@@ -55,6 +56,8 @@
|
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"@babel/core": "^7.26.0",
|
|
59
|
+
"@semantic-release/changelog": "^7.0.0",
|
|
60
|
+
"@semantic-release/git": "^11.0.1",
|
|
58
61
|
"@types/jest": "^29.2.1",
|
|
59
62
|
"@types/react": "~19.1.1",
|
|
60
63
|
"babel-preset-expo": "~57.0.10",
|
|
@@ -64,6 +67,7 @@
|
|
|
64
67
|
"jest": "^29.7.0",
|
|
65
68
|
"prettier": "^3.0.0",
|
|
66
69
|
"react-native": "0.82.1",
|
|
70
|
+
"semantic-release": "^25.0.9",
|
|
67
71
|
"typescript": "^5.9.2"
|
|
68
72
|
},
|
|
69
73
|
"jest": {
|