@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.
@@ -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 pack contents
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
- ![HealthKit and Health Connect for Expo](https://raw.githubusercontent.com/appeeky/expo-healthkit/main/docs/banner.jpg)
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`, `CHANGELOG.md`, and `.cursor/skills/expo-healthkit/` (`SKILL.md`, `reference.md`, `examples.md`) in the same PR.
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 InvalidIdentifierException(type)
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 -> throw InvalidIdentifierException(type)
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, filter, period)
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) ?: throw InvalidIdentifierException(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 -> throw InvalidIdentifierException(type)
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) ?: throw InvalidIdentifierException(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) ?: throw InvalidIdentifierException(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.0",
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": {