@kingstinct/react-native-healthkit 15.1.0 → 16.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.
Files changed (107) hide show
  1. package/README.md +22 -3
  2. package/ReactNativeHealthkit.podspec +17 -6
  3. package/app.plugin.js +18 -34
  4. package/app.plugin.ts +25 -50
  5. package/ios/CategoryTypeModule.swift +1 -0
  6. package/ios/CharacteristicTypeModule.swift +7 -6
  7. package/ios/Constants.swift +9 -34
  8. package/ios/CoreModule.swift +72 -41
  9. package/ios/CorrelationTypeModule.swift +3 -2
  10. package/ios/ElectrocardiogramModule.swift +4 -3
  11. package/ios/HeartbeatSeriesModule.swift +4 -3
  12. package/ios/Helpers.swift +10 -243
  13. package/ios/MedicationModule.swift +9 -8
  14. package/ios/MemoryEstimation.swift +1 -0
  15. package/ios/PredicateHelpers.swift +16 -86
  16. package/ios/QuantityTypeModule.swift +3 -2
  17. package/ios/Serializers.swift +16 -67
  18. package/ios/SourceProxy.swift +1 -0
  19. package/ios/StateOfMindModule.swift +3 -2
  20. package/ios/WorkoutProxy.swift +3 -2
  21. package/ios/WorkoutsModule.swift +4 -3
  22. package/lib/commonjs/generated/healthkit.generated.js +3 -0
  23. package/lib/commonjs/healthkit.ios.js +4 -2
  24. package/lib/commonjs/healthkit.js +21 -78
  25. package/lib/commonjs/types/Auth.js +3 -18
  26. package/lib/commonjs/types/Background.js +2 -10
  27. package/lib/commonjs/types/QueryOptions.js +2 -17
  28. package/lib/module/generated/healthkit.generated.js +3 -0
  29. package/lib/module/healthkit.ios.js +2 -0
  30. package/lib/module/healthkit.js +20 -77
  31. package/lib/module/types/Auth.js +1 -18
  32. package/lib/module/types/Background.js +1 -10
  33. package/lib/module/types/QueryOptions.js +1 -17
  34. package/lib/typescript/generated/healthkit.generated.d.ts +6 -2
  35. package/lib/typescript/healthkit.d.ts +3 -2
  36. package/lib/typescript/healthkit.ios.d.ts +11 -9
  37. package/lib/typescript/specs/CoreModule.nitro.d.ts +16 -0
  38. package/lib/typescript/specs/SourceProxy.nitro.d.ts +2 -4
  39. package/lib/typescript/types/Auth.d.ts +1 -16
  40. package/lib/typescript/types/Background.d.ts +1 -9
  41. package/lib/typescript/types/Device.d.ts +1 -13
  42. package/lib/typescript/types/QuantityType.d.ts +1 -1
  43. package/lib/typescript/types/QueryOptions.d.ts +3 -37
  44. package/lib/typescript/types/Shared.d.ts +2 -25
  45. package/lib/typescript/types/Source.d.ts +1 -10
  46. package/lib/typescript/types/Subscriptions.d.ts +2 -3
  47. package/nitrogen/generated/ios/ReactNativeHealthkit-Swift-Cxx-Bridge.cpp +8 -0
  48. package/nitrogen/generated/ios/ReactNativeHealthkit-Swift-Cxx-Bridge.hpp +68 -0
  49. package/nitrogen/generated/ios/c++/HybridCategoryTypeModuleSpecSwift.hpp +1 -1
  50. package/nitrogen/generated/ios/c++/HybridCoreModuleSpecSwift.hpp +8 -0
  51. package/nitrogen/generated/ios/c++/HybridCorrelationTypeModuleSpecSwift.hpp +1 -1
  52. package/nitrogen/generated/ios/c++/HybridElectrocardiogramModuleSpecSwift.hpp +1 -1
  53. package/nitrogen/generated/ios/c++/HybridHeartbeatSeriesModuleSpecSwift.hpp +1 -1
  54. package/nitrogen/generated/ios/c++/HybridMedicationModuleSpecSwift.hpp +1 -1
  55. package/nitrogen/generated/ios/c++/HybridQuantityTypeModuleSpecSwift.hpp +1 -1
  56. package/nitrogen/generated/ios/c++/HybridStateOfMindModuleSpecSwift.hpp +1 -1
  57. package/nitrogen/generated/ios/swift/CategorySample.swift +8 -8
  58. package/nitrogen/generated/ios/swift/CorrelationSample.swift +8 -8
  59. package/nitrogen/generated/ios/swift/ElectrocardiogramSample.swift +8 -8
  60. package/nitrogen/generated/ios/swift/Func_void_std__unordered_map_std__string__std__chrono__system_clock__time_point_.swift +54 -0
  61. package/nitrogen/generated/ios/swift/HeartbeatSeriesSample.swift +8 -8
  62. package/nitrogen/generated/ios/swift/HybridCoreModuleSpec.swift +1 -0
  63. package/nitrogen/generated/ios/swift/HybridCoreModuleSpec_cxx.swift +25 -0
  64. package/nitrogen/generated/ios/swift/MedicationDoseEvent.swift +8 -8
  65. package/nitrogen/generated/ios/swift/ObjectTypeIdentifier.swift +4 -0
  66. package/nitrogen/generated/ios/swift/QuantitySample.swift +8 -8
  67. package/nitrogen/generated/ios/swift/QuantityTypeIdentifier.swift +4 -0
  68. package/nitrogen/generated/ios/swift/QuantityTypeIdentifierWriteable.swift +4 -0
  69. package/nitrogen/generated/ios/swift/SampleTypeIdentifier.swift +4 -0
  70. package/nitrogen/generated/ios/swift/SampleTypeIdentifierWriteable.swift +4 -0
  71. package/nitrogen/generated/ios/swift/StateOfMindSample.swift +8 -8
  72. package/nitrogen/generated/shared/c++/CategorySample.hpp +7 -7
  73. package/nitrogen/generated/shared/c++/CorrelationSample.hpp +7 -7
  74. package/nitrogen/generated/shared/c++/ElectrocardiogramSample.hpp +7 -7
  75. package/nitrogen/generated/shared/c++/HeartbeatSeriesSample.hpp +7 -7
  76. package/nitrogen/generated/shared/c++/HybridCoreModuleSpec.cpp +1 -0
  77. package/nitrogen/generated/shared/c++/HybridCoreModuleSpec.hpp +3 -1
  78. package/nitrogen/generated/shared/c++/MedicationDoseEvent.hpp +7 -7
  79. package/nitrogen/generated/shared/c++/ObjectTypeIdentifier.hpp +49 -45
  80. package/nitrogen/generated/shared/c++/QuantitySample.hpp +7 -7
  81. package/nitrogen/generated/shared/c++/QuantityTypeIdentifier.hpp +42 -38
  82. package/nitrogen/generated/shared/c++/QuantityTypeIdentifierWriteable.hpp +42 -38
  83. package/nitrogen/generated/shared/c++/SampleTypeIdentifier.hpp +48 -44
  84. package/nitrogen/generated/shared/c++/SampleTypeIdentifierWriteable.hpp +47 -43
  85. package/nitrogen/generated/shared/c++/StateOfMindSample.hpp +7 -7
  86. package/package.json +4 -1
  87. package/src/generated/healthkit-schema.json +13 -0
  88. package/src/generated/healthkit.generated.ts +5 -0
  89. package/src/healthkit.ios.ts +3 -0
  90. package/src/healthkit.ts +27 -84
  91. package/src/specs/CoreModule.nitro.ts +19 -0
  92. package/src/specs/SourceProxy.nitro.ts +2 -4
  93. package/src/test-setup.ts +1 -0
  94. package/src/types/Auth.ts +4 -17
  95. package/src/types/Background.ts +1 -9
  96. package/src/types/Device.ts +1 -13
  97. package/src/types/QuantityType.ts +1 -1
  98. package/src/types/QueryOptions.ts +7 -65
  99. package/src/types/Shared.ts +7 -28
  100. package/src/types/Source.ts +1 -11
  101. package/src/types/Subscriptions.ts +2 -3
  102. package/ios/BackgroundDeliveryManager.swift +0 -284
  103. package/ios/BackgroundLaunchHook.mm +0 -41
  104. package/ios/Bridge.h +0 -10
  105. package/ios/ExceptionCatcher.h +0 -5
  106. package/ios/ExceptionCatcher.mm +0 -32
  107. package/ios/module.modulemap +0 -5
