@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.
- package/README.md +22 -3
- package/ReactNativeHealthkit.podspec +17 -6
- package/app.plugin.js +18 -34
- package/app.plugin.ts +25 -50
- package/ios/CategoryTypeModule.swift +1 -0
- package/ios/CharacteristicTypeModule.swift +7 -6
- package/ios/Constants.swift +9 -34
- package/ios/CoreModule.swift +72 -41
- package/ios/CorrelationTypeModule.swift +3 -2
- package/ios/ElectrocardiogramModule.swift +4 -3
- package/ios/HeartbeatSeriesModule.swift +4 -3
- package/ios/Helpers.swift +10 -243
- package/ios/MedicationModule.swift +9 -8
- package/ios/MemoryEstimation.swift +1 -0
- package/ios/PredicateHelpers.swift +16 -86
- package/ios/QuantityTypeModule.swift +3 -2
- package/ios/Serializers.swift +16 -67
- package/ios/SourceProxy.swift +1 -0
- package/ios/StateOfMindModule.swift +3 -2
- package/ios/WorkoutProxy.swift +3 -2
- package/ios/WorkoutsModule.swift +4 -3
- package/lib/commonjs/generated/healthkit.generated.js +3 -0
- package/lib/commonjs/healthkit.ios.js +4 -2
- package/lib/commonjs/healthkit.js +21 -78
- package/lib/commonjs/types/Auth.js +3 -18
- package/lib/commonjs/types/Background.js +2 -10
- package/lib/commonjs/types/QueryOptions.js +2 -17
- package/lib/module/generated/healthkit.generated.js +3 -0
- package/lib/module/healthkit.ios.js +2 -0
- package/lib/module/healthkit.js +20 -77
- package/lib/module/types/Auth.js +1 -18
- package/lib/module/types/Background.js +1 -10
- package/lib/module/types/QueryOptions.js +1 -17
- package/lib/typescript/generated/healthkit.generated.d.ts +6 -2
- package/lib/typescript/healthkit.d.ts +3 -2
- package/lib/typescript/healthkit.ios.d.ts +11 -9
- package/lib/typescript/specs/CoreModule.nitro.d.ts +16 -0
- package/lib/typescript/specs/SourceProxy.nitro.d.ts +2 -4
- package/lib/typescript/types/Auth.d.ts +1 -16
- package/lib/typescript/types/Background.d.ts +1 -9
- package/lib/typescript/types/Device.d.ts +1 -13
- package/lib/typescript/types/QuantityType.d.ts +1 -1
- package/lib/typescript/types/QueryOptions.d.ts +3 -37
- package/lib/typescript/types/Shared.d.ts +2 -25
- package/lib/typescript/types/Source.d.ts +1 -10
- package/lib/typescript/types/Subscriptions.d.ts +2 -3
- package/nitrogen/generated/ios/ReactNativeHealthkit-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/ReactNativeHealthkit-Swift-Cxx-Bridge.hpp +68 -0
- package/nitrogen/generated/ios/c++/HybridCategoryTypeModuleSpecSwift.hpp +1 -1
- package/nitrogen/generated/ios/c++/HybridCoreModuleSpecSwift.hpp +8 -0
- package/nitrogen/generated/ios/c++/HybridCorrelationTypeModuleSpecSwift.hpp +1 -1
- package/nitrogen/generated/ios/c++/HybridElectrocardiogramModuleSpecSwift.hpp +1 -1
- package/nitrogen/generated/ios/c++/HybridHeartbeatSeriesModuleSpecSwift.hpp +1 -1
- package/nitrogen/generated/ios/c++/HybridMedicationModuleSpecSwift.hpp +1 -1
- package/nitrogen/generated/ios/c++/HybridQuantityTypeModuleSpecSwift.hpp +1 -1
- package/nitrogen/generated/ios/c++/HybridStateOfMindModuleSpecSwift.hpp +1 -1
- package/nitrogen/generated/ios/swift/CategorySample.swift +8 -8
- package/nitrogen/generated/ios/swift/CorrelationSample.swift +8 -8
- package/nitrogen/generated/ios/swift/ElectrocardiogramSample.swift +8 -8
- package/nitrogen/generated/ios/swift/Func_void_std__unordered_map_std__string__std__chrono__system_clock__time_point_.swift +54 -0
- package/nitrogen/generated/ios/swift/HeartbeatSeriesSample.swift +8 -8
- package/nitrogen/generated/ios/swift/HybridCoreModuleSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridCoreModuleSpec_cxx.swift +25 -0
- package/nitrogen/generated/ios/swift/MedicationDoseEvent.swift +8 -8
- package/nitrogen/generated/ios/swift/ObjectTypeIdentifier.swift +4 -0
- package/nitrogen/generated/ios/swift/QuantitySample.swift +8 -8
- package/nitrogen/generated/ios/swift/QuantityTypeIdentifier.swift +4 -0
- package/nitrogen/generated/ios/swift/QuantityTypeIdentifierWriteable.swift +4 -0
- package/nitrogen/generated/ios/swift/SampleTypeIdentifier.swift +4 -0
- package/nitrogen/generated/ios/swift/SampleTypeIdentifierWriteable.swift +4 -0
- package/nitrogen/generated/ios/swift/StateOfMindSample.swift +8 -8
- package/nitrogen/generated/shared/c++/CategorySample.hpp +7 -7
- package/nitrogen/generated/shared/c++/CorrelationSample.hpp +7 -7
- package/nitrogen/generated/shared/c++/ElectrocardiogramSample.hpp +7 -7
- package/nitrogen/generated/shared/c++/HeartbeatSeriesSample.hpp +7 -7
- package/nitrogen/generated/shared/c++/HybridCoreModuleSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridCoreModuleSpec.hpp +3 -1
- package/nitrogen/generated/shared/c++/MedicationDoseEvent.hpp +7 -7
- package/nitrogen/generated/shared/c++/ObjectTypeIdentifier.hpp +49 -45
- package/nitrogen/generated/shared/c++/QuantitySample.hpp +7 -7
- package/nitrogen/generated/shared/c++/QuantityTypeIdentifier.hpp +42 -38
- package/nitrogen/generated/shared/c++/QuantityTypeIdentifierWriteable.hpp +42 -38
- package/nitrogen/generated/shared/c++/SampleTypeIdentifier.hpp +48 -44
- package/nitrogen/generated/shared/c++/SampleTypeIdentifierWriteable.hpp +47 -43
- package/nitrogen/generated/shared/c++/StateOfMindSample.hpp +7 -7
- package/package.json +4 -1
- package/src/generated/healthkit-schema.json +13 -0
- package/src/generated/healthkit.generated.ts +5 -0
- package/src/healthkit.ios.ts +3 -0
- package/src/healthkit.ts +27 -84
- package/src/specs/CoreModule.nitro.ts +19 -0
- package/src/specs/SourceProxy.nitro.ts +2 -4
- package/src/test-setup.ts +1 -0
- package/src/types/Auth.ts +4 -17
- package/src/types/Background.ts +1 -9
- package/src/types/Device.ts +1 -13
- package/src/types/QuantityType.ts +1 -1
- package/src/types/QueryOptions.ts +7 -65
- package/src/types/Shared.ts +7 -28
- package/src/types/Source.ts +1 -11
- package/src/types/Subscriptions.ts +2 -3
- package/ios/BackgroundDeliveryManager.swift +0 -284
- package/ios/BackgroundLaunchHook.mm +0 -41
- package/ios/Bridge.h +0 -10
- package/ios/ExceptionCatcher.h +0 -5
- package/ios/ExceptionCatcher.mm +0 -32
- 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 |
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
46
|
-
|
|
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
|
|
23
|
-
*
|
|
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
|
-
[
|
|
75
|
-
|
|
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 CharacteristicTypeModule: HybridCharacteristicTypeModuleSpec {
|
|
5
6
|
func getBloodTypeAsync() -> Promise<BloodType> {
|
|
@@ -32,10 +33,10 @@ class CharacteristicTypeModule: HybridCharacteristicTypeModuleSpec {
|
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
//
|
|
36
|
+
// Uses the healthStore shared through ReactNativeHealthkitCore
|
|
36
37
|
|
|
37
38
|
func getBiologicalSex() throws -> BiologicalSex {
|
|
38
|
-
let biologicalSexObject = try
|
|
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
|
|
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
|
|
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
|
|
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
|
|
95
|
+
let wheelchairUseObject = try healthStore.wheelchairUse()
|
|
95
96
|
if let wheelChairUse = WheelchairUse(
|
|
96
97
|
rawValue: Int32(wheelchairUseObject.wheelchairUse.rawValue)) {
|
|
97
98
|
return wheelChairUse
|
package/ios/Constants.swift
CHANGED
|
@@ -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
|
-
}
|
package/ios/CoreModule.swift
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
}
|
|
179
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
}
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|