package/README.md CHANGED
@@ -15,7 +15,7 @@ React Native bindings for HealthKit with full TypeScript and Promise support cov
15
15
  | 75+ Workout Activity Types | ✅ | ✅ | ✅ | Swimming, running, table tennis etc.. |
16
16
  | Correlation Types | ✅ | ✅ | ✅ | Food and blood pressure |
17
17
  | Document Types | ✅ | ❌ | ✅ | [CDA documents](https://developer.apple.com/documentation/healthkit/hkcdadocument) exposed as Base64 data |
18
- | Clinical Records | ⚠️ | ❌ | ⚠️ | Lab results etc in [FHIR JSON format](https://www.hl7.org/fhir/json.html) (see [Clinical Records](https://github.com/kingstinct/react-native-healthkit#clinical-records)) |
18
+ | Clinical Records | ✅ | ❌ | ✅ | Lab results, immunizations etc. as [FHIR](https://www.hl7.org/fhir/json.html) via [`@react-native-healthkit/health-records`](#clinical-records) |
19
19
 
20
20
  ### Disclaimer
21
21
 
@@ -23,6 +23,8 @@ This library is provided as-is without any warranty and is not affiliated with A
23
23
 
24
24
  ## Installation
25
25
 
26
+ [`@react-native-healthkit/core`](packages/core) holds the code shared with [`@react-native-healthkit/health-records`](packages/health-records): types, config plugin building blocks and the `ReactNativeHealthkitCore` pod. It is a regular dependency pinned to an exact version, so your package manager installs the matching copy for you and there is no version to keep in sync. In a bare React Native app you additionally need it in your own `package.json`, because the React Native CLI only autolinks packages listed there.
27
+
26
28
  ### Expo
27
29
  Usage with Expo is possible - just keep in mind it will not work in Expo Go and [you'll need to roll your own Dev Client](https://docs.expo.dev/development/getting-started/).
28
30
 
@@ -52,7 +54,9 @@ this will give you defaults that make the app build without any further configur
52
54
  3. Build a new Dev Client
53
55
 
54
56
  ### Native or Expo Bare Workflow
55
- 1. `yarn add @kingstinct/react-native-healthkit react-native-nitro-modules`
57
+ 1. `yarn add @kingstinct/react-native-healthkit @react-native-healthkit/core react-native-nitro-modules`
58
+
59
+ `@react-native-healthkit/core` is installed automatically as a dependency, but the React Native CLI only autolinks packages listed in your own `package.json`, so name it explicitly here.
56
60
  2. `npx pod-install`
57
61
  3. Set `NSHealthUpdateUsageDescription` and `NSHealthShareUsageDescription` in your `Info.plist`
58
62
  4. Enable the HealthKit capability for the project in Xcode.
@@ -202,7 +206,22 @@ We're striving to do as straight a mapping as possible to the Native Libraries.
202
206
 
203
207
  ## Clinical Records
204
208
 
205
- For accessing Clinical Records use old version (3.x) or use specific branch "including-clinical-records". The reason is we cannot refer to this code natively in apps without getting approval from Apple, this could probably be solved by the config plugin but we haven't had time to look into it yet.
209
+ Clinical health records (allergies, conditions, immunizations, lab results, medications, procedures, vital signs, clinical notes and coverage) live in a separate package, [`@react-native-healthkit/health-records`](https://github.com/kingstinct/react-native-healthkit/tree/master/packages/health-records), so apps that don't need them avoid the extra entitlement and App Review scrutiny.
210
+
211
+ - Records are read-only and delivered as FHIR resources, with anchored queries, change subscriptions and background delivery.
212
+ - The app needs the HealthKit **Clinical Health Records** capability (`health-records` in `com.apple.developer.healthkit.access`) and an `NSHealthClinicalHealthRecordsShareUsageDescription`. Its Expo config plugin sets up both.
213
+ - Health Records are only available in supported regions and never on the simulator, so check `supportsHealthRecords()` before offering the feature.
214
+
215
+ ```ts
216
+ import { supportsHealthRecords, requestAuthorization, queryClinicalRecords } from '@react-native-healthkit/health-records'
217
+
218
+ if (supportsHealthRecords()) {
219
+ await requestAuthorization(['HKClinicalTypeIdentifierLabResultRecord'])
220
+ const labResults = await queryClinicalRecords('HKClinicalTypeIdentifierLabResultRecord', { limit: 0 })
221
+ }
222
+ ```
223
+
224
+ See the [health-records README](https://github.com/kingstinct/react-native-healthkit/blob/master/packages/health-records/README.md) for installation, configuration and the full API.
206
225
 
207
226
  ## Android alternatives
208
227
 
@@ -1,7 +1,12 @@
1
1
  require "json"
2
+ require "pathname"
2
3
 
3
4
  package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
5
 
6
+ # Resolved like Node resolves the dependency, so monorepo and node_modules layouts both work.
7
+ core_ios_dir = Pathname.new(File.join(File.dirname(`node --print "require.resolve('@react-native-healthkit/core/package.json', { paths: ['#{__dir__}'] })"`.strip), "ios"))
8
+ .relative_path_from(Pathname.new(__dir__)).to_s
9
+
5
10
  Pod::Spec.new do |s|
6
11
  s.name = "ReactNativeHealthkit"
7
12
  s.version = package["version"]
@@ -16,20 +21,17 @@ Pod::Spec.new do |s|
16
21
  s.source_files = [
17
22
  # Implementation (Swift)
18
23
  "ios/**/*.{swift}",
19
- # Autolinking/Registration (Objective-C++)
20
- "ios/**/*.{m,mm}",
21
- # Implementation (C++ objects)
24
+ # Implementation (C++ objects)
22
25
  "cpp/**/*.{hpp,cpp}",
23
26
  ]
24
27
 
25
- s.public_header_files = "ios/**/*.h"
26
-
27
28
  s.pod_target_xcconfig = {
28
29
  # C++ compiler flags, mainly for folly.
29
30
  "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) FOLLY_NO_CONFIG FOLLY_CFG_NO_COROUTINES",
30
31
  # Allow importing Objective-C headers in Swift without bridging header
31
32
  "CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES" => "YES",
32
- "SWIFT_INCLUDE_PATHS" => "$(inherited) $(PODS_TARGET_SRCROOT)/ios"
33
+ # Core's `internal import ReactNativeHealthkitCore_Private` must resolve here too (explicit module builds).
34
+ "SWIFT_INCLUDE_PATHS" => "$(inherited) \"$(PODS_TARGET_SRCROOT)/#{core_ios_dir}\""
33
35
  }
34
36
 
35
37
  load 'nitrogen/generated/ios/ReactNativeHealthkit+autolinking.rb'
@@ -45,6 +47,15 @@ Pod::Spec.new do |s|
45
47
  :execution_position => :before_compile,
46
48
  }
47
49
 
50
+ # The three packages are released in lockstep on a single version (changesets
51
+ # `fixed`), so the only combination this pod is ever built and tested against
52
+ # is a core of the exact same version. Requiring that here turns a mismatch --
53
+ # including a mixed-version family, e.g. this pod at 16.0.0 next to its
54
+ # sibling at 16.1.0 -- into a `pod install` version conflict naming both pods,
55
+ # instead of a Swift compile error inside Pods or a runtime surprise. Core's
56
+ # Swift surface is internal to these packages and is not semver-managed, so a
57
+ # same-major requirement would not be enough.
58
+ s.dependency 'ReactNativeHealthkitCore', s.version.to_s
48
59
  s.dependency 'React-jsi'
49
60
  s.dependency 'React-callinvoker'
50
61
  install_modules_dependencies(s)
package/app.plugin.js CHANGED
@@ -7,43 +7,27 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
7
7
  };
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  const config_plugins_1 = require("@expo/config-plugins");
10
+ const plugin_1 = require("@react-native-healthkit/core/plugin");
10
11
  const package_json_1 = __importDefault(require("./package.json"));
11
- const withEntitlementsPlugin = (config, props) => {
12
- return (0, config_plugins_1.withEntitlementsPlist)(config, (configPlist) => {
13
- configPlist.modResults['com.apple.developer.healthkit'] = true;
14
- // background is enabled by default, but possible to opt-out from
15
- // (haven't seen any drawbacks from having it enabled)
16
- if (props?.background !== false) {
17
- configPlist.modResults['com.apple.developer.healthkit.background-delivery'] = true;
18
- }
19
- return configPlist;
20
- });
21
- };
22
- const withInfoPlistPlugin = (config, props) => {
23
- return (0, config_plugins_1.withInfoPlist)(config, (configPlist) => {
24
- const existingShareDescription = configPlist.modResults.NSHealthShareUsageDescription;
25
- configPlist.modResults.NSHealthShareUsageDescription =
26
- typeof props?.NSHealthShareUsageDescription === 'string'
27
- ? props.NSHealthShareUsageDescription
28
- : typeof existingShareDescription === 'string'
29
- ? existingShareDescription
30
- : `${config.name ?? package_json_1.default.name} wants to read your health data`;
31
- if (props?.NSHealthUpdateUsageDescription !== false) {
32
- const existingUpdateDescription = configPlist.modResults.NSHealthUpdateUsageDescription;
33
- configPlist.modResults.NSHealthUpdateUsageDescription =
34
- typeof props?.NSHealthUpdateUsageDescription === 'string'
35
- ? props.NSHealthUpdateUsageDescription
36
- : typeof existingUpdateDescription === 'string'
37
- ? existingUpdateDescription
38
- : `${config.name ?? package_json_1.default.name} wants to update your health data`;
39
- }
40
- return configPlist;
41
- });
42
- };
43
12
  const healthkitAppPlugin = (config, props) => {
13
+ const withUpdateUsageDescription = (config) => props?.NSHealthUpdateUsageDescription === false
14
+ ? config
15
+ : (0, plugin_1.withUsageDescription)(config, {
16
+ key: 'NSHealthUpdateUsageDescription',
17
+ value: props?.NSHealthUpdateUsageDescription,
18
+ fallback: (appName) => `${appName} wants to update your health data`,
19
+ fallbackAppName: package_json_1.default.name,
20
+ });
44
21
  return (0, config_plugins_1.withPlugins)(config, [
45
- [withEntitlementsPlugin, props],
46
- [withInfoPlistPlugin, props],
22
+ [
23
+ plugin_1.withHealthKit,
24
+ {
25
+ background: props?.background,
26
+ NSHealthShareUsageDescription: props?.NSHealthShareUsageDescription,
27
+ fallbackAppName: package_json_1.default.name,
28
+ },
29
+ ],
30
+ withUpdateUsageDescription,
47
31
  ]);
48
32
  };
49
33
  exports.default = (0, config_plugins_1.createRunOncePlugin)(healthkitAppPlugin, package_json_1.default.name, package_json_1.default.version);
package/app.plugin.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  type ConfigPlugin,
3
3
  createRunOncePlugin,
4
- withEntitlementsPlist,
5
- withInfoPlist,
6
4
  withPlugins,
7
5
  } from '@expo/config-plugins'
6
+ import {
7
+ withHealthKit,
8
+ withUsageDescription,
9
+ } from '@react-native-healthkit/core/plugin'
8
10
 
9
11
  import pkg from './package.json'
10
12
 
@@ -19,60 +21,33 @@ type AppPluginConfig = InfoPlistConfig & {
19
21
  /**
20
22
  * Adds the background-delivery entitlement. Enabled by default; set to
21
23
  * `false` to opt out. Launch-time observer registration needs no AppDelegate
22
- * change: the pod hooks UIApplicationDidFinishLaunchingNotification itself
23
- * (see ios/BackgroundLaunchHook.mm).
24
+ * change: the core pod hooks UIApplicationDidFinishLaunchingNotification
25
+ * itself.
24
26
  */
25
27
  background?: BackgroundConfig
26
28
  }
27
29
 
28
- const withEntitlementsPlugin: ConfigPlugin<{
29
- background?: BackgroundConfig
30
- }> = (config, props) => {
31
- return withEntitlementsPlist(config, (configPlist) => {
32
- configPlist.modResults['com.apple.developer.healthkit'] = true
33
-
34
- // background is enabled by default, but possible to opt-out from
35
- // (haven't seen any drawbacks from having it enabled)
36
- if (props?.background !== false) {
37
- configPlist.modResults[
38
- 'com.apple.developer.healthkit.background-delivery'
39
- ] = true
40
- }
41
-
42
- return configPlist
43
- })
44
- }
45
-
46
- const withInfoPlistPlugin: ConfigPlugin<InfoPlistConfig> = (config, props) => {
47
- return withInfoPlist(config, (configPlist) => {
48
- const existingShareDescription =
49
- configPlist.modResults.NSHealthShareUsageDescription
50
- configPlist.modResults.NSHealthShareUsageDescription =
51
- typeof props?.NSHealthShareUsageDescription === 'string'
52
- ? props.NSHealthShareUsageDescription
53
- : typeof existingShareDescription === 'string'
54
- ? existingShareDescription
55
- : `${config.name ?? pkg.name} wants to read your health data`
56
-
57
- if (props?.NSHealthUpdateUsageDescription !== false) {
58
- const existingUpdateDescription =
59
- configPlist.modResults.NSHealthUpdateUsageDescription
60
- configPlist.modResults.NSHealthUpdateUsageDescription =
61
- typeof props?.NSHealthUpdateUsageDescription === 'string'
62
- ? props.NSHealthUpdateUsageDescription
63
- : typeof existingUpdateDescription === 'string'
64
- ? existingUpdateDescription
65
- : `${config.name ?? pkg.name} wants to update your health data`
66
- }
67
-
68
- return configPlist
69
- })
70
- }
71
-
72
30
  const healthkitAppPlugin: ConfigPlugin<AppPluginConfig> = (config, props) => {
31
+ const withUpdateUsageDescription: ConfigPlugin = (config) =>
32
+ props?.NSHealthUpdateUsageDescription === false
33
+ ? config
34
+ : withUsageDescription(config, {
35
+ key: 'NSHealthUpdateUsageDescription',
36
+ value: props?.NSHealthUpdateUsageDescription,
37
+ fallback: (appName) => `${appName} wants to update your health data`,
38
+ fallbackAppName: pkg.name,
39
+ })
40
+
73
41
  return withPlugins(config, [
74
- [withEntitlementsPlugin, props],
75
- [withInfoPlistPlugin, props],
42
+ [
43
+ withHealthKit,
44
+ {
45
+ background: props?.background,
46
+ NSHealthShareUsageDescription: props?.NSHealthShareUsageDescription,
47
+ fallbackAppName: pkg.name,
48
+ },
49
+ ],
50
+ withUpdateUsageDescription,
76
51
  ])
77
52
  }
78
53
 
@@ -1,5 +1,6 @@
1
1
  import HealthKit
2
2
  import NitroModules
3
+ import ReactNativeHealthkitCore
3
4
 
4
5
  class CategoryTypeModule: HybridCategoryTypeModuleSpec {
5
6
  func saveCategorySample(
@@ -1,5 +1,6 @@
1
1
  import HealthKit
2
2
  import NitroModules
3
+ import ReactNativeHealthkitCore
3
4
 
4
5
  class CharacteristicTypeModule: HybridCharacteristicTypeModuleSpec {
5
6
  func getBloodTypeAsync() -> Promise<BloodType> {
@@ -32,10 +33,10 @@ class CharacteristicTypeModule: HybridCharacteristicTypeModuleSpec {
32
33
  }
33
34
  }
34
35
 
35
- // Using the global 'store' instance defined in Auth.swift
36
+ // Uses the healthStore shared through ReactNativeHealthkitCore
36
37
 
37
38
  func getBiologicalSex() throws -> BiologicalSex {
38
- let biologicalSexObject = try store.biologicalSex()
39
+ let biologicalSexObject = try healthStore.biologicalSex()
39
40
  if let biologicalSex = BiologicalSex(
40
41
  rawValue: Int32(biologicalSexObject.biologicalSex.rawValue)) {
41
42
  return biologicalSex
@@ -46,7 +47,7 @@ class CharacteristicTypeModule: HybridCharacteristicTypeModuleSpec {
46
47
 
47
48
  func getDateOfBirth() throws -> Date? {
48
49
  do {
49
- let components = try store.dateOfBirthComponents()
50
+ let components = try healthStore.dateOfBirthComponents()
50
51
  return components.date
51
52
  } catch {
52
53
  let nsError = error as NSError
@@ -69,7 +70,7 @@ class CharacteristicTypeModule: HybridCharacteristicTypeModuleSpec {
69
70
  }
70
71
 
71
72
  func getBloodType() throws -> BloodType {
72
- let bloodTypeObject = try store.bloodType()
73
+ let bloodTypeObject = try healthStore.bloodType()
73
74
 
74
75
  if let bloodType = BloodType(rawValue: Int32(bloodTypeObject.bloodType.rawValue)) {
75
76
  return bloodType
@@ -80,7 +81,7 @@ class CharacteristicTypeModule: HybridCharacteristicTypeModuleSpec {
80
81
  }
81
82
 
82
83
  func getFitzpatrickSkinType() throws -> FitzpatrickSkinType {
83
- let skinTypeObject = try store.fitzpatrickSkinType()
84
+ let skinTypeObject = try healthStore.fitzpatrickSkinType()
84
85
 
85
86
  if let skinType = FitzpatrickSkinType(rawValue: Int32(skinTypeObject.skinType.rawValue)) {
86
87
  return skinType
@@ -91,7 +92,7 @@ class CharacteristicTypeModule: HybridCharacteristicTypeModuleSpec {
91
92
  }
92
93
 
93
94
  func getWheelchairUse() throws -> WheelchairUse {
94
- let wheelchairUseObject = try store.wheelchairUse()
95
+ let wheelchairUseObject = try healthStore.wheelchairUse()
95
96
  if let wheelChairUse = WheelchairUse(
96
97
  rawValue: Int32(wheelchairUseObject.wheelchairUse.rawValue)) {
97
98
  return wheelChairUse
@@ -7,6 +7,15 @@
7
7
 
8
8
  import Foundation
9
9
  import HealthKit
10
+ import ReactNativeHealthkitCore
11
+
12
+ let logPrefix = "[react-native-healthkit]"
13
+
14
+ /// Namespaces this package's persisted background-delivery types in the shared
15
+ /// BackgroundDeliveryManager. The id matches the UserDefaults keys used before
16
+ /// the manager moved to ReactNativeHealthkitCore, so existing installs keep
17
+ /// their configuration.
18
+ let backgroundDeliveryScope = BackgroundDeliveryScope(id: "com.kingstinct.healthkit")
10
19
 
11
20
  let INIT_ERROR = "HEALTHKIT_INIT_ERROR"
12
21
  let INIT_ERROR_MESSAGE = "HealthKit not initialized"
@@ -14,17 +23,7 @@ let TYPE_IDENTIFIER_ERROR = "HEALTHKIT_TYPE_IDENTIFIER_NOT_RECOGNIZED_ERROR"
14
23
  let QUERY_ERROR = "HEALTHKIT_QUERY_ERROR"
15
24
  let GENERIC_ERROR = "HEALTHKIT_ERROR"
16
25
 
17
- let HKCharacteristicTypeIdentifier_PREFIX = "HKCharacteristicTypeIdentifier"
18
- let HKQuantityTypeIdentifier_PREFIX = "HKQuantityTypeIdentifier"
19
- let HKCategoryTypeIdentifier_PREFIX = "HKCategoryTypeIdentifier"
20
- let HKCorrelationTypeIdentifier_PREFIX = "HKCorrelationTypeIdentifier"
21
26
  let HKActivitySummaryTypeIdentifier = "HKActivitySummaryTypeIdentifier"
22
- let HKAudiogramTypeIdentifier = "HKAudiogramSampleType"
23
- let HKWorkoutTypeIdentifier = "HKWorkoutTypeIdentifier"
24
- let HKWorkoutRouteTypeIdentifier = "HKWorkoutRouteTypeIdentifier"
25
- let HKDataTypeIdentifierHeartbeatSeries = "HKDataTypeIdentifierHeartbeatSeries"
26
- let HKStateOfMindTypeIdentifier = "HKStateOfMindTypeIdentifier"
27
- let HKElectrocardiogramType = "HKElectrocardiogramType"
28
27
 
29
28
  let HKWorkoutActivityTypePropertyName = "activityType"
30
29
  let HKWorkoutSessionLocationTypePropertyName = "locationType"
@@ -34,27 +33,3 @@ let SpeedUnit = HKUnit(from: "m/s") // HKUnit.meter().unitDivided(by: HKUnit.se
34
33
  let METUnit = HKUnit(from: "kcal/hr·kg")
35
34
 
36
35
  let DEFAULT_QUERY_LIMIT = 20
37
-
38
- /// Apple renamed `HKCategoryTypeIdentifierAudioExposureEvent` to
39
- /// `HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent` in iOS 14, but the new
40
- /// constant still carries the old raw string at runtime. Building the identifier from the
41
- /// modern name as a raw string therefore makes `categoryType(forIdentifier:)` return nil,
42
- /// and samples read back from HealthKit report the legacy name. These helpers translate
43
- /// in both directions so JS only ever sees the modern identifier.
44
- let HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent_NAME =
45
- "HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent"
46
- let HKCategoryTypeIdentifierAudioExposureEvent_NAME = "HKCategoryTypeIdentifierAudioExposureEvent"
47
-
48
- func hkCategoryTypeIdentifier(fromIdentifierName name: String) -> HKCategoryTypeIdentifier {
49
- if name == HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent_NAME {
50
- return .environmentalAudioExposureEvent
51
- }
52
- return HKCategoryTypeIdentifier(rawValue: name)
53
- }
54
-
55
- func categoryTypeIdentifierName(fromHealthKitIdentifier identifier: String) -> String {
56
- if identifier == HKCategoryTypeIdentifierAudioExposureEvent_NAME {
57
- return HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent_NAME
58
- }
59
- return identifier
60
- }
@@ -1,5 +1,6 @@
1
1
  import HealthKit
2
2
  import NitroModules
3
+ import ReactNativeHealthkitCore
3
4
 
4
5
  //
5
6
  // Core.swift
@@ -8,8 +9,6 @@ import NitroModules
8
9
  // Created by Robert Herber on 2025-05-28.
9
10
  //
10
11
 
11
- var store = HKHealthStore.init()
12
-
13
12
  // Thread-safe cache with concurrent read/exclusive write access
14
13
  private let quantityTypeCacheQueue = DispatchQueue(
15
14
  label: "com.kingstinct.healthkit.cache",
@@ -57,7 +56,7 @@ async throws -> [HKQuantityType: HKUnit] {
57
56
  }
58
57
 
59
58
  return try await withCheckedThrowingContinuation { continuation in
60
- store.preferredUnits(for: Set(quantityTypes)) {
59
+ healthStore.preferredUnits(for: Set(quantityTypes)) {
61
60
  (typePerUnits: [HKQuantityType: HKUnit], error: Error?) in
62
61
  DispatchQueue.main.async {
63
62
  if let error = error {
@@ -82,7 +81,7 @@ class CoreModule: HybridCoreModuleSpec {
82
81
  return Promise.async {
83
82
  let objectType = try perObjectTypeFrom(objectTypeIdentifier: typeIdentifier)
84
83
  if #available(iOS 16.0, *) {
85
- try await store.requestPerObjectReadAuthorization(
84
+ try await healthStore.requestPerObjectReadAuthorization(
86
85
  for: objectType,
87
86
  predicate: nil
88
87
  )
@@ -92,6 +91,37 @@ class CoreModule: HybridCoreModuleSpec {
92
91
  }
93
92
  }
94
93
 
94
+ func getEarliestAuthorizedSampleDates(objectTypeIdentifiers: [ObjectTypeIdentifier]) -> Promise<
95
+ [String: Date]
96
+ > {
97
+ return Promise.async {
98
+ #if compiler(>=6.4)
99
+ if #available(iOS 27.0, *) {
100
+ let objectTypes = objectTypesFromArray(typeIdentifiers: objectTypeIdentifiers)
101
+
102
+ if objectTypes.isEmpty {
103
+ return [String: Date]()
104
+ }
105
+
106
+ let earliestDates = try await healthStore.earliestAuthorizedSampleDate(for: objectTypes)
107
+
108
+ return earliestDates.reduce(into: [String: Date]()) { result, entry in
109
+ result[entry.key.identifier] = entry.value
110
+ }
111
+ }
112
+ #else
113
+ if #available(iOS 27.0, *) {
114
+ warnWithPrefix(
115
+ "getEarliestAuthorizedSampleDates needs to be built with Xcode 27.0 or later to report limited-access dates"
116
+ )
117
+ }
118
+ #endif
119
+
120
+ // Before iOS 27 there is no limited-access earliest date, so no type has one.
121
+ return [String: Date]()
122
+ }
123
+ }
124
+
95
125
  func currentAppSource() -> any HybridSourceProxySpec {
96
126
  return SourceProxy(source: HKSource.default())
97
127
  }
@@ -133,7 +163,7 @@ class CoreModule: HybridCoreModuleSpec {
133
163
  ) throws -> AuthorizationStatus {
134
164
  let objectType = try objectTypeFrom(objectTypeIdentifier: type)
135
165
 
136
- let authStatus = store.authorizationStatus(for: objectType)
166
+ let authStatus = healthStore.authorizationStatus(for: objectType)
137
167
 
138
168
  if let authStatus = AuthorizationStatus(rawValue: Int32(authStatus.rawValue)) {
139
169
  return authStatus
@@ -157,27 +187,27 @@ class CoreModule: HybridCoreModuleSpec {
157
187
  returning: .unnecessary
158
188
  )
159
189
  }
160
- var caughtError: NSError?
161
- let started = RunBlockCatchingObjCExceptions({
162
- store.getRequestStatusForAuthorization(toShare: toShare, read: toRead) {
163
- status, error in
164
- DispatchQueue.main.async {
165
- if let error = error {
166
- continuation.resume(throwing: error)
167
- } else {
168
- if let authStatus = AuthorizationRequestStatus(rawValue: Int32(status.rawValue)) {
169
- continuation.resume(returning: authStatus)
190
+ do {
191
+ try runCatchingObjCExceptions {
192
+ healthStore.getRequestStatusForAuthorization(toShare: toShare, read: toRead) {
193
+ status, error in
194
+ DispatchQueue.main.async {
195
+ if let error = error {
196
+ continuation.resume(throwing: error)
170
197
  } else {
171
- continuation.resume(
172
- throwing: runtimeErrorWithPrefix(
173
- "Unrecognized authStatus returned: \(status.rawValue)"))
198
+ if let authStatus = AuthorizationRequestStatus(rawValue: Int32(status.rawValue)) {
199
+ continuation.resume(returning: authStatus)
200
+ } else {
201
+ continuation.resume(
202
+ throwing: runtimeErrorWithPrefix(
203
+ "Unrecognized authStatus returned: \(status.rawValue)"))
204
+ }
174
205
  }
175
206
  }
176
207
  }
177
208
  }
178
- }, &caughtError)
179
- if !started, let caughtError {
180
- continuation.resume(throwing: caughtError)
209
+ } catch {
210
+ continuation.resume(throwing: error)
181
211
  }
182
212
  }
183
213
  }
@@ -189,20 +219,20 @@ class CoreModule: HybridCoreModuleSpec {
189
219
  let toRead = objectTypesFromArray(typeIdentifiers: toRequest.toRead ?? [])
190
220
 
191
221
  return try await withCheckedThrowingContinuation { continuation in
192
- var caughtError: NSError?
193
- let started = RunBlockCatchingObjCExceptions({
194
- store.requestAuthorization(toShare: share, read: toRead) { status, error in
195
- DispatchQueue.main.async {
196
- if let error = error {
197
- continuation.resume(throwing: error)
198
- } else {
199
- continuation.resume(returning: status)
222
+ do {
223
+ try runCatchingObjCExceptions {
224
+ healthStore.requestAuthorization(toShare: share, read: toRead) { status, error in
225
+ DispatchQueue.main.async {
226
+ if let error = error {
227
+ continuation.resume(throwing: error)
228
+ } else {
229
+ continuation.resume(returning: status)
230
+ }
200
231
  }
201
232
  }
202
233
  }
203
- }, &caughtError)
204
- if !started, let caughtError {
205
- continuation.resume(throwing: caughtError)
234
+ } catch {
235
+ continuation.resume(throwing: error)
206
236
  }
207
237
  }
208
238
  }
@@ -243,7 +273,7 @@ class CoreModule: HybridCoreModuleSpec {
243
273
  }
244
274
  }
245
275
 
246
- store.execute(query)
276
+ healthStore.execute(query)
247
277
  }
248
278
  }
249
279
  }
@@ -255,7 +285,7 @@ class CoreModule: HybridCoreModuleSpec {
255
285
  if let frequency = HKUpdateFrequency(rawValue: Int(updateFrequency.rawValue)) {
256
286
  let type = try objectTypeFrom(objectTypeIdentifier: typeIdentifier)
257
287
  return try await withCheckedThrowingContinuation { continuation in
258
- store.enableBackgroundDelivery(
288
+ healthStore.enableBackgroundDelivery(
259
289
  for: type,
260
290
  frequency: frequency
261
291
  ) { (success, error) in
@@ -280,7 +310,7 @@ class CoreModule: HybridCoreModuleSpec {
280
310
  return Promise.async {
281
311
  let type = try objectTypeFrom(objectTypeIdentifier: typeIdentifier)
282
312
  return try await withCheckedThrowingContinuation { continuation in
283
- store.disableBackgroundDelivery(
313
+ healthStore.disableBackgroundDelivery(
284
314
  for: type
285
315
  ) { (success, error) in
286
316
  DispatchQueue.main.async {
@@ -297,7 +327,7 @@ class CoreModule: HybridCoreModuleSpec {
297
327
  func disableAllBackgroundDelivery() -> Promise<Bool> {
298
328
  return Promise.async {
299
329
  try await withCheckedThrowingContinuation { continuation in
300
- store.disableAllBackgroundDelivery(completion: { (success, error) in
330
+ healthStore.disableAllBackgroundDelivery(completion: { (success, error) in
301
331
  DispatchQueue.main.async {
302
332
  guard let err = error else {
303
333
  return continuation.resume(returning: success)
@@ -377,7 +407,7 @@ class CoreModule: HybridCoreModuleSpec {
377
407
 
378
408
  let of = try sampleTypeFrom(sampleTypeIdentifierWriteable: objectTypeIdentifier)
379
409
  return try await withCheckedThrowingContinuation { continuation in
380
- store.deleteObjects(of: of, predicate: predicate) { (_, count, error) in
410
+ healthStore.deleteObjects(of: of, predicate: predicate) { (_, count, error) in
381
411
  DispatchQueue.main.async {
382
412
  if let error = error {
383
413
  continuation.resume(throwing: error)
@@ -442,7 +472,7 @@ class CoreModule: HybridCoreModuleSpec {
442
472
 
443
473
  }
444
474
 
445
- store.execute(query)
475
+ healthStore.execute(query)
446
476
 
447
477
  self._runningQueries.updateValue(query, forKey: queryId)
448
478
 
@@ -463,7 +493,7 @@ class CoreModule: HybridCoreModuleSpec {
463
493
  return false
464
494
  }
465
495
 
466
- store.stop(query)
496
+ healthStore.stop(query)
467
497
 
468
498
  self._runningQueries.removeValue(forKey: queryId)
469
499
 
@@ -485,6 +515,7 @@ class CoreModule: HybridCoreModuleSpec {
485
515
  }
486
516
 
487
517
  BackgroundDeliveryManager.shared.configure(
518
+ scope: backgroundDeliveryScope,
488
519
  typeIdentifiers: typeIdentifiers,
489
520
  frequency: frequency
490
521
  )
@@ -495,7 +526,7 @@ class CoreModule: HybridCoreModuleSpec {
495
526
 
496
527
  func clearBackgroundTypes() -> Promise<Bool> {
497
528
  return Promise.async {
498
- BackgroundDeliveryManager.shared.clearConfiguration()
529
+ BackgroundDeliveryManager.shared.clearConfiguration(scope: backgroundDeliveryScope)
499
530
  return true
500
531
  }
501
532
  }
@@ -503,7 +534,7 @@ class CoreModule: HybridCoreModuleSpec {
503
534
  func unsubscribeQueries(queryIds: [String]) -> Double {
504
535
  let successCounts = queryIds.map { queryId in
505
536
  if let query = self._runningQueries[queryId] {
506
- store.stop(query)
537
+ healthStore.stop(query)
507
538
 
508
539
  self._runningQueries.removeValue(forKey: queryId)
509
540
 
@@ -1,5 +1,6 @@
1
1
  import HealthKit
2
2
  import NitroModules
3
+ import ReactNativeHealthkitCore
3
4
 
4
5
  func serializeCorrelationSample(correlation: HKCorrelation, unitMap: [HKQuantityType: HKUnit])
5
6
  -> CorrelationSample {
@@ -28,10 +29,10 @@ func serializeCorrelationSample(correlation: HKCorrelation, unitMap: [HKQuantity
28
29
  startDate: correlation.startDate,
29
30
  endDate: correlation.endDate,
30
31
  hasUndeterminedDuration: correlation.hasUndeterminedDuration,
31
- metadata: serializeMetadata(correlation.metadata),
32
32
  uuid: correlation.uuid.uuidString,
33
33
  sourceRevision: serializeSourceRevision(correlation.sourceRevision),
34
- device: serializeDevice(hkDevice: correlation.device)
34
+ device: serializeDevice(hkDevice: correlation.device),
35
+ metadata: serializeMetadata(correlation.metadata)
35
36
  )
36
37
  }
37
38