├── .editorconfig ├── .env.github ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── eas-build.yml │ ├── master.yml │ ├── pr.yml │ └── test.yml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── pre-push ├── .solidarity ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app.plugin.js ├── babel.config.js ├── bun.lock ├── docs ├── images │ ├── diet.jpg │ ├── diet.webp │ ├── github-mark-white.png │ ├── github-mark-white.webp │ ├── healthkit.png │ ├── healthkit.webp │ ├── heart-data.jpg │ ├── heart-data.webp │ ├── hollander.jpg │ ├── hollander.webp │ ├── lift.jpg │ ├── lift.webp │ ├── running-sunset.jpg │ ├── running-sunset.webp │ ├── situps.jpg │ ├── situps.webp │ ├── tie-shoes.jpg │ └── tie-shoes.webp ├── index.html ├── scripts │ └── prism.js └── styles │ ├── prism.css │ └── style.css ├── example-expo ├── .expo-shared │ └── assets.json ├── .gitignore ├── App.tsx ├── app.config.js ├── app.json ├── assets │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png ├── babel.config.js ├── bun.lock ├── eas.json ├── metro.config.js ├── package.json └── tsconfig.json ├── example ├── .bundle │ └── config ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── babel.config.js ├── bun.lock ├── index.tsx ├── ios │ ├── File.swift │ ├── HealthkitExample-Bridging-Header.h │ ├── HealthkitExample.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── HealthkitExample.xcscheme │ ├── HealthkitExample.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── HealthkitExample │ │ ├── AppDelegate.h │ │ ├── AppDelegate.mm │ │ ├── HealthkitExample.entitlements │ │ ├── Images.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── LaunchScreen.storyboard │ │ └── main.m │ ├── Podfile │ └── Podfile.lock ├── metro.config.js ├── package.json ├── react-native.config.js ├── src │ ├── App.tsx │ └── utils.ts └── tsconfig.json ├── ios ├── Constants.swift ├── Helpers.swift ├── ReactNativeHealthkit-Bridging-Header.h ├── ReactNativeHealthkit.m ├── ReactNativeHealthkit.swift ├── ReactNativeHealthkit.xcodeproj │ └── project.pbxproj └── Serializers.swift ├── jest.config.json ├── kingstinct-react-native-healthkit.podspec ├── package.json ├── src ├── hooks │ ├── useHealthkitAuthorization.test.ts │ ├── useHealthkitAuthorization.ts │ ├── useIsHealthDataAvailable.test.ts │ ├── useIsHealthDataAvailable.ts │ ├── useMostRecentCategorySample.ts │ ├── useMostRecentQuantitySample.ts │ ├── useMostRecentWorkout.ts │ ├── useSources.ts │ ├── useStatisticsForQuantity.ts │ └── useSubscribeToChanges.ts ├── index.ios.tsx ├── index.native.tsx ├── index.tsx ├── index.web.tsx ├── native-types.ts ├── test-setup.ts ├── test-utils.ts ├── types.ts └── utils │ ├── deleteQuantitySample.ts │ ├── deleteSamples.ts │ ├── deleteWorkoutSample.ts │ ├── deserializeCategorySample.test.ts │ ├── deserializeCategorySample.ts │ ├── deserializeCorrelation.ts │ ├── deserializeHeartbeatSeriesSample.ts │ ├── deserializeSample.ts │ ├── deserializeWorkout.ts │ ├── ensureMetadata.ts │ ├── ensureTotals.ts │ ├── ensureUnit.ts │ ├── getDateOfBirth.ts │ ├── getMostRecentCategorySample.ts │ ├── getMostRecentQuantitySample.ts │ ├── getMostRecentWorkout.ts │ ├── getPreferredUnit.ts │ ├── getPreferredUnits.ts │ ├── getPreferredUnitsTyped.ts │ ├── getRequestStatusForAuthorization.ts │ ├── getWorkoutPlanById.ts │ ├── prepareOptions.ts │ ├── queryCategorySamples.ts │ ├── queryCategorySamplesWithAnchor.ts │ ├── queryCorrelationSamples.ts │ ├── queryHeartbeatSeriesSamples.ts │ ├── queryHeartbeatSeriesSamplesWithAnchor.ts │ ├── queryQuantitySamples.ts │ ├── queryQuantitySamplesWithAnchor.ts │ ├── querySources.ts │ ├── queryStateOfMindSamples.ts │ ├── queryStatisticsCollectionForQuantity.ts │ ├── queryStatisticsForQuantity.ts │ ├── queryWorkoutSamplesWithAnchor.ts │ ├── queryWorkouts.ts │ ├── requestAuthorization.ts │ ├── saveCategorySample.ts │ ├── saveCorrelationSample.ts │ ├── saveQuantitySample.ts │ ├── saveStateOfMindSample.ts │ ├── saveWorkoutRoute.ts │ ├── saveWorkoutSample.ts │ ├── serializeDate.test.ts │ ├── serializeDate.ts │ ├── startWatchApp.ts │ ├── subscribeToChanges.ts │ └── workoutSessionMirroringStartHandler.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /.env.github: -------------------------------------------------------------------------------- 1 | BUN_VERSION=1.1.45 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | docs/scripts/prism.js 2 | lib -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-kingstinct/react-native", 3 | "overrides": [ 4 | { 5 | "files": ["package.json"], 6 | "rules": { 7 | "jsonc/array-bracket-newline": 0 8 | } 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [robertherber, kingstinct] 4 | -------------------------------------------------------------------------------- /.github/workflows/eas-build.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: EAS Build 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the main branch 7 | on: 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: master 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | eas-ios: 16 | # The type of runner that the job will run on 17 | uses: Kingstinct/utils/.github/workflows/eas-build.yml@main 18 | secrets: 19 | EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} 20 | with: 21 | platform: ios 22 | build_profile: development-sim 23 | expo_organisation_or_user_slug: react-native-healthkit 24 | working_directory: ./example-expo 25 | always_run: true 26 | no_wait: false 27 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Publish latest 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the main branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | 11 | concurrency: 12 | group: master 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | expo-publish-latest: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 20 20 | env: 21 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v4 27 | 28 | - uses: cardinalby/export-env-action@v2 29 | with: 30 | envFile: .env.github 31 | 32 | - uses: oven-sh/setup-bun@v2 33 | with: 34 | bun-version: ${{ env.BUN_VERSION }} 35 | 36 | - name: Install dependencies (example-expo) 37 | run: bun install --frozen-lockfile 38 | working-directory: example-expo 39 | 40 | - name: Install dependencies (example) 41 | run: bun install --frozen-lockfile 42 | working-directory: example 43 | 44 | - name: Expo GitHub Action 45 | uses: expo/expo-github-action@v8 46 | with: 47 | # Your Expo username, for authentication. 48 | token: ${{ secrets.EXPO_TOKEN }} 49 | expo-version: latest 50 | eas-version: latest 51 | # If Expo should be stored in the GitHub Actions cache (can be true or false) 52 | expo-cache: true # optional 53 | 54 | - name: Find Metro cache 55 | id: metro-cache-dir-path 56 | uses: actions/github-script@v7 57 | with: 58 | result-encoding: string 59 | script: | 60 | const os = require('os'); 61 | const path = require('path'); 62 | return path.join(os.tmpdir(), 'metro-cache'); 63 | 64 | - name: Restore Metro cache 65 | uses: actions/cache@v4 66 | with: 67 | path: ${{ steps.metro-cache-dir-path.outputs.result }} 68 | key: ${{ runner.os }}-metro-cache-${{ matrix.app }}-${{ github.ref }} 69 | restore-keys: | 70 | ${{ runner.os }}-metro-cache-${{ matrix.app }}-${{ github.base_ref }} 71 | ${{ runner.os }}-metro-cache-${{ matrix.app }}- 72 | 73 | # Runs a set of commands using the runners shell 74 | - name: Publish 75 | run: bunx eas-cli@latest update --branch=master --auto -p=ios --json --non-interactive 76 | working-directory: example-expo -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: PR - Publish Preview 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the main branch 7 | on: 8 | pull_request_target: 9 | workflow_dispatch: 10 | 11 | concurrency: 12 | group: preview-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 16 | jobs: 17 | expo-publish-preview: 18 | # The type of runner that the job will run on 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 20 21 | env: 22 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true 23 | 24 | # Steps represent a sequence of tasks that will be executed as part of the job 25 | steps: 26 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 27 | - uses: actions/checkout@v4 28 | with: 29 | ref: refs/pull/${{ github.event.number }}/merge 30 | 31 | - uses: cardinalby/export-env-action@v2 32 | with: 33 | envFile: .env.github 34 | 35 | - uses: oven-sh/setup-bun@v2 36 | with: 37 | bun-version: ${{ env.BUN_VERSION }} 38 | 39 | - name: Install dependencies (example-expo) 40 | run: bun install --frozen-lockfile 41 | working-directory: example-expo 42 | 43 | - name: Install dependencies (example) 44 | run: bun install --frozen-lockfile 45 | working-directory: example 46 | 47 | - name: Expo GitHub Action 48 | uses: expo/expo-github-action@v8 49 | with: 50 | # Your Expo username, for authentication. 51 | token: ${{ secrets.EXPO_TOKEN }} 52 | expo-version: latest 53 | eas-version: latest 54 | # If Expo should be stored in the GitHub Actions cache (can be true or false) 55 | expo-cache: true # optional 56 | 57 | - name: Find Metro cache 58 | id: metro-cache-dir-path 59 | uses: actions/github-script@v7 60 | with: 61 | result-encoding: string 62 | script: | 63 | const os = require('os'); 64 | const path = require('path'); 65 | return path.join(os.tmpdir(), 'metro-cache'); 66 | 67 | - name: Restore Metro cache 68 | uses: actions/cache@v4 69 | with: 70 | path: ${{ steps.metro-cache-dir-path.outputs.result }} 71 | key: ${{ runner.os }}-metro-cache-${{ matrix.app }}-${{ github.ref }} 72 | restore-keys: | 73 | ${{ runner.os }}-metro-cache-${{ matrix.app }}-${{ github.base_ref }} 74 | ${{ runner.os }}-metro-cache-${{ matrix.app }}- 75 | 76 | # Runs a set of commands using the runners shell 77 | - name: Publish 78 | run: bunx eas-cli@latest update --branch=master --auto -p=ios --json --non-interactive 79 | working-directory: example-expo 80 | 81 | - uses: marocchino/sticky-pull-request-comment@v2 82 | with: 83 | recreate: true 84 | message: | 85 | Expo preview is ready for review. 86 | 87 | There should soon be a [new native build available here](https://expo.dev/accounts/react-native-healthkit/projects/healthkit-example-expo) 88 | 89 | With that installed just scan this QR code: 90 | ![QR Preview](https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=exp%2Bhealthkit-example-expo%3A%2F%2Fu.expo.dev%2Fupdate%2F${{ env.BUILD_ID }}) 91 | 92 | eas-ios: 93 | # The type of runner that the job will run on 94 | uses: Kingstinct/utils/.github/workflows/eas-build.yml@main 95 | secrets: 96 | EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} 97 | with: 98 | platform: ios 99 | build_profile: development 100 | expo_organisation_or_user_slug: react-native-healthkit 101 | working_directory: ./example-expo 102 | always_run: true 103 | package_manager: bun 104 | no_wait: false 105 | checkout_ref: refs/pull/${{ github.event.number }}/merge 106 | 107 | package-preview: 108 | runs-on: ubuntu-latest 109 | steps: 110 | - uses: actions/checkout@v4 111 | - uses: cardinalby/export-env-action@v2 112 | with: 113 | envFile: .env.github 114 | 115 | - uses: oven-sh/setup-bun@v2 116 | with: 117 | bun-version: ${{ env.BUN_VERSION }} 118 | 119 | - name: Install example deps 120 | run: bun install --frozen-lockfile 121 | working-directory: example 122 | 123 | - name: Install example-expo deps 124 | run: bun install --frozen-lockfile 125 | working-directory: example-expo 126 | 127 | - run: bun install --frozen-lockfile 128 | 129 | - run: npx pkg-pr-new publish -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Test 4 | 5 | # Controls when the action will run. 6 | on: 7 | push: 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 12 | jobs: 13 | # This workflow contains a single job called "build" 14 | test: 15 | # The type of runner that the job will run on 16 | runs-on: macos-latest 17 | timeout-minutes: 10 18 | 19 | # Steps represent a sequence of tasks that will be executed as part of the job 20 | steps: 21 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | 25 | - uses: cardinalby/export-env-action@v2 26 | with: 27 | envFile: .env.github 28 | 29 | - uses: oven-sh/setup-bun@v2 30 | with: 31 | bun-version: ${{ env.BUN_VERSION }} 32 | 33 | - name: Install example deps 34 | run: bun install --frozen-lockfile 35 | working-directory: example 36 | 37 | - name: Install example-expo deps 38 | run: bun install --frozen-lockfile 39 | working-directory: example-expo 40 | 41 | - name: Install deps 42 | run: bun install --frozen-lockfile 43 | 44 | - name: Test 45 | run: bun test-only 46 | 47 | lint: 48 | # The type of runner that the job will run on 49 | runs-on: ubuntu-latest 50 | timeout-minutes: 10 51 | 52 | # Steps represent a sequence of tasks that will be executed as part of the job 53 | steps: 54 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 55 | - name: Checkout 56 | uses: actions/checkout@v4 57 | 58 | - uses: cardinalby/export-env-action@v2 59 | with: 60 | envFile: .env.github 61 | 62 | - uses: oven-sh/setup-bun@v2 63 | with: 64 | bun-version: ${{ env.BUN_VERSION }} 65 | 66 | - name: Install example deps 67 | run: cd example && bun install && cd .. 68 | 69 | - name: Install example-expo deps 70 | run: cd example-expo && bun install && cd .. 71 | 72 | - name: Install deps 73 | run: bun install --frozen-lockfile 74 | 75 | - name: Lint 76 | run: bun lint 77 | 78 | typecheck: 79 | # The type of runner that the job will run on 80 | runs-on: ubuntu-latest 81 | timeout-minutes: 10 82 | 83 | # Steps represent a sequence of tasks that will be executed as part of the job 84 | steps: 85 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 86 | - name: Checkout 87 | uses: actions/checkout@v4 88 | 89 | - uses: cardinalby/export-env-action@v2 90 | with: 91 | envFile: .env.github 92 | 93 | - uses: oven-sh/setup-bun@v2 94 | with: 95 | bun-version: ${{ env.BUN_VERSION }} 96 | 97 | - name: Install example deps 98 | run: cd example && bun install && cd .. 99 | 100 | - name: Install example-expo deps 101 | run: cd example-expo && bun install && cd .. 102 | 103 | - name: Install deps 104 | run: bun install --frozen-lockfile 105 | 106 | - name: Types 107 | run: bun typecheck 108 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Cocoapods 33 | # 34 | example/ios/Pods 35 | 36 | # node.js 37 | # 38 | node_modules/ 39 | 40 | # Expo 41 | .expo/* 42 | 43 | # generated by bob 44 | lib/ 45 | /.eslintcache 46 | 47 | *.log -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | bunx --no -- commitlint --edit "${1}" -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | bun lint-staged -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | bun run test 5 | -------------------------------------------------------------------------------- /.solidarity: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/solidaritySchema", 3 | "requirements": { 4 | "bun": [ 5 | { 6 | "rule": "cli", 7 | "binary": "bun", 8 | "version": "--version", 9 | "semver": ">=1.1" 10 | } 11 | ], 12 | "Cocoapods": [ 13 | { 14 | "rule": "cli", 15 | "binary": "pod", 16 | "semver": ">=1.10" 17 | } 18 | ], 19 | "swiftlint": [ 20 | { 21 | "rule": "cli", 22 | "binary": "swiftlint", 23 | "semver": ">=0.43" 24 | } 25 | ], 26 | "Xcode": [ 27 | { 28 | "rule": "cli", 29 | "binary": "xcodebuild", 30 | "semver": ">=15", 31 | "platform": "darwin" 32 | }, 33 | { 34 | "rule": "cli", 35 | "binary": "xcrun", 36 | "semver": ">=68", 37 | "platform": "darwin" 38 | } 39 | ] 40 | } 41 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. 4 | 5 | ## Development workflow 6 | 7 | To get started with the project, run `bun bootstrap` in the root directory to install the required dependencies for each package: 8 | 9 | ```sh 10 | bun bootstrap 11 | ``` 12 | 13 | While developing, you can run the [example app](/example/) to test your changes. 14 | 15 | To start the packager: 16 | 17 | ```sh 18 | bun example start 19 | ``` 20 | 21 | To run the example app on iOS: 22 | 23 | ```sh 24 | bun example ios 25 | ``` 26 | 27 | Make sure your code passes TypeScript and ESLint. Run the following to verify: 28 | 29 | ```sh 30 | bun typescript 31 | bun lint 32 | ``` 33 | 34 | To fix formatting errors, run the following: 35 | 36 | ```sh 37 | bun lint --fix 38 | ``` 39 | 40 | Remember to add tests for your change if possible. Run the unit tests by: 41 | 42 | ```sh 43 | bun run test 44 | ``` 45 | 46 | You can use `bunx solidarity` to make sure everything is set up correctly, for example [make sure you haft `swiftlint` installed](https://github.com/realm/SwiftLint#installation) since we use this as a pre-commit git hook. 47 | 48 | To edit the Objective-C files, open `example/ios/ReactNativeHealthkitExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > @kingstinct/react-native-healthkit`. 49 | 50 | To edit the Kotlin files, open `example/android` in Android studio and find the source files at `kingstinctreactnativehealthkit` under `Android`. 51 | 52 | ### Commit message convention 53 | 54 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 55 | 56 | - `fix`: bug fixes, e.g. fix crash due to deprecated method. 57 | - `feat`: new features, e.g. add new method to the module. 58 | - `refactor`: code refactor, e.g. migrate from class components to hooks. 59 | - `docs`: changes into documentation, e.g. add usage example for the module.. 60 | - `test`: adding or updating tests, eg add integration tests using detox. 61 | - `chore`: tooling changes, e.g. change CI config. 62 | 63 | Our pre-commit hooks verify that your commit message matches this format when committing. 64 | 65 | ### Linting and tests 66 | 67 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) 68 | 69 | We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. 70 | 71 | Our pre-commit hooks verify that the linter and tests pass when committing. 72 | 73 | ### Scripts 74 | 75 | The `package.json` file contains various scripts for common tasks: 76 | 77 | - `bun bootstrap`: setup project by installing all dependencies and pods. 78 | - `bun typescript`: type-check files with TypeScript. 79 | - `bun lint`: lint files with ESLint. 80 | - `bun test`: run unit tests with Jest. 81 | - `bun example start`: start the Metro server for the example app. 82 | - `bun example ios`: run the example app on iOS. 83 | 84 | ### Sending a pull request 85 | 86 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). 87 | 88 | When you're sending a pull request: 89 | 90 | - Prefer small pull requests focused on one change. 91 | - Verify that linters and tests are passing. 92 | - Review the documentation to make sure it looks good. 93 | - Follow the pull request template when opening a pull request. 94 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. 95 | 96 | ## Code of Conduct 97 | 98 | ### Our Pledge 99 | 100 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 101 | 102 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 103 | 104 | ### Our Standards 105 | 106 | Examples of behavior that contributes to a positive environment for our community include: 107 | 108 | - Demonstrating empathy and kindness toward other people 109 | - Being respectful of differing opinions, viewpoints, and experiences 110 | - Giving and gracefully accepting constructive feedback 111 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 112 | - Focusing on what is best not just for us as individuals, but for the overall community 113 | 114 | Examples of unacceptable behavior include: 115 | 116 | - The use of sexualized language or imagery, and sexual attention or 117 | advances of any kind 118 | - Trolling, insulting or derogatory comments, and personal or political attacks 119 | - Public or private harassment 120 | - Publishing others' private information, such as a physical or email 121 | address, without their explicit permission 122 | - Other conduct which could reasonably be considered inappropriate in a 123 | professional setting 124 | 125 | ### Enforcement Responsibilities 126 | 127 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 128 | 129 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 130 | 131 | ### Scope 132 | 133 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 134 | 135 | ### Enforcement 136 | 137 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly. 138 | 139 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 140 | 141 | ### Enforcement Guidelines 142 | 143 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 144 | 145 | #### 1. Correction 146 | 147 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 148 | 149 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 150 | 151 | #### 2. Warning 152 | 153 | **Community Impact**: A violation through a single incident or series of actions. 154 | 155 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 156 | 157 | #### 3. Temporary Ban 158 | 159 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 160 | 161 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 162 | 163 | #### 4. Permanent Ban 164 | 165 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 166 | 167 | **Consequence**: A permanent ban from any sort of public interaction within the community. 168 | 169 | ### Attribution 170 | 171 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 172 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 173 | 174 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 175 | 176 | [homepage]: https://www.contributor-covenant.org 177 | 178 | For answers to common questions about this code of conduct, see the FAQ at 179 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 180 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Robert Herber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @kingstinct/react-native-healthkit 2 | 3 | [![Test Status](https://github.com/Kingstinct/react-native-healthkit/actions/workflows/test.yml/badge.svg)](https://github.com/Kingstinct/react-native-healthkit/actions/workflows/test.yml) 4 | [![Latest version on NPM](https://img.shields.io/npm/v/@kingstinct/react-native-healthkit)](https://www.npmjs.com/package/@kingstinct/react-native-healthkit) 5 | [![Downloads on NPM](https://img.shields.io/npm/dt/@kingstinct/react-native-healthkit)](https://www.npmjs.com/package/@kingstinct/react-native-healthkit) 6 | [![Discord](https://dcbadge.vercel.app/api/server/hrgnETpsJA?style=flat)](https://discord.gg/hrgnETpsJA) 7 | 8 | 9 | React Native bindings for HealthKit with full TypeScript and Promise support covering about any kind of data. Keeping TypeScript mappings as close as possible to HealthKit - both in regards to naming and serialization. This will make it easier to keep this library up-to-date with HealthKit as well as browsing [the official documentation](https://developer.apple.com/documentation/healthkit) (and if something - metadata properties for example - is not typed it will still be accessible). 10 | 11 | | Data Types | Query | Save | Subscribe | Examples | 12 | | ----------------------------|:------|:------|:----------|:---------------------------------------| 13 | | 100+ Quantity Types | ✅ | ✅ | ✅ | Steps, energy burnt, blood glucose etc.. | 14 | | 63 Category Types | ✅ | ✅ | ✅ | Sleep analysis, mindful sessions etc.. | 15 | | 75+ Workout Activity Types | ✅ | ✅ | ✅ | Swimming, running, table tennis etc.. | 16 | | Correlation Types | ✅ | ✅ | ✅ | Food and blood pressure | 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)) | 19 | 20 | ### Disclaimer 21 | 22 | This library is provided as-is without any warranty and is not affiliated with Apple in any way. The data might be incomplete or inaccurate. 23 | 24 | ## Installation 25 | 26 | ### Expo 27 | 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 | 29 | 1. `yarn add @kingstinct/react-native-healthkit` 30 | 2. Update your app.json with the config plugin: 31 | ```json 32 | { 33 | "expo": { 34 | "plugins": ["@kingstinct/react-native-healthkit"] 35 | } 36 | } 37 | ``` 38 | this will give you defaults that make the app build without any further configuration. If you want, you can override the defaults: 39 | ```json 40 | { 41 | "expo": { 42 | "plugins": [ 43 | ["@kingstinct/react-native-healthkit", { 44 | "NSHealthShareUsageDescription": "Your own custom usage description", 45 | "NSHealthUpdateUsageDescription": false, // if you have no plans to update data, you could skip adding it to your info.plist 46 | "background": false // if you have no plans to use it in background mode, you could skip adding it to the entitlements 47 | }] 48 | ] 49 | } 50 | } 51 | ``` 52 | 3. Build a new Dev Client 53 | 54 | ### Native or Expo Bare Workflow 55 | 1. `yarn add @kingstinct/react-native-healthkit` 56 | 2. `npx pod-install` 57 | 3. Set `NSHealthUpdateUsageDescription` and `NSHealthShareUsageDescription` in your `Info.plist` 58 | 4. Enable the HealthKit capability for the project in Xcode. 59 | 5. Since this package is using Swift you might also need to add a bridging header in your project if you haven't already, you can [find more about that in the official React Native docs](https://reactnative.dev/docs/native-modules-ios#exporting-swift) 60 | 61 | ## Usage 62 | 63 | During runtime check and request permissions with `requestAuthorization`. Failing to request authorization, or requesting a permission you haven't requested yet, will result in the app crashing. This is easy to miss - for example by requesting authorization in the same component where you have a hook trying to fetch data right away.. :) 64 | 65 | Some hook examples: 66 | ```TypeScript 67 | import { HKQuantityTypeIdentifier, useHealthkitAuthorization } from '@kingstinct/react-native-healthkit'; 68 | 69 | const [authorizationStatus, requestAuthorization] = useHealthkitAuthorization([HKQuantityTypeIdentifier.bloodGlucose]) 70 | 71 | // make sure that you've requested authorization before requesting data, otherwise your app will crash 72 | import { useMostRecentQuantitySample, HKQuantityTypeIdentifier, useMostRecentCategorySample } from '@kingstinct/react-native-healthkit'; 73 | 74 | const mostRecentBloodGlucoseSample = useMostRecentQuantitySample(HKQuantityTypeIdentifier.bloodGlucose) 75 | const lastBodyFatSample = useMostRecentQuantitySample(HKQuantityTypeIdentifier.bodyFatPercentage) 76 | const lastMindfulSession = useMostRecentCategorySample(HKCategoryTypeIdentifier.mindfulSession) 77 | const lastWorkout = useMostRecentWorkout() 78 | ``` 79 | 80 | Some imperative examples: 81 | ```TypeScript 82 | import HealthKit, { HKUnit, HKQuantityTypeIdentifier, HKInsulinDeliveryReason, HKCategoryTypeIdentifier } from '@kingstinct/react-native-healthkit'; 83 | 84 | const isAvailable = await HealthKit.isHealthDataAvailable(); 85 | 86 | /* Read latest sample of any data */ 87 | await HealthKit.requestAuthorization([HKQuantityTypeIdentifier.bodyFatPercentage]); // request read permission for bodyFatPercentage 88 | 89 | const { quantity, unit, startDate, endDate } = await HealthKit.getMostRecentQuantitySample(HKQuantityTypeIdentifier.bodyFatPercentage); // read latest sample 90 | 91 | console.log(quantity) // 17.5 92 | console.log(unit) // % 93 | 94 | await HealthKit.requestAuthorization([HKQuantityTypeIdentifier.heartRate]); // request read permission for heart rate 95 | 96 | /* Subscribe to data (Make sure to request permissions before subscribing to changes) */ 97 | const [hasRequestedAuthorization, setHasRequestedAuthorization] = useState(false); 98 | 99 | useEffect(() => { 100 | HealthKit.requestAuthorization([HKQuantityTypeIdentifier.heartRate]).then(() => { 101 | setHasRequestedAuthorization(true); 102 | }); 103 | }, []); 104 | 105 | useEffect(() => { 106 | if (hasRequestedAuthorization) { 107 | const unsubscribe = HealthKit.subscribeToChanges(HKQuantityTypeIdentifier.heartRate, () => { 108 | // refetch data as needed 109 | }); 110 | } 111 | 112 | return () => unsubscribe(); 113 | }, [hasRequestedAuthorization]); 114 | 115 | /* write data */ 116 | await HealthKit.requestAuthorization([], [HKQuantityTypeIdentifier.insulinDelivery]); // request write permission for insulin delivery 117 | 118 | ReactNativeHealthkit.saveQuantitySample( 119 | HKQuantityTypeIdentifier.insulinDelivery, 120 | HKUnit.InternationalUnit, 121 | 5.5, 122 | { 123 | metadata: { 124 | // Metadata keys could be arbirtary string to store app-specific data. 125 | // To use built-in types from https://developer.apple.com/documentation/healthkit/samples/metadata_keys 126 | // you need to specify string values instead of variable names (by dropping MetadataKey from the name). 127 | HKInsulinDeliveryReason: HKInsulinDeliveryReason.basal, 128 | }, 129 | } 130 | ); 131 | ``` 132 | 133 | ### HealthKit Anchors (breaking change in 6.0) 134 | In 6.0 you can use HealthKit anchors to get changes and deleted items which is very useful for syncing. This is a breaking change - but a very easy one to handle that TypeScript should help you with. Most queries now return an object containing samples which is what was returned as only an array before. 135 | 136 | ```newAnchor``` is a base64-encoded string returned from HealthKit that contain sync information. After each successful sync, store the anchor for the next time your anchor query is called to only return the values that have changed. 137 | 138 | ```limit``` will indicate how many records to consider when sycning data, you can set this value to 0 indicate no limit. 139 | 140 | Example: 141 | 142 | ```TypeScript 143 | const { newAnchor, samples, deletedSamples } = await queryQuantitySamplesWithAnchor(HKQuantityTypeIdentifier.stepCount, { 144 | limit: 2, 145 | }) 146 | 147 | const nextResult = await queryQuantitySamplesWithAnchor(HKQuantityTypeIdentifier.stepCount, { 148 | limit: 2, 149 | anchor: newAnchor, 150 | }) 151 | 152 | // etc.. 153 | ``` 154 | 155 | ## A note on Apple Documentation 156 | 157 | We're striving to do as straight a mapping as possible to the Native Libraries. This means that in most cases the Apple Documentation makes sense. However, when it comes to the Healthkit [Metadata Keys](https://developer.apple.com/documentation/healthkit/samples/metadata_keys) the documentation doesn't actually reflect the serialized values. For example HKMetadataKeyExternalUUID in the documentation serializes to HKExternalUUID - which is what we use. 158 | 159 | ## Clinical Records 160 | 161 | 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. 162 | 163 | ## Android alternatives 164 | 165 | For a similar library for Android, check out [react-native-health-connect](https://github.com/matinzd/react-native-health-connect/) that works with the new Health Connect. For Google Fit [react-native-google-fit](https://www.npmjs.com/package/react-native-google-fit) seems to be the most popular option, and and another possible option is to work directly with the Google Fit REST API which I've some experience with. 166 | 167 | ## Contributing 168 | 169 | See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. 170 | 171 | ## Sponsorship and enterprise-grade support 172 | 173 | If you're using @kingstinct/react-native-healthkit to build your production app [please consider funding its continued development](https://github.com/sponsors/Kingstinct). It helps us spend more time on keeping this library as good as it can be. 174 | 175 | At Kingstinct we're also able to provide enterprise-grade support for this package, [find us here](https://kingstinct.com) or [drop an email](mailto:healthkit@kingstinct.com) for more information. Also feel free to join our [Discord community](https://discord.gg/EHScS93v). 176 | 177 | ## License 178 | 179 | MIT 180 | -------------------------------------------------------------------------------- /app.plugin.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, functional/immutable-data, no-param-reassign, @typescript-eslint/no-var-requires */ 2 | const { 3 | withPlugins, 4 | createRunOncePlugin, 5 | withEntitlementsPlist, 6 | withInfoPlist, 7 | } = require('@expo/config-plugins') 8 | 9 | /** 10 | * @typedef ConfigPlugin 11 | * @type {import('@expo/config-plugins').ConfigPlugin} 12 | * @template T = void 13 | */ 14 | 15 | // please note that the BackgroundConfig currently doesn't actually enable background delivery for any types, but you 16 | // can set it to false if you don't want the entitlement 17 | 18 | /** 19 | * @typedef BackgroundConfig 20 | * @type {false | Partial>} 24 | 25 | /** 26 | * @typedef InfoPlistConfig 27 | * @type {{ 28 | * NSHealthShareUsageDescription?: string | false, 29 | * NSHealthUpdateUsageDescription?: string | false 30 | * }} 31 | */ 32 | 33 | /** 34 | * @typedef AppPluginConfig 35 | * @type {InfoPlistConfig & { background?: BackgroundConfig }} 36 | */ 37 | 38 | /** 39 | * @type {ConfigPlugin<{background: BackgroundConfig}>} 40 | */ 41 | const withEntitlementsPlugin = ( 42 | config, 43 | /** 44 | * @type {{background: BackgroundConfig} | undefined} 45 | * */ 46 | props, 47 | ) => withEntitlementsPlist(config, (config) => { 48 | config.modResults['com.apple.developer.healthkit'] = true 49 | 50 | // background is enabled by default, but possible to opt-out from 51 | // (haven't seen any drawbacks from having it enabled) 52 | if (props?.background !== false) { 53 | config.modResults['com.apple.developer.healthkit.background-delivery'] = true 54 | } 55 | 56 | return config 57 | }) 58 | 59 | /** 60 | * @type {ConfigPlugin} 61 | */ 62 | const withInfoPlistPlugin = (config, 63 | /** 64 | * @type {{NSHealthShareUsageDescription: string | boolean, NSHealthUpdateUsageDescription: string | boolean} | undefined} 65 | * */ 66 | props) => withInfoPlist(config, (config) => { 67 | if (props?.NSHealthShareUsageDescription !== false) { 68 | config.modResults.NSHealthShareUsageDescription = props.NSHealthShareUsageDescription ?? `${config.name} wants to read your health data` 69 | } 70 | 71 | if (props?.NSHealthUpdateUsageDescription !== false) { 72 | config.modResults.NSHealthUpdateUsageDescription = props.NSHealthUpdateUsageDescription ?? `${config.name} wants to update your health data` 73 | } 74 | 75 | return config 76 | }) 77 | 78 | const pkg = require('./package.json') 79 | 80 | /** 81 | * @type {ConfigPlugin} 82 | */ 83 | const healthkitAppPlugin = (config, props) => withPlugins(config, [ 84 | [withEntitlementsPlugin, props], 85 | [withInfoPlistPlugin, props], 86 | ]) 87 | 88 | /** 89 | * @type {ConfigPlugin} 90 | */ 91 | module.exports = createRunOncePlugin(healthkitAppPlugin, pkg.name, pkg.version) 92 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | } 4 | -------------------------------------------------------------------------------- /docs/images/diet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/diet.jpg -------------------------------------------------------------------------------- /docs/images/diet.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/diet.webp -------------------------------------------------------------------------------- /docs/images/github-mark-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/github-mark-white.png -------------------------------------------------------------------------------- /docs/images/github-mark-white.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/github-mark-white.webp -------------------------------------------------------------------------------- /docs/images/healthkit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/healthkit.png -------------------------------------------------------------------------------- /docs/images/healthkit.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/healthkit.webp -------------------------------------------------------------------------------- /docs/images/heart-data.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/heart-data.jpg -------------------------------------------------------------------------------- /docs/images/heart-data.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/heart-data.webp -------------------------------------------------------------------------------- /docs/images/hollander.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/hollander.jpg -------------------------------------------------------------------------------- /docs/images/hollander.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/hollander.webp -------------------------------------------------------------------------------- /docs/images/lift.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/lift.jpg -------------------------------------------------------------------------------- /docs/images/lift.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/lift.webp -------------------------------------------------------------------------------- /docs/images/running-sunset.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/running-sunset.jpg -------------------------------------------------------------------------------- /docs/images/running-sunset.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/running-sunset.webp -------------------------------------------------------------------------------- /docs/images/situps.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/situps.jpg -------------------------------------------------------------------------------- /docs/images/situps.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/situps.webp -------------------------------------------------------------------------------- /docs/images/tie-shoes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/tie-shoes.jpg -------------------------------------------------------------------------------- /docs/images/tie-shoes.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/docs/images/tie-shoes.webp -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @kingstinct/react-native-healthkit 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 |
51 |

Access HealthKit seamlessly and effortlessly with Expo and React Native

54 |
55 |
    61 |
  • ✅ All 100+ Quantity Types
  • 62 |
  • ✅ All 63+ Category Types
  • 63 |
  • ✅ All 75+ Workout Types
  • 64 |
  • ✅ Save, query and subscribe to data
  • 65 |
  • ✅ TypeScript, Promise-based, Expo Plugin
  • 66 |
  • ✅ Convenient hooks
  • 67 |
  • Future-proof - mapping closely to the Native API
  • 68 |
69 |
70 |
71 |
72 |
73 |
74 |

75 | 1. Install with Expo ⚙️ 76 |

77 |
    78 |
  1. Install dependency

    79 |
    yarn install @kingstinct/react-native-healthkit
    80 |
    81 | Copy 82 |
    83 |
  2. 84 |
  3. Add Config Plugin to your app.json/app.config.js

    85 | 86 |
    {
     87 |   "expo": {
     88 |     "plugins": [
     89 |       "@kingstinct/react-native-healthkit"
     90 |     ]
     91 |   }
     92 | }
    93 | 94 |
    95 | Copy 96 |
    97 |
  4. 98 |
  5. Build a new Dev Client
  6. 99 |
100 | 101 | 120 | 121 | 122 | 123 |
124 | 125 |
126 |
127 |
128 |
// --- 2. Authorize 🔒 ---
129 | 
130 | import { 
131 |   Text, 
132 |   Button,
133 |   View
134 | } from 'react-native';
135 | import { 
136 |   HKQuantityTypeIdentifier, 
137 |   useHealthkitAuthorization 
138 | } from '@kingstinct/react-native-healthkit';
139 | 
140 | export default () => {
141 |   const [
142 |     authorizationStatus, 
143 |     requestAuthorization
144 |   ] = useHealthkitAuthorization([
145 |     HKQuantityTypeIdentifier.bloodGlucose
146 |   ])
147 | 
148 |   return <View>
149 |     <Text>Authorization Status: {authorizationStatus}</Text>
150 |     <Button 
151 |       onPress={requestAuthorization} 
152 |       title="Request Authorization" 
153 |     />
154 |   </View>
155 | }
156 |
157 |
158 |
159 |
160 | 161 |
// --- 3. Launch 🚀 ---
162 | 
163 | import { 
164 |   Text, 
165 |   View
166 | } from 'react-native';
167 | import { 
168 |   useMostRecentQuantitySample, 
169 |   HKQuantityTypeIdentifier,
170 | } from '@kingstinct/react-native-healthkit';
171 | 
172 | export default () => {
173 |   const sample = useMostRecentQuantitySample(
174 |     HKQuantityTypeIdentifier.bloodGlucose
175 |   )
176 | 
177 |   return <View>
178 |     <Text>Blood Glucose: {sample?.quantity}</Text>
179 |   </View>
180 | }
181 |
182 |
183 | 184 | 201 | 247 | 248 | 249 | 250 | 251 | 252 | 255 | 256 | 257 | -------------------------------------------------------------------------------- /docs/styles/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.29.0 2 | https://prismjs.com/download.html#themes=prism-okaidia&languages=markup+clike+javascript+jsx+tsx+typescript&plugins=line-highlight+toolbar+copy-to-clipboard */ 3 | code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help} 4 | pre[data-line]{position:relative;padding:1em 0 1em 3em}.line-highlight{position:absolute;left:0;right:0;padding:inherit 0;margin-top:1em;background:hsla(24,20%,50%,.08);background:linear-gradient(to right,hsla(24,20%,50%,.1) 70%,hsla(24,20%,50%,0));pointer-events:none;line-height:inherit;white-space:pre}@media print{.line-highlight{-webkit-print-color-adjust:exact;color-adjust:exact}}.line-highlight:before,.line-highlight[data-end]:after{content:attr(data-start);position:absolute;top:.4em;left:.6em;min-width:1em;padding:0 .5em;background-color:hsla(24,20%,50%,.4);color:#f4f1ef;font:bold 65%/1.5 sans-serif;text-align:center;vertical-align:.3em;border-radius:999px;text-shadow:none;box-shadow:0 1px #fff}.line-highlight[data-end]:after{content:attr(data-end);top:auto;bottom:.4em}.line-numbers .line-highlight:after,.line-numbers .line-highlight:before{content:none}pre[id].linkable-line-numbers span.line-numbers-rows{pointer-events:all}pre[id].linkable-line-numbers span.line-numbers-rows>span:before{cursor:pointer}pre[id].linkable-line-numbers span.line-numbers-rows>span:hover:before{background-color:rgba(128,128,128,.2)} 5 | div.code-toolbar{position:relative}div.code-toolbar>.toolbar{position:absolute;z-index:10;top:.3em;right:.2em;transition:opacity .3s ease-in-out;opacity:0}div.code-toolbar:hover>.toolbar{opacity:1}div.code-toolbar:focus-within>.toolbar{opacity:1}div.code-toolbar>.toolbar>.toolbar-item{display:inline-block}div.code-toolbar>.toolbar>.toolbar-item>a{cursor:pointer}div.code-toolbar>.toolbar>.toolbar-item>button{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}div.code-toolbar>.toolbar>.toolbar-item>a,div.code-toolbar>.toolbar>.toolbar-item>button,div.code-toolbar>.toolbar>.toolbar-item>span{color:#bbb;font-size:.8em;padding:0 .5em;background:#f5f2f0;background:rgba(224,224,224,.2);box-shadow:0 2px 0 0 rgba(0,0,0,.2);border-radius:.5em}div.code-toolbar>.toolbar>.toolbar-item>a:focus,div.code-toolbar>.toolbar>.toolbar-item>a:hover,div.code-toolbar>.toolbar>.toolbar-item>button:focus,div.code-toolbar>.toolbar>.toolbar-item>button:hover,div.code-toolbar>.toolbar>.toolbar-item>span:focus,div.code-toolbar>.toolbar>.toolbar-item>span:hover{color:inherit;text-decoration:none} 6 | -------------------------------------------------------------------------------- /docs/styles/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | background: #222; 7 | padding: 0; 8 | margin: 0; 9 | font-family: sans-serif; 10 | font-weight: 350; 11 | font-size: 13px; 12 | /* text-transform: uppercase; */ 13 | color: #000; 14 | } 15 | 16 | .instructions { 17 | color: black; 18 | text-align: left; 19 | } 20 | 21 | .instructions li { 22 | margin-bottom: 30px; 23 | } 24 | 25 | .code-window-prism { 26 | max-width: 768px; 27 | backdrop-filter: blur(40px); 28 | -webkit-backdrop-filter: blur(40px); 29 | opacity: 0.9; 30 | user-select: all; 31 | -webkit-user-select: all; 32 | } 33 | 34 | @media(min-width: 768px) { 35 | .code-window-prism { 36 | border-radius: 10px; 37 | } 38 | } 39 | 40 | .code-window { 41 | background-color: #0009; 42 | border-radius: 5px; 43 | border: 1px solid white; 44 | backdrop-filter: blur(4px); 45 | 46 | text-align: left; 47 | padding: 10px; 48 | padding-left: 20px; 49 | padding-right: 20px; 50 | margin-bottom: 10px; 51 | } 52 | 53 | .toggle-package-manager, 54 | .toggle-expo-rn { 55 | border: 1px solid #fffa; 56 | border-radius: 5px; 57 | cursor: pointer; 58 | user-select: auto; 59 | -webkit-user-select: auto; 60 | padding-left: 5px; 61 | } 62 | 63 | .toggle-expo-rn { 64 | border: 1px solid #0008; 65 | border-radius: 5px; 66 | cursor: pointer; 67 | user-select: auto; 68 | -webkit-user-select: auto; 69 | padding-left: 5px; 70 | } 71 | 72 | .toggle-package-manager { 73 | width: 100px 74 | } 75 | 76 | .code-window { 77 | position: relative; 78 | } 79 | 80 | .code-window pre { 81 | color: white; 82 | user-select: all; 83 | -webkit-user-select: all; 84 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 85 | } 86 | 87 | .copy-code { 88 | color: white; 89 | border: 1px solid #fffa; 90 | border-right: 0px; 91 | border-bottom: 0px; 92 | border-top-left-radius: 5px; 93 | cursor: pointer; 94 | padding-left: 5px; 95 | display: inline; 96 | position: absolute; 97 | bottom: 0px; 98 | right: 0px; 99 | padding-top: 1px; 100 | padding-left: 5px; 101 | user-select: none; 102 | -webkit-user-select: none; 103 | display: none; 104 | } 105 | 106 | .code-window:hover .copy-code { 107 | display: block 108 | } 109 | 110 | .contrast-safe-text { 111 | backdrop-filter: blur(4px); 112 | background-color: #fffa; 113 | border-radius: 10px; 114 | padding-left: 5px; 115 | padding-right: 5px; 116 | padding: 6px; 117 | } 118 | 119 | .tabs { 120 | display: flex; 121 | flex: 1; 122 | padding-top: 15px; 123 | margin-top: 10px; 124 | align-self: center; 125 | color: 'black'; 126 | text-decoration: none; 127 | display: flex; 128 | flex-direction: row; 129 | justify-content: space-between; 130 | border-top: 1px solid #0001; 131 | padding-left: 30px; 132 | padding-right: 10px; 133 | } 134 | 135 | .tabs a:visited { 136 | color: black; 137 | text-decoration: none; 138 | } 139 | 140 | .tabs a:hover { 141 | color: black; 142 | text-decoration: underline; 143 | } 144 | 145 | .tabs a:link { 146 | color: black; 147 | text-decoration: underline; 148 | } 149 | 150 | header { 151 | background-color: #fff; 152 | padding: 0; 153 | margin: 0; 154 | } 155 | 156 | .section { 157 | text-align: center; 158 | position: absolute; 159 | width: 100%; 160 | height: 100vh; 161 | letter-spacing: 1.5px; 162 | overflow: hidden; 163 | clip: rect(0, auto, auto, 0); 164 | } 165 | 166 | .section .fixed { 167 | overflow: hidden; 168 | position: fixed; 169 | top: 50%; 170 | left: 50%; 171 | max-width: 720px; 172 | width: 100%; 173 | margin-top: 50px; 174 | } 175 | 176 | .section .white { 177 | color: #fff; 178 | } 179 | 180 | h1 { 181 | user-select: none; 182 | -webkit-user-select: none; 183 | } 184 | 185 | .section:nth-child(1) { 186 | background-color: #224430; 187 | /* background-image: url('../images/heart-data.webp'); */ 188 | background-image: url('../images/diet.webp'); 189 | background-position: center; 190 | background-size: cover; 191 | color: #000; 192 | top: 0; 193 | z-index: 1; 194 | } 195 | 196 | .section:nth-child(2) { 197 | box-shadow: inset 0 1px 80px rgba(0, 0, 0, 0.14); 198 | background-color: #ccd; 199 | background-image: url('../images/hollander.webp'); 200 | background-position: center; 201 | background-size: cover; 202 | color: #fff; 203 | top: 100vh; 204 | z-index: 2; 205 | } 206 | 207 | 208 | .section:nth-child(3) { 209 | box-shadow: inset 0 1px 80px rgba(0, 0, 0, 0.14); 210 | background-color: #f44; 211 | background-image: url('../images/heart-data.webp'); 212 | background-position: center; 213 | background-size: cover; 214 | color: #fff; 215 | top: 200vh; 216 | z-index: 3; 217 | } 218 | 219 | 220 | .section:nth-child(4) { 221 | box-shadow: inset 0 1px 80px rgba(0, 0, 0, 0.14); 222 | background-color: #cce; 223 | background-image: url('../images/tie-shoes.webp'); 224 | background-position: center; 225 | background-size: cover; 226 | color: #fff; 227 | top: 300vh; 228 | z-index: 4; 229 | } 230 | 231 | 232 | .section:nth-child(5) { 233 | box-shadow: inset 0 1px 80px rgba(0, 0, 0, 0.14); 234 | background-color: #850ac2; 235 | background-image: url('../images/running-sunset.webp'); 236 | background-position: center; 237 | background-size: cover; 238 | color: #fff; 239 | top: 400vh; 240 | z-index: 5; 241 | } 242 | 243 | 244 | .section:nth-child(6) { 245 | box-shadow: inset 0 1px 80px rgba(0, 0, 0, 0.14); 246 | background-color: #0ac20a; 247 | color: #fff; 248 | top: 500vh; 249 | z-index: 6; 250 | } 251 | 252 | 253 | .section:nth-child(7) { 254 | box-shadow: inset 0 1px 80px rgba(0, 0, 0, 0.14); 255 | background-color: #c20a85; 256 | color: #fff; 257 | top: 600vh; 258 | z-index: 7; 259 | } 260 | 261 | 262 | .section:nth-child(8) { 263 | box-shadow: inset 0 1px 80px rgba(0, 0, 0, 0.14); 264 | background-color: #0ac285; 265 | color: #fff; 266 | top: 700vh; 267 | z-index: 8; 268 | } 269 | 270 | 271 | .section:nth-child(9) { 272 | box-shadow: inset 0 1px 80px rgba(0, 0, 0, 0.14); 273 | background-color: #c20a0a; 274 | color: #fff; 275 | top: 800vh; 276 | z-index: 9; 277 | } 278 | 279 | .section .fixed { 280 | transform: translate(-50%, -50%); 281 | } 282 | 283 | .section:nth-child(10) { 284 | background-color: #000; 285 | color: #fff; 286 | top: 900vh; 287 | z-index: 10; 288 | } 289 | 290 | .section:nt .fixed { 291 | transform: translate(-50%, -50%); 292 | } 293 | 294 | .oop { 295 | position: relative; 296 | z-index: auto; 297 | height: 10px; 298 | background: linear-gradient(141deg, #48ded4 0%, #a026bf 51%, #e82c75 75%); 299 | } -------------------------------------------------------------------------------- /example-expo/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /example-expo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # @generated expo-cli sync-e7dcf75f4e856f7b6f3239b3f3a7dd614ee755a8 17 | # The following patterns were generated by expo-cli 18 | 19 | # OSX 20 | # 21 | .DS_Store 22 | 23 | # Xcode 24 | # 25 | build/ 26 | *.pbxuser 27 | !default.pbxuser 28 | *.mode1v3 29 | !default.mode1v3 30 | *.mode2v3 31 | !default.mode2v3 32 | *.perspectivev3 33 | !default.perspectivev3 34 | xcuserdata 35 | *.xccheckout 36 | *.moved-aside 37 | DerivedData 38 | *.hmap 39 | *.ipa 40 | *.xcuserstate 41 | project.xcworkspace 42 | 43 | # node.js 44 | # 45 | node_modules/ 46 | 47 | # BUCK 48 | buck-out/ 49 | \.buckd/ 50 | *.keystore 51 | !debug.keystore 52 | 53 | # Bundle artifacts 54 | *.jsbundle 55 | 56 | # CocoaPods 57 | /ios/Pods/ 58 | 59 | # Expo 60 | .expo/ 61 | web-build/ 62 | dist/ 63 | ios/ 64 | 65 | app.plugin.js 66 | 67 | # @end expo-cli -------------------------------------------------------------------------------- /example-expo/App.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-relative-packages 2 | import App from '../example/src/App' 3 | 4 | export default App 5 | -------------------------------------------------------------------------------- /example-expo/app.config.js: -------------------------------------------------------------------------------- 1 | /** @typedef { import('@expo/config-types').ExpoConfig } ExpoConfig */ 2 | 3 | /** 4 | * @type {{ APP_ENV: 'staging' | 'development'}} 5 | */ 6 | const { CUSTOM_NAME } = process.env 7 | 8 | /** 9 | * @param { { config: ExpoConfig } } 10 | * @returns {ExpoConfig} 11 | */ 12 | export default ({ config }) => (CUSTOM_NAME ? { ...config, name: CUSTOM_NAME } : config) 13 | -------------------------------------------------------------------------------- /example-expo/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Healthkit Expo", 4 | "slug": "healthkit-example-expo", 5 | "owner": "react-native-healthkit", 6 | "version": "1.0.0", 7 | "scheme": "healthkit-example", 8 | "orientation": "portrait", 9 | "icon": "./assets/icon.png", 10 | "userInterfaceStyle": "light", 11 | "splash": { 12 | "image": "./assets/splash.png", 13 | "resizeMode": "contain", 14 | "backgroundColor": "#ffffff" 15 | }, 16 | "updates": { 17 | "fallbackToCacheTimeout": 0, 18 | "url": "https://u.expo.dev/f91da835-1fb0-4398-ba8f-f362cf3d3cc2" 19 | }, 20 | "assetBundlePatterns": [ 21 | "**/*" 22 | ], 23 | "ios": { 24 | "supportsTablet": true, 25 | "bundleIdentifier": "com.kingstinct.healthkitexample.expo", 26 | "buildNumber": "3" 27 | }, 28 | "web": { 29 | "favicon": "./assets/favicon.png" 30 | }, 31 | "runtimeVersion": { 32 | "policy": "sdkVersion" 33 | }, 34 | "plugins": [ 35 | [ 36 | "expo-build-properties", 37 | { 38 | "ios": { 39 | "newArchEnabled": true 40 | }, 41 | "android": { 42 | "newArchEnabled": true 43 | } 44 | } 45 | ], 46 | [ "./app.plugin", 47 | { 48 | "NSHealthShareUsageDescription": "Overriden default message: This app uses HealthKit to store and sync your data" 49 | } ] 50 | ], 51 | "extra": { 52 | "eas": { 53 | "projectId": "f91da835-1fb0-4398-ba8f-f362cf3d3cc2" 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /example-expo/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/example-expo/assets/adaptive-icon.png -------------------------------------------------------------------------------- /example-expo/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/example-expo/assets/favicon.png -------------------------------------------------------------------------------- /example-expo/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/example-expo/assets/icon.png -------------------------------------------------------------------------------- /example-expo/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingstinct/react-native-healthkit/1cd2f116d25bc1041b1a7ba24f3d9f903b9fdd75/example-expo/assets/splash.png -------------------------------------------------------------------------------- /example-expo/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['babel-preset-expo'], 3 | } 4 | -------------------------------------------------------------------------------- /example-expo/eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 0.51.0", 4 | "promptToConfigurePushNotifications": false 5 | }, 6 | "build": { 7 | "development": { 8 | "developmentClient": true, 9 | "distribution": "internal" 10 | }, 11 | "development-sim": { 12 | "developmentClient": true, 13 | "distribution": "internal", 14 | "ios": { 15 | "simulator": true 16 | } 17 | }, 18 | "preview": { 19 | "distribution": "internal" 20 | }, 21 | "production": { 22 | "ios": { 23 | "autoIncrement": "buildNumber" 24 | } 25 | } 26 | }, 27 | "submit": { 28 | "production": {} 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example-expo/metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require('@expo/metro-config') 2 | const escape = require('escape-string-regexp') 3 | const exclusionList = require('metro-config/src/defaults/exclusionList') 4 | const path = require('path') 5 | 6 | const examplePak = require('../example/package.json') 7 | const pak = require('../package.json') 8 | 9 | const config = getDefaultConfig(__dirname) 10 | 11 | const root = path.resolve(__dirname, '..') 12 | const exampleRoot = path.resolve(__dirname, '../example') 13 | 14 | const modules = Object.keys({ 15 | ...pak.peerDependencies, 16 | }) 17 | 18 | const exampleModules = Object.keys({ 19 | ...examplePak.dependencies, 20 | }) 21 | 22 | module.exports = { 23 | ...config, 24 | projectRoot: __dirname, 25 | watchFolders: [root, exampleRoot], 26 | 27 | // We need to make sure that only one version is loaded for peerDependencies 28 | // So we block them at the root, and alias them to the versions in example's node_modules 29 | resolver: { 30 | ...config.resolver, 31 | blacklistRE: exclusionList( 32 | [ 33 | ...modules.map( 34 | (m) => new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`), 35 | ), ...exampleModules.map( 36 | (m) => new RegExp(`^${escape(path.join(exampleRoot, 'node_modules', m))}\\/.*$`), 37 | ), 38 | ], 39 | ), 40 | 41 | extraNodeModules: [...modules, ...exampleModules].reduce((acc, name) => { 42 | acc[name] = path.join(__dirname, 'node_modules', name) 43 | return acc 44 | }, {}), 45 | }, 46 | 47 | transformer: { 48 | ...config.transformer, 49 | getTransformOptions: () => ({ 50 | transform: { 51 | experimentalImportSupport: false, 52 | inlineRequires: true, 53 | }, 54 | }), 55 | }, 56 | } 57 | -------------------------------------------------------------------------------- /example-expo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "healthkit-example-expo", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start --dev-client", 7 | "ios": "expo run:ios", 8 | "eject": "expo eject", 9 | "build-local": "bunx eas-cli@latest build --platform=ios --profile=development --local", 10 | "build-local-sim": "bunx eas-cli@latest build --platform=ios --profile=development --local", 11 | "build": "bunx eas-cli@latest build --platform=ios --profile=development", 12 | "typecheck": "tsc --noEmit", 13 | "postinstall": "cp ../app.plugin.js ./app.plugin.js", 14 | "android": "expo run:android", 15 | "upgrade-interactive": "bunx npm-check-updates --format group -i" 16 | }, 17 | "dependencies": { 18 | "@expo/match-media": "^0.3.0", 19 | "@kingstinct/react-native-healthkit": "8.4.0", 20 | "@react-navigation/native": "^6.0.12", 21 | "babel-plugin-module-resolver": "^5.0.2", 22 | "dayjs": "^1.11.11", 23 | "expo": "51", 24 | "expo-build-properties": "~0.12.3", 25 | "expo-dev-client": "~4.0.17", 26 | "expo-linear-gradient": "~13.0.2", 27 | "expo-modules-core": "~1.12.14", 28 | "expo-screen-orientation": "~7.0.5", 29 | "expo-status-bar": "~1.12.1", 30 | "expo-updates": "~0.25.16", 31 | "react": "18.2.0", 32 | "react-dom": "18.2.0", 33 | "react-native": "0.74.2", 34 | "react-native-paper": "^4.12.1", 35 | "react-native-safe-area-context": "4.10.4", 36 | "react-native-screens": "3.31.1", 37 | "react-native-web": "~0.19.10" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.20.0", 41 | "@expo/metro-config": "~0.18.1", 42 | "@types/react": "~18.2.79", 43 | "typescript": "~5.3.3" 44 | }, 45 | "private": true 46 | } 47 | -------------------------------------------------------------------------------- /example-expo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | }, 6 | "baseUrl": ".", 7 | "paths": { 8 | "@kingstinct/react-native-healthkit": [ "../src/index" ], 9 | "@kingstinct/react-native-healthkit/*": [ "../src/*" ], 10 | "example": [ "../example" ], 11 | "example/*": [ "../example/*" ], 12 | "example/src/*": [ "../example/src/*" ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /example/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.4 2 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby '2.6.10' 5 | 6 | gem 'cocoapods', '>= 1.11.3' 7 | -------------------------------------------------------------------------------- /example/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.5) 5 | rexml 6 | activesupport (6.1.5.1) 7 | concurrent-ruby (~> 1.0, >= 1.0.2) 8 | i18n (>= 1.6, < 2) 9 | minitest (>= 5.1) 10 | tzinfo (~> 2.0) 11 | zeitwerk (~> 2.3) 12 | addressable (2.8.0) 13 | public_suffix (>= 2.0.2, < 5.0) 14 | algoliasearch (1.27.5) 15 | httpclient (~> 2.8, >= 2.8.3) 16 | json (>= 1.5.1) 17 | atomos (0.1.3) 18 | claide (1.1.0) 19 | cocoapods (1.11.3) 20 | addressable (~> 2.8) 21 | claide (>= 1.0.2, < 2.0) 22 | cocoapods-core (= 1.11.3) 23 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 24 | cocoapods-downloader (>= 1.4.0, < 2.0) 25 | cocoapods-plugins (>= 1.0.0, < 2.0) 26 | cocoapods-search (>= 1.0.0, < 2.0) 27 | cocoapods-trunk (>= 1.4.0, < 2.0) 28 | cocoapods-try (>= 1.1.0, < 2.0) 29 | colored2 (~> 3.1) 30 | escape (~> 0.0.4) 31 | fourflusher (>= 2.3.0, < 3.0) 32 | gh_inspector (~> 1.0) 33 | molinillo (~> 0.8.0) 34 | nap (~> 1.0) 35 | ruby-macho (>= 1.0, < 3.0) 36 | xcodeproj (>= 1.21.0, < 2.0) 37 | cocoapods-core (1.11.3) 38 | activesupport (>= 5.0, < 7) 39 | addressable (~> 2.8) 40 | algoliasearch (~> 1.0) 41 | concurrent-ruby (~> 1.1) 42 | fuzzy_match (~> 2.0.4) 43 | nap (~> 1.0) 44 | netrc (~> 0.11) 45 | public_suffix (~> 4.0) 46 | typhoeus (~> 1.0) 47 | cocoapods-deintegrate (1.0.5) 48 | cocoapods-downloader (1.6.3) 49 | cocoapods-plugins (1.0.0) 50 | nap 51 | cocoapods-search (1.0.1) 52 | cocoapods-trunk (1.6.0) 53 | nap (>= 0.8, < 2.0) 54 | netrc (~> 0.11) 55 | cocoapods-try (1.2.0) 56 | colored2 (3.1.2) 57 | concurrent-ruby (1.1.10) 58 | escape (0.0.4) 59 | ethon (0.15.0) 60 | ffi (>= 1.15.0) 61 | ffi (1.15.5) 62 | fourflusher (2.3.1) 63 | fuzzy_match (2.0.4) 64 | gh_inspector (1.1.3) 65 | httpclient (2.8.3) 66 | i18n (1.10.0) 67 | concurrent-ruby (~> 1.0) 68 | json (2.6.1) 69 | minitest (5.15.0) 70 | molinillo (0.8.0) 71 | nanaimo (0.3.0) 72 | nap (1.1.0) 73 | netrc (0.11.0) 74 | public_suffix (4.0.7) 75 | rexml (3.2.5) 76 | ruby-macho (2.5.1) 77 | typhoeus (1.4.0) 78 | ethon (>= 0.9.0) 79 | tzinfo (2.0.4) 80 | concurrent-ruby (~> 1.0) 81 | xcodeproj (1.21.0) 82 | CFPropertyList (>= 2.3.3, < 4.0) 83 | atomos (~> 0.1.3) 84 | claide (>= 1.0.2, < 2.0) 85 | colored2 (~> 3.1) 86 | nanaimo (~> 0.3.0) 87 | rexml (~> 3.2.4) 88 | zeitwerk (2.5.4) 89 | 90 | PLATFORMS 91 | ruby 92 | 93 | DEPENDENCIES 94 | cocoapods (~> 1.11, >= 1.11.2) 95 | 96 | RUBY VERSION 97 | ruby 2.7.4p191 98 | 99 | BUNDLED WITH 100 | 2.2.27 101 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const pak = require('../package.json') 4 | 5 | module.exports = { 6 | presets: ['module:metro-react-native-babel-preset'], 7 | plugins: [ 8 | [ 9 | 'module-resolver', 10 | { 11 | extensions: [ 12 | '.tsx', '.ts', '.js', '.json', 13 | ], 14 | alias: { 15 | [pak.name]: path.join(__dirname, '..', pak.source), 16 | [`${pak.name}/*`]: path.join(__dirname, '..', pak.source, '*'), 17 | '@kingstinct/react-native-healthkit': '../src', 18 | }, 19 | }, 20 | ], 21 | ], 22 | } 23 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from 'react-native' 2 | 3 | import App from './src/App' 4 | 5 | AppRegistry.registerComponent('HealthkitExample', () => App) 6 | -------------------------------------------------------------------------------- /example/ios/File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // HealthkitExample 4 | // 5 | 6 | import Foundation 7 | -------------------------------------------------------------------------------- /example/ios/HealthkitExample-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | -------------------------------------------------------------------------------- /example/ios/HealthkitExample.xcodeproj/xcshareddata/xcschemes/HealthkitExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 35 | 37 | 43 | 44 | 45 | 46 | 52 | 54 | 60 | 61 | 62 | 63 | 65 | 66 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /example/ios/HealthkitExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/HealthkitExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/HealthkitExample/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : RCTAppDelegate 5 | 6 | @property (nonatomic, strong) UIWindow *window; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /example/ios/HealthkitExample/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | 5 | @implementation AppDelegate 6 | 7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 8 | { 9 | self.moduleName = @"HealthkitExample"; 10 | // You can add your custom initial props in the dictionary below. 11 | // They will be passed down to the ViewController used by React Native. 12 | self.initialProps = @{}; 13 | 14 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 15 | } 16 | 17 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 18 | { 19 | #if DEBUG 20 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; 21 | #else 22 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 23 | #endif 24 | } 25 | 26 | /// This method controls whether the `concurrentRoot`feature of React18 is turned on or off. 27 | /// 28 | /// @see: https://reactjs.org/blog/2022/03/29/react-v18.html 29 | /// @note: This requires to be rendering on Fabric (i.e. on the New Architecture). 30 | /// @return: `true` if the `concurrentRoot` feature is enabled. Otherwise, it returns `false`. 31 | - (BOOL)concurrentRootEnabled 32 | { 33 | return true; 34 | } 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /example/ios/HealthkitExample/HealthkitExample.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.healthkit 6 | 7 | com.apple.developer.healthkit.access 8 | 9 | com.apple.developer.healthkit.background-delivery 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/HealthkitExample/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /example/ios/HealthkitExample/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/HealthkitExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSHealthUpdateUsageDescription 6 | This is an example app that wants to update your health data 7 | NSHealthShareUsageDescription 8 | This is an example app that wants to read your health data 9 | LSApplicationCategoryType 10 | 11 | CFBundleDevelopmentRegion 12 | en 13 | CFBundleDisplayName 14 | example 15 | CFBundleExecutable 16 | $(EXECUTABLE_NAME) 17 | CFBundleIdentifier 18 | $(PRODUCT_BUNDLE_IDENTIFIER) 19 | CFBundleInfoDictionaryVersion 20 | 6.0 21 | CFBundleName 22 | $(PRODUCT_NAME) 23 | CFBundlePackageType 24 | APPL 25 | CFBundleShortVersionString 26 | 1.0 27 | CFBundleSignature 28 | ???? 29 | CFBundleVersion 30 | 1 31 | LSRequiresIPhoneOS 32 | 33 | NSAppTransportSecurity 34 | 35 | NSExceptionDomains 36 | 37 | localhost 38 | 39 | NSExceptionAllowsInsecureHTTPLoads 40 | 41 | 42 | 43 | 44 | NSLocationWhenInUseUsageDescription 45 | 46 | UILaunchStoryboardName 47 | LaunchScreen 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UIViewControllerBasedStatusBarAppearance 59 | 60 | UIAppFonts 61 | 62 | AntDesign.ttf 63 | Entypo.ttf 64 | EvilIcons.ttf 65 | Feather.ttf 66 | FontAwesome.ttf 67 | FontAwesome5_Brands.ttf 68 | FontAwesome5_Regular.ttf 69 | FontAwesome5_Solid.ttf 70 | Foundation.ttf 71 | Ionicons.ttf 72 | MaterialIcons.ttf 73 | MaterialCommunityIcons.ttf 74 | SimpleLineIcons.ttf 75 | Octicons.ttf 76 | Zocial.ttf 77 | Fontisto.ttf 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /example/ios/HealthkitExample/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/ios/HealthkitExample/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | require_relative '../node_modules/react-native/scripts/react_native_pods' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | platform :ios, '14.0' 5 | install! 'cocoapods', :deterministic_uuids => false 6 | 7 | ENV['RCT_NEW_ARCH_ENABLED'] = '0' 8 | 9 | target 'HealthkitExample' do 10 | config = use_native_modules! 11 | 12 | # Flags change depending on the env values. 13 | flags = get_default_flags() 14 | 15 | use_react_native!( 16 | :path => config[:reactNativePath], 17 | # to enable hermes on iOS, change `false` to `true` and then install pods 18 | :hermes_enabled => true, 19 | :fabric_enabled => flags[:fabric_enabled], 20 | # An absolute path to your application root. 21 | :app_path => "#{Pod::Config.instance.installation_root}/.." 22 | ) 23 | 24 | # Enables Flipper. 25 | # 26 | # Note that if you have use_frameworks! enabled, Flipper will not work and 27 | # you should disable the next line. 28 | use_flipper!() 29 | 30 | post_install do |installer| 31 | react_native_post_install(installer) 32 | __apply_Xcode_12_5_M1_post_install_workaround(installer) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const escape = require('escape-string-regexp') 2 | const exclusionList = require('metro-config/src/defaults/exclusionList') 3 | const path = require('path') 4 | 5 | const pak = require('../package.json') 6 | 7 | const root = path.resolve(__dirname, '..') 8 | 9 | const modules = Object.keys({ 10 | ...pak.peerDependencies, 11 | }) 12 | 13 | module.exports = { 14 | projectRoot: __dirname, 15 | watchFolders: [root], 16 | 17 | // We need to make sure that only one version is loaded for peerDependencies 18 | // So we block them at the root, and alias them to the versions in example's node_modules 19 | resolver: { 20 | blacklistRE: exclusionList( 21 | modules.map( 22 | (m) => new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`), 23 | ), 24 | ), 25 | 26 | extraNodeModules: modules.reduce((acc, name) => { 27 | acc[name] = path.join(__dirname, 'node_modules', name) 28 | return acc 29 | }, {}), 30 | }, 31 | 32 | transformer: { 33 | getTransformOptions: () => ({ 34 | transform: { 35 | experimentalImportSupport: false, 36 | inlineRequires: true, 37 | }, 38 | }), 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-healthkit-example", 3 | "description": "Example app for react-native-healthkit", 4 | "version": "0.0.1", 5 | "private": true, 6 | "scripts": { 7 | "ios": "react-native run-ios", 8 | "start": "react-native start", 9 | "pods": "pod-install --quiet", 10 | "postinstall": "patch-package" 11 | }, 12 | "dependencies": { 13 | "@kingstinct/react-native-healthkit": "latest", 14 | "@react-navigation/native": "^6.0.12", 15 | "dayjs": "^1.11.5", 16 | "react": "18.2.0", 17 | "react-native": "0.71.8", 18 | "react-native-paper": "^4.12.4", 19 | "react-native-safe-area-context": "4.5.0", 20 | "react-native-screens": "^3.17.0", 21 | "react-native-vector-icons": "^9.2.0" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.12.10", 25 | "@babel/runtime": "^7.12.5", 26 | "@types/react": "~18.0.27", 27 | "babel-plugin-module-resolver": "^4.1.0", 28 | "metro-react-native-babel-preset": "^0.67.0", 29 | "patch-package": "^6.4.7", 30 | "postinstall-postinstall": "^2.1.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/react-native.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | dependencies: { 5 | '@kingstinct/react-native-healthkit': { 6 | root: path.join(__dirname, '..'), 7 | }, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /example/src/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | import { HKQuantityTypeIdentifier } from '@kingstinct/react-native-healthkit' 3 | 4 | import type { HKQuantitySampleForSaving, CLLocationForSaving } from '@kingstinct/react-native-healthkit' 5 | 6 | const distanceSamples = [ 7 | { 8 | quantity: 1609.4, 9 | unit: 'm', 10 | quantityType: HKQuantityTypeIdentifier.distanceWalkingRunning, 11 | }, 12 | ] as readonly HKQuantitySampleForSaving[] 13 | 14 | const energySamples = [ 15 | { 16 | quantity: 123, 17 | unit: 'kcal', 18 | quantityType: HKQuantityTypeIdentifier.activeEnergyBurned, 19 | }, 20 | ] as readonly HKQuantitySampleForSaving[] 21 | 22 | const hrSamples = [ 23 | { 24 | quantityType: HKQuantityTypeIdentifier.heartRate, 25 | unit: 'count/min', 26 | quantity: 180, 27 | startDate: new Date(1693238969173), 28 | endDate: new Date(1693238969173 + 5000), 29 | }, 30 | { 31 | quantityType: HKQuantityTypeIdentifier.heartRate, 32 | unit: 'count/min', 33 | quantity: 120, 34 | startDate: new Date(1693238969173 + 5000), 35 | endDate: new Date(1693238969173 + 10000), 36 | }, 37 | { 38 | quantityType: HKQuantityTypeIdentifier.heartRate, 39 | unit: 'count/min', 40 | quantity: 90, 41 | startDate: new Date(1693238969173 + 10000), 42 | endDate: new Date(1693238969173 + 15000), 43 | }, 44 | { 45 | quantityType: HKQuantityTypeIdentifier.heartRate, 46 | unit: 'count/min', 47 | quantity: 60, 48 | startDate: new Date(1693238969173 + 15000), 49 | endDate: new Date(1693238969173 + 20000), 50 | }, 51 | { 52 | quantityType: HKQuantityTypeIdentifier.heartRate, 53 | unit: 'count/min', 54 | quantity: 120, 55 | startDate: new Date(1693238969173 + 20000), 56 | endDate: new Date(1693238969173 + 25000), 57 | }, 58 | { 59 | quantityType: HKQuantityTypeIdentifier.heartRate, 60 | unit: 'count/min', 61 | quantity: 110, 62 | startDate: new Date(1693238969173 + 25000), 63 | endDate: new Date(1693238969173 + 30000), 64 | }, 65 | { 66 | quantityType: HKQuantityTypeIdentifier.heartRate, 67 | unit: 'count/min', 68 | quantity: 120, 69 | startDate: new Date(1693238969173 + 30000), 70 | endDate: new Date(1693238969173 + 35000), 71 | }, 72 | { 73 | quantityType: HKQuantityTypeIdentifier.heartRate, 74 | unit: 'count/min', 75 | quantity: 110, 76 | startDate: new Date(1693238969173 + 35000), 77 | endDate: new Date(1693238969173 + 40000), 78 | }, 79 | { 80 | quantityType: HKQuantityTypeIdentifier.heartRate, 81 | unit: 'count/min', 82 | quantity: 120, 83 | startDate: new Date(1693238969173 + 40000), 84 | endDate: new Date(1693238969173 + 45000), 85 | }, 86 | { 87 | quantityType: HKQuantityTypeIdentifier.heartRate, 88 | unit: 'count/min', 89 | quantity: 110, 90 | startDate: new Date(1693238969173 + 45000), 91 | endDate: new Date(1693238969173 + 50000), 92 | }, 93 | ] as readonly HKQuantitySampleForSaving[] 94 | 95 | const runningSpeedSamples = [ 96 | { 97 | quantityType: HKQuantityTypeIdentifier.runningSpeed, 98 | unit: 'm/s', 99 | quantity: 2, 100 | startDate: new Date(1693238969173), 101 | endDate: new Date(1693238969173 + 5000), 102 | }, 103 | { 104 | quantityType: HKQuantityTypeIdentifier.runningSpeed, 105 | unit: 'm/s', 106 | quantity: 2.4, 107 | startDate: new Date(1693238969173 + 5000), 108 | endDate: new Date(1693238969173 + 10000), 109 | }, 110 | { 111 | quantityType: HKQuantityTypeIdentifier.runningSpeed, 112 | unit: 'm/s', 113 | quantity: 2.2, 114 | startDate: new Date(1693238969173 + 10000), 115 | endDate: new Date(1693238969173 + 15000), 116 | }, 117 | { 118 | quantityType: HKQuantityTypeIdentifier.runningSpeed, 119 | unit: 'm/s', 120 | quantity: 2.2, 121 | startDate: new Date(1693238969173 + 15000), 122 | endDate: new Date(1693238969173 + 20000), 123 | }, 124 | { 125 | quantityType: HKQuantityTypeIdentifier.runningSpeed, 126 | unit: 'm/s', 127 | quantity: 2.2, 128 | startDate: new Date(1693238969173 + 20000), 129 | endDate: new Date(1693238969173 + 25000), 130 | }, 131 | { 132 | quantityType: HKQuantityTypeIdentifier.runningSpeed, 133 | unit: 'm/s', 134 | quantity: 2.2, 135 | startDate: new Date(1693238969173 + 25000), 136 | endDate: new Date(1693238969173 + 30000), 137 | }, 138 | { 139 | quantityType: HKQuantityTypeIdentifier.runningSpeed, 140 | unit: 'm/s', 141 | quantity: 2.2, 142 | startDate: new Date(1693238969173 + 35000), 143 | endDate: new Date(1693238969173 + 40000), 144 | }, 145 | { 146 | quantityType: HKQuantityTypeIdentifier.runningSpeed, 147 | unit: 'm/s', 148 | quantity: 2.2, 149 | startDate: new Date(1693238969173 + 40000), 150 | endDate: new Date(1693238969173 + 55000), 151 | }, 152 | ] as readonly HKQuantitySampleForSaving[] 153 | 154 | const locationSamples = [ 155 | { 156 | timestamp: 1693238969173, 157 | latitude: 37.33092521, 158 | longitude: -122.03077056, 159 | speed: 1.1455277261275059, 160 | altitude: 12.7, 161 | verticalAccuracy: 4.7, 162 | horizontalAccuracy: 3.4, 163 | course: -1, 164 | }, 165 | { 166 | timestamp: 1693238969173 + 2000, 167 | latitude: 37.330798, 168 | longitude: -122.03072881, 169 | speed: 1.285095821168931, 170 | altitude: 13.6, 171 | verticalAccuracy: 4.7, 172 | horizontalAccuracy: 3.4, 173 | course: -1, 174 | }, 175 | { 176 | timestamp: 1693238969173 + 4000, 177 | latitude: 37.33074557, 178 | longitude: -122.0306985, 179 | speed: 1.402523088645488, 180 | altitude: 14.5, 181 | verticalAccuracy: 4.7, 182 | horizontalAccuracy: 3.4, 183 | course: -1, 184 | }, 185 | { 186 | timestamp: 1693238969173 + 6000, 187 | latitude: 37.33069642, 188 | longitude: -122.03066881, 189 | speed: 1.5322020207350469, 190 | altitude: 13.7, 191 | verticalAccuracy: 4.7, 192 | horizontalAccuracy: 3.4, 193 | course: -1, 194 | }, 195 | { 196 | timestamp: 1693238969173 + 8000, 197 | latitude: 37.33063636, 198 | longitude: -122.03063618, 199 | speed: 2.1356288346039416, 200 | altitude: 14, 201 | verticalAccuracy: 4.7, 202 | horizontalAccuracy: 3.4, 203 | course: -1, 204 | }, 205 | { 206 | timestamp: 1693238969173 + 10000, 207 | latitude: 37.33058184, 208 | longitude: -122.03060705, 209 | speed: 2.1841806264885295, 210 | altitude: 12.6, 211 | verticalAccuracy: 4.7, 212 | horizontalAccuracy: 3.4, 213 | course: -1, 214 | }, 215 | { 216 | timestamp: 1693238969173 + 12000, 217 | latitude: 37.33051217, 218 | longitude: -122.03054075, 219 | speed: 2.1502912379836863, 220 | altitude: 12.6, 221 | verticalAccuracy: 4.7, 222 | horizontalAccuracy: 3.4, 223 | course: -1, 224 | }, 225 | { 226 | timestamp: 1693238969173 + 14000, 227 | latitude: 37.33047387, 228 | longitude: -122.03043177, 229 | speed: 2.1502912379836863, 230 | altitude: 12.6, 231 | verticalAccuracy: 4.7, 232 | horizontalAccuracy: 3.4, 233 | course: -1, 234 | }, 235 | { 236 | timestamp: 1693238969173 + 16000, 237 | latitude: 37.3304468, 238 | longitude: -122.03030278, 239 | speed: 2.1753854497292195, 240 | altitude: 13.3, 241 | verticalAccuracy: 4.7, 242 | horizontalAccuracy: 3.4, 243 | course: -1, 244 | }, 245 | { 246 | timestamp: 1693238969173 + 18000, 247 | latitude: 37.33043844, 248 | longitude: -122.03023338, 249 | speed: 2.1638740622473494, 250 | altitude: 13.5, 251 | verticalAccuracy: 4.7, 252 | horizontalAccuracy: 3.4, 253 | course: -1, 254 | }, 255 | { 256 | timestamp: 1693238969173 + 20000, 257 | latitude: 37.33043145, 258 | longitude: -122.03009112, 259 | speed: 2.1693958013404178, 260 | altitude: 13.5, 261 | verticalAccuracy: 4.7, 262 | horizontalAccuracy: 3.4, 263 | course: -1, 264 | }, 265 | { 266 | timestamp: 1693238969173 + 22000, 267 | latitude: 37.3304318, 268 | longitude: -122.03001785, 269 | speed: 2.7971076334601155, 270 | altitude: 13.3, 271 | verticalAccuracy: 4.7, 272 | horizontalAccuracy: 3.4, 273 | course: -1, 274 | }, 275 | { 276 | timestamp: 1693238969173 + 24000, 277 | latitude: 37.3304353, 278 | longitude: -122.02993796, 279 | speed: 2.7288485957616135, 280 | altitude: 13.3, 281 | verticalAccuracy: 4.7, 282 | horizontalAccuracy: 3.4, 283 | course: -1, 284 | }, 285 | { 286 | timestamp: 1693238969173 + 26000, 287 | latitude: 37.33044444, 288 | longitude: -122.02977746, 289 | speed: 2.7288485957616135, 290 | altitude: 13.8, 291 | verticalAccuracy: 5.1, 292 | horizontalAccuracy: 2.8, 293 | course: -1, 294 | }, 295 | { 296 | timestamp: 1693238969173 + 28000, 297 | latitude: 37.33044907, 298 | longitude: -122.02969739, 299 | speed: 2.710493562684472, 300 | altitude: 13.7, 301 | verticalAccuracy: 5, 302 | horizontalAccuracy: 2.9, 303 | course: -1, 304 | }, 305 | { 306 | timestamp: 1693238969173 + 30000, 307 | latitude: 37.33045275, 308 | longitude: -122.02953296, 309 | speed: 2.7128010044790143, 310 | altitude: 14.1, 311 | verticalAccuracy: 4.8, 312 | horizontalAccuracy: 3.3, 313 | course: -1, 314 | }, 315 | { 316 | timestamp: 1693238969173 + 32000, 317 | latitude: 37.33045243, 318 | longitude: -122.02944956, 319 | speed: 2.7484091075606267, 320 | altitude: 13.6, 321 | verticalAccuracy: 4.8, 322 | horizontalAccuracy: 3.3, 323 | course: -1, 324 | }, 325 | { 326 | timestamp: 1693238969173 + 34000, 327 | latitude: 37.33039592, 328 | longitude: -122.0293017, 329 | speed: 2.73806763140424, 330 | altitude: 13.3, 331 | verticalAccuracy: 4.8, 332 | horizontalAccuracy: 3.3, 333 | course: -1, 334 | }, 335 | ] as readonly CLLocationForSaving[] 336 | /** 337 | * Generate a unix timestamp from one minute ago 338 | * @returns number 339 | */ 340 | const getTimestampOneMinuteAgo = (): number => Date.now() - 60000 341 | 342 | /** 343 | * Generates HR, distance, energy, speed and location samples to generate a sample Apple Health workout 344 | * @returns number 345 | */ 346 | export const generateWorkoutSamples = () => { 347 | const startTime = getTimestampOneMinuteAgo() 348 | 349 | // get HR samples and change startDate to be 5 seconds later on each sample 350 | const hr = hrSamples.map((sample, index) => ({ 351 | ...sample, 352 | startDate: new Date(startTime + index * 5000), 353 | endDate: new Date(startTime + (index + 1) * 5000), 354 | })) 355 | 356 | const speed = runningSpeedSamples.map((sample, index) => ({ 357 | ...sample, 358 | startDate: new Date(startTime + index * 5000), 359 | endDate: new Date(startTime + (index + 1) * 5000), 360 | })) 361 | 362 | const locations = locationSamples.map((sample, index) => ({ 363 | ...sample, 364 | timestamp: startTime + index * 2000, 365 | })) 366 | 367 | return { 368 | startTime, 369 | samples: [ 370 | ...distanceSamples, 371 | ...energySamples, 372 | ...hr, 373 | ...speed, 374 | ], 375 | locationSamples: locations, 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | }, 6 | "baseUrl": ".", 7 | "paths": { 8 | "@kingstinct/react-native-healthkit": [ "../src/index" ], 9 | "@kingstinct/react-native-healthkit/*": [ "../src/*" ], 10 | "example": [ "../example" ], 11 | "example/*": [ "../example/*" ], 12 | "example/src/*": [ "../example/src/*" ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ios/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // kingstinct-react-native-healthkit 4 | // 5 | // Created by Robert Herber on 2023-05-31. 6 | // 7 | 8 | import Foundation 9 | import HealthKit 10 | 11 | let INIT_ERROR = "HEALTHKIT_INIT_ERROR" 12 | let INIT_ERROR_MESSAGE = "HealthKit not initialized" 13 | let TYPE_IDENTIFIER_ERROR = "HEALTHKIT_TYPE_IDENTIFIER_NOT_RECOGNIZED_ERROR" 14 | let QUERY_ERROR = "HEALTHKIT_QUERY_ERROR" 15 | let GENERIC_ERROR = "HEALTHKIT_ERROR" 16 | 17 | let HKCharacteristicTypeIdentifier_PREFIX = "HKCharacteristicTypeIdentifier" 18 | let HKQuantityTypeIdentifier_PREFIX = "HKQuantityTypeIdentifier" 19 | let HKCategoryTypeIdentifier_PREFIX = "HKCategoryTypeIdentifier" 20 | let HKCorrelationTypeIdentifier_PREFIX = "HKCorrelationTypeIdentifier" 21 | let HKActivitySummaryTypeIdentifier = "HKActivitySummaryTypeIdentifier" 22 | let HKAudiogramTypeIdentifier = "HKAudiogramTypeIdentifier" 23 | let HKWorkoutTypeIdentifier = "HKWorkoutTypeIdentifier" 24 | let HKWorkoutRouteTypeIdentifier = "HKWorkoutRouteTypeIdentifier" 25 | let HKDataTypeIdentifierHeartbeatSeries = "HKDataTypeIdentifierHeartbeatSeries" 26 | let HKStateOfMindTypeIdentifier = "HKStateOfMindTypeIdentifier" 27 | 28 | let HKWorkoutActivityTypePropertyName = "activityType" 29 | let HKWorkoutSessionLocationTypePropertyName = "locationType" 30 | 31 | let SpeedUnit = HKUnit(from: "m/s") // HKUnit.meter().unitDivided(by: HKUnit.second()) 32 | // Support for MET data: HKAverageMETs 8.24046 kcal/hr·kg 33 | let METUnit = HKUnit(from: "kcal/hr·kg") 34 | -------------------------------------------------------------------------------- /ios/ReactNativeHealthkit-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | -------------------------------------------------------------------------------- /ios/ReactNativeHealthkit.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F4FF95D7245B92E800C19C63 /* ReactNativeHealthkit.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FF95D6245B92E800C19C63 /* ReactNativeHealthkit.swift */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXCopyFilesBuildPhase section */ 14 | 58B511D91A9E6C8500147676 /* CopyFiles */ = { 15 | isa = PBXCopyFilesBuildPhase; 16 | buildActionMask = 2147483647; 17 | dstPath = "include/$(PRODUCT_NAME)"; 18 | dstSubfolderSpec = 16; 19 | files = ( 20 | ); 21 | runOnlyForDeploymentPostprocessing = 0; 22 | }; 23 | /* End PBXCopyFilesBuildPhase section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 134814201AA4EA6300B7C361 /* libReactNativeHealthkit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libReactNativeHealthkit.a; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | B3E7B5891CC2AC0600A0062D /* ReactNativeHealthkit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ReactNativeHealthkit.m; sourceTree = ""; }; 28 | F4FF95D5245B92E700C19C63 /* ReactNativeHealthkit-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeHealthkit-Bridging-Header.h"; sourceTree = ""; }; 29 | F4FF95D6245B92E800C19C63 /* ReactNativeHealthkit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactNativeHealthkit.swift; sourceTree = ""; }; 30 | /* End PBXFileReference section */ 31 | 32 | /* Begin PBXFrameworksBuildPhase section */ 33 | 58B511D81A9E6C8500147676 /* Frameworks */ = { 34 | isa = PBXFrameworksBuildPhase; 35 | buildActionMask = 2147483647; 36 | files = ( 37 | ); 38 | runOnlyForDeploymentPostprocessing = 0; 39 | }; 40 | /* End PBXFrameworksBuildPhase section */ 41 | 42 | /* Begin PBXGroup section */ 43 | 134814211AA4EA7D00B7C361 /* Products */ = { 44 | isa = PBXGroup; 45 | children = ( 46 | 134814201AA4EA6300B7C361 /* libReactNativeHealthkit.a */, 47 | ); 48 | name = Products; 49 | sourceTree = ""; 50 | }; 51 | 58B511D21A9E6C8500147676 = { 52 | isa = PBXGroup; 53 | children = ( 54 | F4FF95D6245B92E800C19C63 /* ReactNativeHealthkit.swift */, 55 | B3E7B5891CC2AC0600A0062D /* ReactNativeHealthkit.m */, 56 | F4FF95D5245B92E700C19C63 /* ReactNativeHealthkit-Bridging-Header.h */, 57 | 134814211AA4EA7D00B7C361 /* Products */, 58 | ); 59 | sourceTree = ""; 60 | }; 61 | /* End PBXGroup section */ 62 | 63 | /* Begin PBXNativeTarget section */ 64 | 58B511DA1A9E6C8500147676 /* ReactNativeHealthkit */ = { 65 | isa = PBXNativeTarget; 66 | buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "ReactNativeHealthkit" */; 67 | buildPhases = ( 68 | 58B511D71A9E6C8500147676 /* Sources */, 69 | 58B511D81A9E6C8500147676 /* Frameworks */, 70 | 58B511D91A9E6C8500147676 /* CopyFiles */, 71 | ); 72 | buildRules = ( 73 | ); 74 | dependencies = ( 75 | ); 76 | name = ReactNativeHealthkit; 77 | productName = RCTDataManager; 78 | productReference = 134814201AA4EA6300B7C361 /* libReactNativeHealthkit.a */; 79 | productType = "com.apple.product-type.library.static"; 80 | }; 81 | /* End PBXNativeTarget section */ 82 | 83 | /* Begin PBXProject section */ 84 | 58B511D31A9E6C8500147676 /* Project object */ = { 85 | isa = PBXProject; 86 | attributes = { 87 | LastUpgradeCheck = 0920; 88 | ORGANIZATIONNAME = Facebook; 89 | TargetAttributes = { 90 | 58B511DA1A9E6C8500147676 = { 91 | CreatedOnToolsVersion = 6.1.1; 92 | }; 93 | }; 94 | }; 95 | buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "ReactNativeHealthkit" */; 96 | compatibilityVersion = "Xcode 3.2"; 97 | developmentRegion = English; 98 | hasScannedForEncodings = 0; 99 | knownRegions = ( 100 | English, 101 | en, 102 | ); 103 | mainGroup = 58B511D21A9E6C8500147676; 104 | productRefGroup = 58B511D21A9E6C8500147676; 105 | projectDirPath = ""; 106 | projectRoot = ""; 107 | targets = ( 108 | 58B511DA1A9E6C8500147676 /* ReactNativeHealthkit */, 109 | ); 110 | }; 111 | /* End PBXProject section */ 112 | 113 | /* Begin PBXSourcesBuildPhase section */ 114 | 58B511D71A9E6C8500147676 /* Sources */ = { 115 | isa = PBXSourcesBuildPhase; 116 | buildActionMask = 2147483647; 117 | files = ( 118 | F4FF95D7245B92E800C19C63 /* ReactNativeHealthkit.swift in Sources */, 119 | ); 120 | runOnlyForDeploymentPostprocessing = 0; 121 | }; 122 | /* End PBXSourcesBuildPhase section */ 123 | 124 | /* Begin XCBuildConfiguration section */ 125 | 58B511ED1A9E6C8500147676 /* Debug */ = { 126 | isa = XCBuildConfiguration; 127 | buildSettings = { 128 | ALWAYS_SEARCH_USER_PATHS = NO; 129 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 130 | CLANG_CXX_LIBRARY = "libc++"; 131 | CLANG_ENABLE_MODULES = YES; 132 | CLANG_ENABLE_OBJC_ARC = YES; 133 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 134 | CLANG_WARN_BOOL_CONVERSION = YES; 135 | CLANG_WARN_COMMA = YES; 136 | CLANG_WARN_CONSTANT_CONVERSION = YES; 137 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 138 | CLANG_WARN_EMPTY_BODY = YES; 139 | CLANG_WARN_ENUM_CONVERSION = YES; 140 | CLANG_WARN_INFINITE_RECURSION = YES; 141 | CLANG_WARN_INT_CONVERSION = YES; 142 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 143 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 144 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 145 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 146 | CLANG_WARN_STRICT_PROTOTYPES = YES; 147 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 148 | CLANG_WARN_UNREACHABLE_CODE = YES; 149 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 150 | COPY_PHASE_STRIP = NO; 151 | ENABLE_STRICT_OBJC_MSGSEND = YES; 152 | ENABLE_TESTABILITY = YES; 153 | GCC_C_LANGUAGE_STANDARD = gnu99; 154 | GCC_DYNAMIC_NO_PIC = NO; 155 | GCC_NO_COMMON_BLOCKS = YES; 156 | GCC_OPTIMIZATION_LEVEL = 0; 157 | GCC_PREPROCESSOR_DEFINITIONS = ( 158 | "DEBUG=1", 159 | "$(inherited)", 160 | ); 161 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 162 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 163 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 164 | GCC_WARN_UNDECLARED_SELECTOR = YES; 165 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 166 | GCC_WARN_UNUSED_FUNCTION = YES; 167 | GCC_WARN_UNUSED_VARIABLE = YES; 168 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 169 | MTL_ENABLE_DEBUG_INFO = YES; 170 | ONLY_ACTIVE_ARCH = YES; 171 | SDKROOT = iphoneos; 172 | }; 173 | name = Debug; 174 | }; 175 | 58B511EE1A9E6C8500147676 /* Release */ = { 176 | isa = XCBuildConfiguration; 177 | buildSettings = { 178 | ALWAYS_SEARCH_USER_PATHS = NO; 179 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 180 | CLANG_CXX_LIBRARY = "libc++"; 181 | CLANG_ENABLE_MODULES = YES; 182 | CLANG_ENABLE_OBJC_ARC = YES; 183 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 184 | CLANG_WARN_BOOL_CONVERSION = YES; 185 | CLANG_WARN_COMMA = YES; 186 | CLANG_WARN_CONSTANT_CONVERSION = YES; 187 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 188 | CLANG_WARN_EMPTY_BODY = YES; 189 | CLANG_WARN_ENUM_CONVERSION = YES; 190 | CLANG_WARN_INFINITE_RECURSION = YES; 191 | CLANG_WARN_INT_CONVERSION = YES; 192 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 193 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 194 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 195 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 196 | CLANG_WARN_STRICT_PROTOTYPES = YES; 197 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 198 | CLANG_WARN_UNREACHABLE_CODE = YES; 199 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 200 | COPY_PHASE_STRIP = YES; 201 | ENABLE_NS_ASSERTIONS = NO; 202 | ENABLE_STRICT_OBJC_MSGSEND = YES; 203 | GCC_C_LANGUAGE_STANDARD = gnu99; 204 | GCC_NO_COMMON_BLOCKS = YES; 205 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 206 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 207 | GCC_WARN_UNDECLARED_SELECTOR = YES; 208 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 209 | GCC_WARN_UNUSED_FUNCTION = YES; 210 | GCC_WARN_UNUSED_VARIABLE = YES; 211 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 212 | MTL_ENABLE_DEBUG_INFO = NO; 213 | SDKROOT = iphoneos; 214 | VALIDATE_PRODUCT = YES; 215 | }; 216 | name = Release; 217 | }; 218 | 58B511F01A9E6C8500147676 /* Debug */ = { 219 | isa = XCBuildConfiguration; 220 | buildSettings = { 221 | HEADER_SEARCH_PATHS = ( 222 | "$(inherited)", 223 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 224 | "$(SRCROOT)/../../../React/**", 225 | "$(SRCROOT)/../../react-native/React/**", 226 | ); 227 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 228 | OTHER_LDFLAGS = "-ObjC"; 229 | PRODUCT_NAME = ReactNativeHealthkit; 230 | SKIP_INSTALL = YES; 231 | SWIFT_OBJC_BRIDGING_HEADER = "ReactNativeHealthkit-Bridging-Header.h"; 232 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 233 | SWIFT_VERSION = 5.0; 234 | }; 235 | name = Debug; 236 | }; 237 | 58B511F11A9E6C8500147676 /* Release */ = { 238 | isa = XCBuildConfiguration; 239 | buildSettings = { 240 | HEADER_SEARCH_PATHS = ( 241 | "$(inherited)", 242 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 243 | "$(SRCROOT)/../../../React/**", 244 | "$(SRCROOT)/../../react-native/React/**", 245 | ); 246 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 247 | OTHER_LDFLAGS = "-ObjC"; 248 | PRODUCT_NAME = ReactNativeHealthkit; 249 | SKIP_INSTALL = YES; 250 | SWIFT_OBJC_BRIDGING_HEADER = "ReactNativeHealthkit-Bridging-Header.h"; 251 | SWIFT_VERSION = 5.0; 252 | }; 253 | name = Release; 254 | }; 255 | /* End XCBuildConfiguration section */ 256 | 257 | /* Begin XCConfigurationList section */ 258 | 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "ReactNativeHealthkit" */ = { 259 | isa = XCConfigurationList; 260 | buildConfigurations = ( 261 | 58B511ED1A9E6C8500147676 /* Debug */, 262 | 58B511EE1A9E6C8500147676 /* Release */, 263 | ); 264 | defaultConfigurationIsVisible = 0; 265 | defaultConfigurationName = Release; 266 | }; 267 | 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "ReactNativeHealthkit" */ = { 268 | isa = XCConfigurationList; 269 | buildConfigurations = ( 270 | 58B511F01A9E6C8500147676 /* Debug */, 271 | 58B511F11A9E6C8500147676 /* Release */, 272 | ); 273 | defaultConfigurationIsVisible = 0; 274 | defaultConfigurationName = Release; 275 | }; 276 | /* End XCConfigurationList section */ 277 | }; 278 | rootObject = 58B511D31A9E6C8500147676 /* Project object */; 279 | } 280 | -------------------------------------------------------------------------------- /ios/Serializers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Serializers.swift 3 | // kingstinct-react-native-healthkit 4 | // 5 | // Created by Robert Herber on 2023-05-31. 6 | // 7 | 8 | import Foundation 9 | import HealthKit 10 | 11 | let _dateFormatter = ISO8601DateFormatter() 12 | 13 | func serializeQuantity(unit: HKUnit, quantity: HKQuantity?) -> [String: Any]? { 14 | guard let q = quantity else { 15 | return nil 16 | } 17 | 18 | return [ 19 | "quantity": q.doubleValue(for: unit), 20 | "unit": unit.unitString 21 | ] 22 | } 23 | 24 | func serializeQuantitySample(sample: HKQuantitySample, unit: HKUnit) -> NSDictionary { 25 | let endDate = _dateFormatter.string(from: sample.endDate) 26 | let startDate = _dateFormatter.string(from: sample.startDate) 27 | 28 | let quantity = sample.quantity.doubleValue(for: unit) 29 | 30 | return [ 31 | "uuid": sample.uuid.uuidString, 32 | "device": serializeDevice(_device: sample.device) as Any, 33 | "quantityType": sample.quantityType.identifier, 34 | "endDate": endDate, 35 | "startDate": startDate, 36 | "quantity": quantity, 37 | "unit": unit.unitString, 38 | "metadata": serializeMetadata(metadata: sample.metadata), 39 | "sourceRevision": serializeSourceRevision(_sourceRevision: sample.sourceRevision) as Any 40 | ] 41 | } 42 | 43 | func serializeDeletedSample(sample: HKDeletedObject) -> NSDictionary { 44 | return [ 45 | "uuid": sample.uuid.uuidString, 46 | "metadata": serializeMetadata(metadata: sample.metadata) 47 | ] 48 | } 49 | 50 | func serializeCategorySample(sample: HKCategorySample) -> NSDictionary { 51 | let endDate = _dateFormatter.string(from: sample.endDate) 52 | let startDate = _dateFormatter.string(from: sample.startDate) 53 | 54 | return [ 55 | "uuid": sample.uuid.uuidString, 56 | "device": serializeDevice(_device: sample.device) as Any, 57 | "categoryType": sample.categoryType.identifier, 58 | "endDate": endDate, 59 | "startDate": startDate, 60 | "value": sample.value, 61 | "metadata": serializeMetadata(metadata: sample.metadata), 62 | "sourceRevision": serializeSourceRevision(_sourceRevision: sample.sourceRevision) as Any 63 | ] 64 | } 65 | 66 | func serializeSource(source: HKSource) -> NSDictionary { 67 | 68 | return [ 69 | "bundleIdentifier": source.bundleIdentifier, 70 | "name": source.name 71 | ] 72 | } 73 | 74 | func serializeUnknownQuantity(quantity: HKQuantity) -> [String: Any]? { 75 | if quantity.is(compatibleWith: HKUnit.percent()) { 76 | return serializeQuantity(unit: HKUnit.percent(), quantity: quantity) 77 | } 78 | 79 | if quantity.is(compatibleWith: HKUnit.second()) { 80 | return serializeQuantity(unit: HKUnit.second(), quantity: quantity) 81 | } 82 | 83 | if quantity.is(compatibleWith: HKUnit.kilocalorie()) { 84 | return serializeQuantity(unit: HKUnit.kilocalorie(), quantity: quantity) 85 | } 86 | 87 | if quantity.is(compatibleWith: HKUnit.count()) { 88 | return serializeQuantity(unit: HKUnit.count(), quantity: quantity) 89 | } 90 | 91 | if quantity.is(compatibleWith: HKUnit.meter()) { 92 | return serializeQuantity(unit: HKUnit.meter(), quantity: quantity) 93 | } 94 | 95 | if #available(iOS 11, *) { 96 | if quantity.is(compatibleWith: HKUnit.internationalUnit()) { 97 | return serializeQuantity(unit: HKUnit.internationalUnit(), quantity: quantity) 98 | } 99 | } 100 | 101 | if #available(iOS 13, *) { 102 | if quantity.is(compatibleWith: HKUnit.hertz()) { 103 | return serializeQuantity(unit: HKUnit.hertz(), quantity: quantity) 104 | } 105 | if quantity.is(compatibleWith: HKUnit.decibelHearingLevel()) { 106 | return serializeQuantity(unit: HKUnit.decibelHearingLevel(), quantity: quantity) 107 | } 108 | } 109 | 110 | if #available(iOS 17.0, *) { 111 | if quantity.is(compatibleWith: HKUnit.lux()) { 112 | return serializeQuantity(unit: HKUnit.lux(), quantity: quantity) 113 | } 114 | } 115 | 116 | #if compiler(>=6) 117 | if #available(iOS 18.0, *) { 118 | if quantity.is(compatibleWith: HKUnit.appleEffortScore()) { 119 | return serializeQuantity(unit: HKUnit.appleEffortScore(), quantity: quantity) 120 | } 121 | } 122 | #endif 123 | 124 | if quantity.is(compatibleWith: SpeedUnit) { 125 | return serializeQuantity(unit: SpeedUnit, quantity: quantity) 126 | } 127 | 128 | if quantity.is(compatibleWith: METUnit) { 129 | return serializeQuantity(unit: METUnit, quantity: quantity) 130 | } 131 | 132 | return nil 133 | } 134 | 135 | func serializeMetadata(metadata: [String: Any]?) -> NSDictionary { 136 | let serialized: NSMutableDictionary = [:] 137 | if let m = metadata { 138 | for item in m { 139 | if let bool = item.value as? Bool { 140 | serialized.setValue(bool, forKey: item.key) 141 | } 142 | if let str = item.value as? String { 143 | serialized.setValue(str, forKey: item.key) 144 | } 145 | 146 | if let double = item.value as? Double { 147 | serialized.setValue(double, forKey: item.key) 148 | } 149 | if let quantity = item.value as? HKQuantity { 150 | if let s = serializeUnknownQuantity(quantity: quantity) { 151 | serialized.setValue(s, forKey: item.key) 152 | } 153 | } 154 | } 155 | } 156 | return serialized 157 | } 158 | 159 | func serializeDevice(_device: HKDevice?) -> [String: String?]? { 160 | guard let device = _device else { 161 | return nil 162 | } 163 | 164 | return [ 165 | "name": device.name, 166 | "firmwareVersion": device.firmwareVersion, 167 | "hardwareVersion": device.hardwareVersion, 168 | "localIdentifier": device.localIdentifier, 169 | "manufacturer": device.manufacturer, 170 | "model": device.model, 171 | "softwareVersion": device.softwareVersion, 172 | "udiDeviceIdentifier": device.udiDeviceIdentifier 173 | ] 174 | } 175 | 176 | func serializeOperatingSystemVersion(_version: OperatingSystemVersion?) -> String? { 177 | guard let version = _version else { 178 | return nil 179 | } 180 | 181 | let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" 182 | 183 | return versionString 184 | } 185 | 186 | func serializeSourceRevision(_sourceRevision: HKSourceRevision?) -> [String: Any?]? { 187 | guard let sourceRevision = _sourceRevision else { 188 | return nil 189 | } 190 | 191 | var dict = [ 192 | "source": [ 193 | "name": sourceRevision.source.name, 194 | "bundleIdentifier": sourceRevision.source.bundleIdentifier 195 | ], 196 | "version": sourceRevision.version as Any 197 | ] as [String: Any] 198 | 199 | if #available(iOS 11, *) { 200 | dict["operatingSystemVersion"] = serializeOperatingSystemVersion(_version: sourceRevision.operatingSystemVersion) 201 | dict["productType"] = sourceRevision.productType 202 | } 203 | 204 | return dict 205 | } 206 | 207 | func deserializeHKQueryAnchor(anchor: String) -> HKQueryAnchor? { 208 | return anchor.isEmpty ? nil : base64StringToHKQueryAnchor(base64String: anchor) 209 | } 210 | 211 | func serializeAnchor(anchor: HKQueryAnchor?) -> String? { 212 | guard let anch = anchor else { 213 | return nil 214 | } 215 | 216 | let data = NSKeyedArchiver.archivedData(withRootObject: anch) 217 | let encoded = data.base64EncodedString() 218 | 219 | return encoded 220 | } 221 | 222 | func serializeStatistic(unit: HKUnit, quantity: HKQuantity?, stats: HKStatistics?) -> [String: Any]? { 223 | guard let q = quantity, let stats = stats else { 224 | return nil 225 | } 226 | 227 | let endDate = _dateFormatter.string(from: stats.endDate) 228 | let startDate = _dateFormatter.string(from: stats.startDate) 229 | let quantityType = stats.quantityType.identifier 230 | 231 | return [ 232 | "quantityType": quantityType, 233 | "startDate": startDate, 234 | "endDate": endDate, 235 | "quantity": q.doubleValue(for: unit), 236 | "unit": unit.unitString 237 | ] 238 | } 239 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "react-native", 3 | "modulePathIgnorePatterns": [ 4 | "/example/node_modules", 5 | "/lib/" 6 | ], 7 | "setupFiles": [ "/src/jest.setup.ts" ], 8 | "testEnvironment": "jsdom" 9 | } 10 | -------------------------------------------------------------------------------- /kingstinct-react-native-healthkit.podspec: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, "package.json"))) 4 | 5 | Pod::Spec.new do |s| 6 | s.name = "kingstinct-react-native-healthkit" 7 | s.version = package["version"] 8 | s.summary = package["description"] 9 | s.homepage = package["homepage"] 10 | s.license = package["license"] 11 | s.authors = package["author"] 12 | 13 | s.platforms = { :ios => "10.0" } 14 | s.source = { :git => "https://github.com/kingstinct/react-native-healthkit.git", :tag => "#{s.version}" } 15 | 16 | 17 | s.source_files = "ios/**/*.{h,m,mm,swift}" 18 | 19 | 20 | s.dependency "React" 21 | end 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kingstinct/react-native-healthkit", 3 | "version": "8.5.0", 4 | "description": "React Native bindings for HealthKit", 5 | "main": "lib/commonjs/index", 6 | "module": "lib/module/index", 7 | "types": "lib/typescript/src/index.d.ts", 8 | "react-native": "src/index", 9 | "source": "src/index", 10 | "files": [ 11 | "src", 12 | "lib", 13 | "ios", 14 | "cpp", 15 | "kingstinct-react-native-healthkit.podspec", 16 | "!lib/typescript/example", 17 | "!**/__tests__", 18 | "!**/__fixtures__", 19 | "!**/__mocks__", 20 | "app.plugin.js" 21 | ], 22 | "scripts": { 23 | "test-only": "bun test --preload ./src/test-setup.ts src", 24 | "test": "concurrently \"bun test-only\" \"bun typecheck\" \"bun lint\"", 25 | "typecheck": "tsc --noEmit", 26 | "lint": "eslint \"**/*.{js,ts,tsx}\" --cache", 27 | "prepare": "bob build && husky install", 28 | "release": "release-it", 29 | "pods": "cd example && pod-install --quiet", 30 | "bootstrap": "cd example && bun install && bun pods", 31 | "upgrade-interactive": "bunx npm-check-updates --format group -i" 32 | }, 33 | "lint-staged": { 34 | "*.swift": "swiftlint ios --fix", 35 | "*.ts": "eslint --fix", 36 | "*.js": "eslint --fix", 37 | "*.json": "eslint --fix" 38 | }, 39 | "keywords": [ 40 | "react-native", 41 | "ios", 42 | "healthkit", 43 | "typescript", 44 | "react-hooks" 45 | ], 46 | "repository": "https://github.com/kingstinct/react-native-healthkit", 47 | "funding": [ 48 | "https://github.com/sponsors/kingstinct", 49 | "https://github.com/sponsors/robertherber" 50 | ], 51 | "author": { 52 | "name": "Robert Herber", 53 | "email": "robert@kingstinct.com", 54 | "url": "https://github.com/robertherber" 55 | }, 56 | "license": "MIT", 57 | "bugs": { 58 | "url": "https://github.com/kingstinct/react-native-healthkit/issues" 59 | }, 60 | "homepage": "https://github.com/kingstinct/react-native-healthkit#readme", 61 | "devDependencies": { 62 | "@babel/core": ">=7", 63 | "@babel/preset-env": "^7.18.10", 64 | "@commitlint/config-conventional": "12", 65 | "@expo/config-plugins": "^6.0.1", 66 | "@graphql-eslint/eslint-plugin": ">=3", 67 | "@release-it/conventional-changelog": "2", 68 | "@testing-library/react-native": "12", 69 | "@types/bun": "^1.1.3", 70 | "@types/node": "^18.7.14", 71 | "@types/react": "~18.0.27", 72 | "@typescript-eslint/eslint-plugin": ">=5", 73 | "@typescript-eslint/parser": ">=5", 74 | "commitlint": "^17.1.1", 75 | "concurrently": "8", 76 | "eslint": "8", 77 | "eslint-config-airbnb": ">=19", 78 | "eslint-config-airbnb-base": ">=15", 79 | "eslint-config-kingstinct": "^5.1.1", 80 | "eslint-import-resolver-typescript": ">=3", 81 | "eslint-plugin-comment-length": ">=0", 82 | "eslint-plugin-functional": ">=4", 83 | "eslint-plugin-import": ">=2", 84 | "eslint-plugin-jest": ">=26", 85 | "eslint-plugin-jsonc": ">=2", 86 | "eslint-plugin-jsx-a11y": ">=6", 87 | "eslint-plugin-react": ">=7", 88 | "eslint-plugin-react-hooks": ">=4", 89 | "eslint-plugin-react-native": ">=4", 90 | "eslint-plugin-react-native-a11y": ">=3", 91 | "eslint-plugin-unicorn": ">=43", 92 | "eslint-plugin-yml": ">=1", 93 | "expo": ">=45", 94 | "graphql": ">=16", 95 | "husky": "8", 96 | "lint-staged": "^13.2.2", 97 | "metro-react-native-babel-preset": "^0.70.3", 98 | "pod-install": "^0.1.0", 99 | "react": "18.2.0", 100 | "react-native": "0.71.8", 101 | "react-native-builder-bob": "^0.18.1", 102 | "react-test-renderer": "18", 103 | "release-it": "14", 104 | "solidarity": "^3.0.4", 105 | "solidarity-react-native": "^2.1.2", 106 | "typescript": "^4.9.4" 107 | }, 108 | "peerDependencies": { 109 | "react": "*", 110 | "react-native": "*" 111 | }, 112 | "commitlint": { 113 | "extends": [ 114 | "@commitlint/config-conventional" 115 | ] 116 | }, 117 | "publishConfig": { 118 | "access": "public", 119 | "registry": "https://registry.npmjs.org/" 120 | }, 121 | "release-it": { 122 | "git": { 123 | "commitMessage": "chore: release ${version}", 124 | "tagName": "v${version}" 125 | }, 126 | "npm": { 127 | "publish": true 128 | }, 129 | "github": { 130 | "release": true 131 | }, 132 | "plugins": { 133 | "@release-it/conventional-changelog": { 134 | "preset": "angular", 135 | "infile": "CHANGELOG.md" 136 | } 137 | } 138 | }, 139 | "eslintIgnore": [ 140 | "node_modules/", 141 | "lib/" 142 | ], 143 | "react-native-builder-bob": { 144 | "source": "src", 145 | "output": "lib", 146 | "targets": [ 147 | "commonjs", 148 | "module", 149 | "typescript" 150 | ] 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/hooks/useHealthkitAuthorization.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react-native' 2 | 3 | import waitForNextUpdate from '../test-utils' 4 | 5 | describe('useHealthkitAuthorization', () => { 6 | let NativeTypes: typeof import('../native-types') 7 | let useHealthkitAuthorization: typeof import('./useHealthkitAuthorization').default 8 | beforeAll(async () => { 9 | NativeTypes = await import('../native-types') 10 | useHealthkitAuthorization = (await import('./useHealthkitAuthorization')).default 11 | }) 12 | 13 | test('should return shouldRequest', async () => { 14 | const { HKAuthorizationRequestStatus, HKCategoryTypeIdentifier, default: Native } = NativeTypes 15 | 16 | jest.spyOn(Native, 'getRequestStatusForAuthorization').mockReturnValue(Promise.resolve(HKAuthorizationRequestStatus.shouldRequest)) 17 | 18 | const { result } = renderHook(() => useHealthkitAuthorization([HKCategoryTypeIdentifier.abdominalCramps])) 19 | 20 | await waitForNextUpdate() 21 | 22 | expect(result.current[0]).toBe(HKAuthorizationRequestStatus.shouldRequest) 23 | }) 24 | 25 | test('should request permissions', async () => { 26 | const { HKAuthorizationRequestStatus, HKCategoryTypeIdentifier, default: Native } = NativeTypes 27 | 28 | const spy = jest.spyOn(Native, 'getRequestStatusForAuthorization').mockReturnValue(Promise.resolve(HKAuthorizationRequestStatus.shouldRequest)) 29 | jest.spyOn(Native, 'requestAuthorization').mockReturnValue(Promise.resolve(true)) 30 | 31 | const { result } = renderHook(() => useHealthkitAuthorization([HKCategoryTypeIdentifier.abdominalCramps])) 32 | 33 | await waitForNextUpdate() 34 | 35 | spy.mockReturnValue(Promise.resolve(HKAuthorizationRequestStatus.unnecessary)) 36 | 37 | let retVal: typeof HKAuthorizationRequestStatus | undefined 38 | await act(async () => { 39 | const r = await result.current[1]() as unknown as typeof HKAuthorizationRequestStatus 40 | retVal = r 41 | }) 42 | 43 | expect(result.current[0]).toBe(HKAuthorizationRequestStatus.unnecessary) 44 | expect(retVal).toBe(HKAuthorizationRequestStatus.unnecessary) 45 | }) 46 | 47 | test('should return unnecessary', async () => { 48 | const { HKAuthorizationRequestStatus, HKCategoryTypeIdentifier, default: Native } = NativeTypes 49 | 50 | jest.spyOn(Native, 'getRequestStatusForAuthorization').mockReturnValue(Promise.resolve(HKAuthorizationRequestStatus.unnecessary)) 51 | 52 | const { result } = renderHook(() => useHealthkitAuthorization([HKCategoryTypeIdentifier.abdominalCramps])) 53 | 54 | await waitForNextUpdate() 55 | 56 | expect(result.current[0]).toBe(HKAuthorizationRequestStatus.unnecessary) 57 | }) 58 | 59 | test('should return null before initalizing', async () => { 60 | const { HKCategoryTypeIdentifier } = NativeTypes 61 | 62 | const { result } = renderHook(() => useHealthkitAuthorization([HKCategoryTypeIdentifier.abdominalCramps])) 63 | 64 | expect(result.current[0]).toBe(null) 65 | 66 | await waitForNextUpdate() 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /src/hooks/useHealthkitAuthorization.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useCallback, useEffect, useRef, useState, 3 | } from 'react' 4 | 5 | import getRequestStatusForAuthorization from '../utils/getRequestStatusForAuthorization' 6 | import requestAuthorization from '../utils/requestAuthorization' 7 | 8 | import type { HealthkitReadAuthorization, HealthkitWriteAuthorization, HKAuthorizationRequestStatus } from '../native-types' 9 | 10 | /** 11 | * @description Hook to retrieve the current authorization status for the given types, and request authorization if needed. 12 | * @see {@link https://developer.apple.com/documentation/healthkit/hkhealthstore/1614152-requestauthorization Apple Docs - requestAuthorization} 13 | * @see {@link https://developer.apple.com/documentation/healthkit/authorizing_access_to_health_data Apple Docs - Authorizing access to health data} 14 | */ 15 | const useHealthkitAuthorization = (read: readonly HealthkitReadAuthorization[], write?: readonly HealthkitWriteAuthorization[]) => { 16 | const [status, setStatus] = useState(null) 17 | 18 | const readMemo = useRef(read) 19 | const writeMemo = useRef(write) 20 | 21 | useEffect(() => { 22 | readMemo.current = read 23 | writeMemo.current = write 24 | }, [read, write]) 25 | 26 | const refreshAuthStatus = useCallback(async () => { 27 | const auth = await getRequestStatusForAuthorization(readMemo.current, writeMemo.current) 28 | 29 | setStatus(auth) 30 | return auth 31 | }, []) 32 | 33 | const request = useCallback(async () => { 34 | await requestAuthorization(readMemo.current, writeMemo.current) 35 | return refreshAuthStatus() 36 | }, [refreshAuthStatus]) 37 | 38 | useEffect(() => { 39 | void refreshAuthStatus() 40 | }, [refreshAuthStatus]) 41 | 42 | return [status, request] as const 43 | } 44 | 45 | export default useHealthkitAuthorization 46 | -------------------------------------------------------------------------------- /src/hooks/useIsHealthDataAvailable.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-native' 2 | 3 | import waitForNextUpdate from '../test-utils' 4 | 5 | describe('useIsHealthDataAvailable', () => { 6 | test('should return false', async () => { 7 | const useIsHealthDataAvailable = (await import('./useIsHealthDataAvailable')).default 8 | const { default: Native } = await import('../native-types') 9 | jest.spyOn(Native, 'isHealthDataAvailable').mockReturnValue(Promise.resolve(false)) 10 | 11 | const { result } = renderHook(useIsHealthDataAvailable) 12 | 13 | await waitForNextUpdate() 14 | 15 | expect(result.current).toBe(false) 16 | }) 17 | 18 | test('should return true', async () => { 19 | const useIsHealthDataAvailable = (await import('./useIsHealthDataAvailable')).default 20 | const { default: Native } = await import('../native-types') 21 | jest.spyOn(Native, 'isHealthDataAvailable').mockReturnValue(Promise.resolve(true)) 22 | 23 | const { result } = renderHook(useIsHealthDataAvailable) 24 | 25 | await waitForNextUpdate() 26 | 27 | expect(result.current).toBe(true) 28 | }) 29 | 30 | test('should return null before initalizing', async () => { 31 | const useIsHealthDataAvailable = (await import('./useIsHealthDataAvailable')).default 32 | const { result } = renderHook(useIsHealthDataAvailable) 33 | 34 | expect(result.current).toBe(null) 35 | 36 | await waitForNextUpdate() 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/hooks/useIsHealthDataAvailable.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | import Native from '../native-types' 4 | 5 | /** 6 | * @description By default, HealthKit data is available on iOS and watchOS. HealthKit data is also available on iPadOS 17 or later. However, devices running in an enterprise environment may restrict access to HealthKit data. 7 | * @see {@link https://developer.apple.com/documentation/healthkit/hkhealthstore/1614180-ishealthdataavailable Apple HealthKit isHealthDataAvailable} 8 | * @returns {boolean | null} true if HealthKit is available; otherwise, false. null while initializing. 9 | */ 10 | const useIsHealthDataAvailable = (): boolean | null => { 11 | const [isAvailable, setIsAvailable] = useState(null) 12 | 13 | useEffect(() => { 14 | const init = async () => { 15 | const res = await Native.isHealthDataAvailable() 16 | setIsAvailable(res) 17 | } 18 | void init() 19 | }, []) 20 | 21 | return isAvailable 22 | } 23 | 24 | export default useIsHealthDataAvailable 25 | -------------------------------------------------------------------------------- /src/hooks/useMostRecentCategorySample.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | import useSubscribeToChanges from './useSubscribeToChanges' 4 | import getMostRecentCategorySample from '../utils/getMostRecentCategorySample' 5 | 6 | import type { HKCategoryTypeIdentifier } from '../native-types' 7 | import type { HKCategorySample } from '../types' 8 | 9 | /** 10 | * @returns the most recent sample for the given category type. 11 | */ 12 | function useMostRecentCategorySample< 13 | TCategory extends HKCategoryTypeIdentifier 14 | >(identifier: TCategory) { 15 | const [category, setCategory] = useState | null>( 16 | null, 17 | ) 18 | const updater = useCallback(() => { 19 | void getMostRecentCategorySample(identifier).then(setCategory) 20 | }, [identifier]) 21 | 22 | useSubscribeToChanges(identifier, updater) 23 | 24 | return category 25 | } 26 | 27 | export default useMostRecentCategorySample 28 | -------------------------------------------------------------------------------- /src/hooks/useMostRecentQuantitySample.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | import ensureUnit from '../utils/ensureUnit' 4 | import getMostRecentQuantitySample from '../utils/getMostRecentQuantitySample' 5 | import subscribeToChanges from '../utils/subscribeToChanges' 6 | 7 | import type { HKQuantityTypeIdentifier, UnitForIdentifier } from '../native-types' 8 | import type { HKQuantitySample } from '../types' 9 | 10 | /** 11 | * @returns the most recent sample for the given quantity type. 12 | */ 13 | function useMostRecentQuantitySample< 14 | TIdentifier extends HKQuantityTypeIdentifier, 15 | TUnit extends UnitForIdentifier 16 | >(identifier: TIdentifier, unit?: TUnit) { 17 | const [lastSample, setLastSample] = useState | null>(null) 20 | 21 | useEffect(() => { 22 | let cancelSubscription: (() => Promise) | undefined 23 | 24 | const init = async () => { 25 | const actualUnit = await ensureUnit(identifier, unit) 26 | 27 | const value = await getMostRecentQuantitySample(identifier, actualUnit) 28 | setLastSample(value) 29 | 30 | cancelSubscription = await subscribeToChanges(identifier, async () => { 31 | const value = await getMostRecentQuantitySample(identifier, actualUnit) 32 | setLastSample(value) 33 | }) 34 | } 35 | void init() 36 | 37 | return () => { 38 | void cancelSubscription?.() 39 | } 40 | }, [identifier, unit]) 41 | 42 | return lastSample 43 | } 44 | 45 | export default useMostRecentQuantitySample 46 | -------------------------------------------------------------------------------- /src/hooks/useMostRecentWorkout.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useEffect, useState, useRef, useCallback, 3 | } from 'react' 4 | 5 | import getMostRecentWorkout from '../utils/getMostRecentWorkout' 6 | import getPreferredUnitsTyped from '../utils/getPreferredUnitsTyped' 7 | import subscribeToChanges from '../utils/subscribeToChanges' 8 | 9 | import type { EnergyUnit, LengthUnit } from '../native-types' 10 | import type { HKWorkout } from '../types' 11 | 12 | /** 13 | * @returns the most recent workout sample. 14 | */ 15 | function useMostRecentWorkout< 16 | TEnergy extends EnergyUnit, 17 | TDistance extends LengthUnit 18 | >(options?: { readonly energyUnit?: TEnergy; readonly distanceUnit?: TDistance }) { 19 | const [workout, setWorkout] = useState | null>(null) 20 | 21 | const optionsRef = useRef(options) 22 | 23 | useEffect(() => { 24 | optionsRef.current = options 25 | }, [options]) 26 | 27 | const update = useCallback(async () => { 28 | const { energyUnit, distanceUnit } = await getPreferredUnitsTyped( 29 | optionsRef.current, 30 | ) 31 | 32 | setWorkout(await getMostRecentWorkout({ 33 | energyUnit, 34 | distanceUnit, 35 | })) 36 | }, []) 37 | 38 | useEffect(() => { 39 | void update() 40 | }, [update]) 41 | 42 | useEffect(() => { 43 | let cancelSubscription: (() => Promise) | undefined 44 | 45 | const init = async () => { 46 | cancelSubscription = await subscribeToChanges( 47 | 'HKWorkoutTypeIdentifier', 48 | update, 49 | ) 50 | } 51 | void init() 52 | 53 | return () => { 54 | void cancelSubscription?.() 55 | } 56 | }, [update]) 57 | 58 | return workout 59 | } 60 | 61 | export default useMostRecentWorkout 62 | -------------------------------------------------------------------------------- /src/hooks/useSources.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react' 2 | 3 | import querySources from '../utils/querySources' 4 | 5 | import type { 6 | HKCategoryTypeIdentifier, 7 | HKQuantityTypeIdentifier, 8 | HKSource, 9 | } from '../native-types' 10 | 11 | function useSources< 12 | TIdentifier extends HKCategoryTypeIdentifier | HKQuantityTypeIdentifier 13 | >(identifier: TIdentifier) { 14 | const [result, setResult] = useState(null) 15 | 16 | const update = useCallback(async () => { 17 | const res = await querySources(identifier) 18 | setResult(res) 19 | }, [identifier]) 20 | 21 | useEffect(() => { 22 | void update() 23 | }, [update]) 24 | 25 | return result 26 | } 27 | 28 | export default useSources 29 | -------------------------------------------------------------------------------- /src/hooks/useStatisticsForQuantity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useState, useEffect, useCallback, useRef, 3 | } from 'react' 4 | 5 | import useSubscribeToChanges from './useSubscribeToChanges' 6 | import queryStatisticsForQuantity from '../utils/queryStatisticsForQuantity' 7 | 8 | import type { HKQuantityTypeIdentifier, HKStatisticsOptions, UnitForIdentifier } from '../native-types' 9 | import type { QueryStatisticsResponse } from '../types' 10 | 11 | function useStatisticsForQuantity = UnitForIdentifier>( 12 | identifier: TIdentifier, 13 | options: readonly HKStatisticsOptions[], 14 | from: Date, 15 | to?: Date, 16 | unit?: TUnit, 17 | ) { 18 | const [result, setResult] = useState | null>(null) 19 | 20 | const optionsRef = useRef(options) 21 | 22 | useEffect(() => { 23 | optionsRef.current = options 24 | }, [options]) 25 | 26 | const update = useCallback(async () => { 27 | const res = await queryStatisticsForQuantity(identifier, optionsRef.current, from, to, unit) 28 | setResult(res) 29 | }, [ 30 | identifier, from, to, unit, 31 | ]) 32 | 33 | useEffect(() => { 34 | void update() 35 | }, [update]) 36 | 37 | useSubscribeToChanges(identifier, update) 38 | 39 | return result 40 | } 41 | 42 | export default useStatisticsForQuantity 43 | -------------------------------------------------------------------------------- /src/hooks/useSubscribeToChanges.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | import subscribeToChanges from '../utils/subscribeToChanges' 4 | 5 | import type { HKSampleTypeIdentifier } from '..' 6 | 7 | function useSubscribeToChanges( 8 | identifier: TIdentifier, 9 | onChange: () => void, 10 | ): void { 11 | const onChangeRef = useRef(onChange) 12 | 13 | useEffect(() => { 14 | onChangeRef.current = onChange 15 | }, [onChange]) 16 | 17 | useEffect(() => { 18 | let cancelSubscription: (() => Promise) | undefined 19 | 20 | const init = async () => { 21 | cancelSubscription = await subscribeToChanges(identifier, onChangeRef.current) 22 | } 23 | void init() 24 | 25 | return () => { 26 | void cancelSubscription?.() 27 | } 28 | }, [identifier]) 29 | } 30 | 31 | export default useSubscribeToChanges 32 | -------------------------------------------------------------------------------- /src/index.native.tsx: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native' 2 | 3 | import { 4 | HKAuthorizationRequestStatus, HKAuthorizationStatus, HKBiologicalSex, HKBloodType, HKFitzpatrickSkinType, HKUnits, HKWheelchairUse, 5 | } from './native-types' 6 | 7 | import type ReactNativeHealthkit from './index.ios' 8 | import type { QueryCategorySamplesFn } from './utils/queryCategorySamples' 9 | import type { QueryQuantitySamplesFn } from './utils/queryQuantitySamples' 10 | 11 | const notAvailableError = `[@kingstinct/react-native-healthkit] Platform "${ 12 | Platform.OS 13 | }" not supported` 14 | 15 | let hasWarned = false 16 | 17 | function UnavailableFn(retVal: T) { 18 | return () => { 19 | if (!hasWarned) { 20 | // eslint-disable-next-line no-console 21 | console.warn(notAvailableError) 22 | hasWarned = true 23 | } 24 | return retVal 25 | } 26 | } 27 | 28 | const authorizationStatusFor = UnavailableFn(Promise.resolve(HKAuthorizationStatus.notDetermined)), 29 | availableQuantityTypes = UnavailableFn([]), 30 | disableAllBackgroundDelivery = UnavailableFn(Promise.resolve(false)), 31 | disableBackgroundDelivery = UnavailableFn(Promise.resolve(false)), 32 | enableBackgroundDelivery = UnavailableFn(Promise.resolve(false)), 33 | getBiologicalSex = UnavailableFn(Promise.resolve(HKBiologicalSex.notSet)), 34 | getBloodType = UnavailableFn(Promise.resolve(HKBloodType.notSet)), 35 | getDateOfBirth = UnavailableFn(Promise.resolve(new Date(0))), 36 | getFitzpatrickSkinType = UnavailableFn(Promise.resolve(HKFitzpatrickSkinType.notSet)), 37 | getMostRecentCategorySample = UnavailableFn(Promise.resolve(null)), 38 | getMostRecentQuantitySample = UnavailableFn(Promise.resolve(null)), 39 | getMostRecentWorkout = UnavailableFn(Promise.resolve(null)), 40 | getPreferredUnit = UnavailableFn(Promise.resolve(HKUnits.Count)), 41 | getPreferredUnits = UnavailableFn(Promise.resolve([])), 42 | getRequestStatusForAuthorization = UnavailableFn(Promise.resolve(HKAuthorizationRequestStatus.unknown)), 43 | getWheelchairUse = UnavailableFn(Promise.resolve(HKWheelchairUse.notSet)), 44 | getWorkoutRoutes = UnavailableFn(Promise.resolve([])), 45 | isHealthDataAvailable = async () => Promise.resolve(false), 46 | useSources = UnavailableFn(null), 47 | useStatisticsForQuantity = UnavailableFn(null), 48 | queryCategorySamples = UnavailableFn(Promise.resolve([])) as unknown as QueryCategorySamplesFn, 49 | queryCategorySamplesWithAnchor = UnavailableFn(Promise.resolve({ 50 | samples: [], 51 | deletedSamples: [], 52 | newAnchor: '', 53 | })), 54 | queryCorrelationSamples = UnavailableFn(Promise.resolve([])), 55 | queryHeartbeatSeriesSamples = UnavailableFn(Promise.resolve([])), 56 | queryHeartbeatSeriesSamplesWithAnchor = UnavailableFn(Promise.resolve({ 57 | samples: [], 58 | deletedSamples: [], 59 | newAnchor: '', 60 | })), 61 | queryQuantitySamples = UnavailableFn(Promise.resolve([])) as unknown as QueryQuantitySamplesFn, 62 | queryQuantitySamplesWithAnchor = UnavailableFn(Promise.resolve({ 63 | samples: [], 64 | deletedSamples: [], 65 | newAnchor: '', 66 | })), 67 | queryStatisticsForQuantity = UnavailableFn(Promise.resolve({ 68 | averageQuantity: undefined, 69 | maximumQuantity: undefined, 70 | minimumQuantity: undefined, 71 | sumQuantity: undefined, 72 | mostRecentQuantity: undefined, 73 | mostRecentQuantityDateInterval: undefined, 74 | duration: undefined, 75 | })), 76 | queryStatisticsCollectionForQuantity = UnavailableFn(Promise.resolve([ 77 | { 78 | averageQuantity: undefined, 79 | maximumQuantity: undefined, 80 | minimumQuantity: undefined, 81 | sumQuantity: undefined, 82 | mostRecentQuantity: undefined, 83 | mostRecentQuantityDateInterval: undefined, 84 | duration: undefined, 85 | }, 86 | ])), 87 | queryWorkouts = UnavailableFn(Promise.resolve([])), 88 | queryWorkoutSamples = UnavailableFn(Promise.resolve([])), 89 | queryWorkoutSamplesWithAnchor = UnavailableFn(Promise.resolve({ 90 | samples: [], 91 | deletedSamples: [], 92 | newAnchor: '', 93 | })), 94 | querySources = UnavailableFn(Promise.resolve([])), 95 | requestAuthorization = UnavailableFn(Promise.resolve(false)), 96 | deleteQuantitySample = UnavailableFn(Promise.resolve(false)), 97 | deleteSamples = UnavailableFn(Promise.resolve(false)), 98 | deleteWorkoutSample = UnavailableFn(Promise.resolve(false)), 99 | getWorkoutPlanById = UnavailableFn(Promise.resolve(null)), 100 | saveCategorySample = UnavailableFn(Promise.resolve(false)), 101 | saveStateOfMindSample = UnavailableFn(Promise.resolve(false)), 102 | saveCorrelationSample = UnavailableFn(Promise.resolve(false)), 103 | saveQuantitySample = UnavailableFn(Promise.resolve(false)), 104 | saveWorkoutSample = UnavailableFn(Promise.resolve(null)), 105 | saveWorkoutRoute = UnavailableFn(Promise.resolve(false)), 106 | subscribeToChanges = UnavailableFn(Promise.resolve(async () => Promise.resolve(false))), 107 | startWatchApp = UnavailableFn(async () => Promise.resolve(false)), 108 | workoutSessionMirroringStartHandler = UnavailableFn(Promise.resolve(false)), 109 | useMostRecentCategorySample = UnavailableFn(null), 110 | useMostRecentQuantitySample = UnavailableFn(null), 111 | useMostRecentWorkout = UnavailableFn(null), 112 | useSubscribeToChanges = UnavailableFn([null, () => null]), 113 | useHealthkitAuthorization = UnavailableFn([null, async () => Promise.resolve(HKAuthorizationRequestStatus.unknown)] as const), 114 | useIsHealthDataAvailable = () => false, 115 | isProtectedDataAvailable = async () => Promise.resolve(false), 116 | queryStateOfMindSamples = UnavailableFn(Promise.resolve([])) 117 | 118 | const Healthkit: typeof ReactNativeHealthkit = { 119 | authorizationStatusFor, 120 | availableQuantityTypes, 121 | deleteQuantitySample, 122 | deleteSamples, 123 | deleteWorkoutSample, 124 | disableAllBackgroundDelivery, 125 | disableBackgroundDelivery, 126 | enableBackgroundDelivery, 127 | getBiologicalSex, 128 | getBloodType, 129 | getDateOfBirth, 130 | getFitzpatrickSkinType, 131 | getMostRecentCategorySample, 132 | getMostRecentQuantitySample, 133 | getMostRecentWorkout, 134 | getPreferredUnit, 135 | getPreferredUnits, 136 | getRequestStatusForAuthorization, 137 | getWheelchairUse, 138 | getWorkoutPlanById, 139 | getWorkoutRoutes, 140 | isHealthDataAvailable, 141 | isProtectedDataAvailable, 142 | queryCategorySamples, 143 | queryCategorySamplesWithAnchor, 144 | queryCorrelationSamples, 145 | queryHeartbeatSeriesSamples, 146 | queryHeartbeatSeriesSamplesWithAnchor, 147 | queryQuantitySamples, 148 | queryQuantitySamplesWithAnchor, 149 | querySources, 150 | queryStatisticsForQuantity, 151 | queryStatisticsCollectionForQuantity, 152 | queryWorkouts, 153 | queryWorkoutSamples, 154 | queryWorkoutSamplesWithAnchor, 155 | requestAuthorization, 156 | saveStateOfMindSample, 157 | saveCategorySample, 158 | saveCorrelationSample, 159 | saveQuantitySample, 160 | saveWorkoutRoute, 161 | saveWorkoutSample, 162 | subscribeToChanges, 163 | startWatchApp, 164 | workoutSessionMirroringStartHandler, 165 | useHealthkitAuthorization, 166 | useIsHealthDataAvailable, 167 | useMostRecentCategorySample, 168 | useMostRecentQuantitySample, 169 | useMostRecentWorkout, 170 | useSources, 171 | useStatisticsForQuantity, 172 | useSubscribeToChanges, 173 | queryStateOfMindSamples, 174 | } 175 | 176 | export { 177 | authorizationStatusFor, 178 | availableQuantityTypes, 179 | deleteQuantitySample, 180 | deleteSamples, 181 | deleteWorkoutSample, 182 | disableAllBackgroundDelivery, 183 | disableBackgroundDelivery, 184 | enableBackgroundDelivery, 185 | getBiologicalSex, 186 | getBloodType, 187 | getDateOfBirth, 188 | getFitzpatrickSkinType, 189 | getMostRecentCategorySample, 190 | getMostRecentQuantitySample, 191 | getMostRecentWorkout, 192 | getPreferredUnit, 193 | getPreferredUnits, 194 | getRequestStatusForAuthorization, 195 | getWheelchairUse, 196 | getWorkoutPlanById, 197 | getWorkoutRoutes, 198 | isHealthDataAvailable, 199 | isProtectedDataAvailable, 200 | queryCategorySamples, 201 | queryCategorySamplesWithAnchor, 202 | queryCorrelationSamples, 203 | queryHeartbeatSeriesSamples, 204 | queryHeartbeatSeriesSamplesWithAnchor, 205 | queryQuantitySamples, 206 | queryQuantitySamplesWithAnchor, 207 | querySources, 208 | queryStatisticsForQuantity, 209 | queryStatisticsCollectionForQuantity, 210 | queryWorkouts, 211 | queryWorkoutSamples, 212 | queryWorkoutSamplesWithAnchor, 213 | requestAuthorization, 214 | saveCategorySample, 215 | saveStateOfMindSample, 216 | saveCorrelationSample, 217 | saveQuantitySample, 218 | saveWorkoutRoute, 219 | saveWorkoutSample, 220 | subscribeToChanges, 221 | startWatchApp, 222 | workoutSessionMirroringStartHandler, 223 | useHealthkitAuthorization, 224 | useIsHealthDataAvailable, 225 | useMostRecentCategorySample, 226 | useMostRecentQuantitySample, 227 | useMostRecentWorkout, 228 | useSources, 229 | useStatisticsForQuantity, 230 | useSubscribeToChanges, 231 | queryStateOfMindSamples, 232 | } 233 | 234 | export * from './types' 235 | 236 | export default Healthkit 237 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import Healthkit from './index.ios' 2 | 3 | export * from './index.ios' 4 | 5 | export default Healthkit 6 | -------------------------------------------------------------------------------- /src/index.web.tsx: -------------------------------------------------------------------------------- 1 | import Healthkit from './index.native' 2 | 3 | export * from './index.native' 4 | 5 | export default Healthkit 6 | -------------------------------------------------------------------------------- /src/test-setup.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | import { mock, jest, beforeAll } from 'bun:test' 3 | 4 | import type Native from './native-types' 5 | 6 | beforeAll(async () => { 7 | const mockModule: typeof Native = { 8 | queryWorkoutSamplesWithAnchor: jest.fn(), 9 | isHealthDataAvailable: jest.fn(), 10 | isProtectedDataAvailable: jest.fn(), 11 | authorizationStatusFor: jest.fn(), 12 | requestAuthorization: jest.fn(), 13 | saveQuantitySample: jest.fn(), 14 | deleteQuantitySample: jest.fn(), 15 | deleteSamples: jest.fn(), 16 | deleteWorkoutSample: jest.fn(), 17 | disableAllBackgroundDelivery: jest.fn(), 18 | disableBackgroundDelivery: jest.fn(), 19 | enableBackgroundDelivery: jest.fn(), 20 | queryCategorySamplesWithAnchor: jest.fn(), 21 | queryQuantitySamplesWithAnchor: jest.fn(), 22 | getBiologicalSex: jest.fn(), 23 | getBloodType: jest.fn(), 24 | getDateOfBirth: jest.fn(), 25 | getFitzpatrickSkinType: jest.fn(), 26 | getPreferredUnits: jest.fn(), 27 | getRequestStatusForAuthorization: jest.fn(), 28 | getWheelchairUse: jest.fn(), 29 | getWorkoutRoutes: jest.fn(), 30 | queryCategorySamples: jest.fn(), 31 | queryCorrelationSamples: jest.fn(), 32 | queryHeartbeatSeriesSamples: jest.fn(), 33 | queryHeartbeatSeriesSamplesWithAnchor: jest.fn(), 34 | queryQuantitySamples: jest.fn(), 35 | querySources: jest.fn(), 36 | queryStatisticsForQuantity: jest.fn(), 37 | queryStatisticsCollectionForQuantity: jest.fn(), 38 | queryWorkoutSamples: jest.fn(), 39 | saveStateOfMindSample: jest.fn(), 40 | saveCategorySample: jest.fn(), 41 | saveCorrelationSample: jest.fn(), 42 | saveWorkoutSample: jest.fn(), 43 | subscribeToObserverQuery: jest.fn(), 44 | unsubscribeQuery: jest.fn(), 45 | saveWorkoutRoute: jest.fn(), 46 | getWorkoutPlanById: jest.fn(), 47 | startWatchAppWithWorkoutConfiguration: jest.fn(), 48 | queryStateOfMindSamples: jest.fn(), 49 | workoutSessionMirroringStartHandler: jest.fn(), 50 | } 51 | 52 | await mock.module('react-native', () => ({ 53 | NativeModules: { 54 | ReactNativeHealthkit: mockModule, 55 | }, 56 | NativeEventEmitter: jest.fn(), 57 | })) 58 | }) 59 | -------------------------------------------------------------------------------- /src/test-utils.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import { act } from '@testing-library/react-native' 3 | 4 | const waitForNextUpdate = async () => { 5 | await act(async () => { 6 | await new Promise((resolve) => { setTimeout(resolve, 0) }) 7 | }) 8 | } 9 | 10 | export default waitForNextUpdate 11 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CLLocationRawForSaving, 3 | EnergyUnit, 4 | HKCategorySampleRaw, 5 | HKCategoryTypeIdentifier, 6 | HKCorrelationRaw, 7 | HKCorrelationTypeIdentifier, 8 | HKDevice, 9 | HKHeartbeatSeriesSampleRaw, 10 | HKQuantityTypeIdentifier, 11 | HKSourceRevision, 12 | HKUnit, 13 | HKWorkoutRaw, 14 | LengthUnit, 15 | MetadataMapperForQuantityIdentifier, 16 | QueryStatisticsResponseRaw, 17 | UnitForIdentifier, 18 | } from './native-types' 19 | 20 | export * from './native-types' 21 | 22 | /** 23 | * Options for querying workouts. 24 | * @template TEnergy The energy unit type. 25 | * @template TDistance The distance unit type. 26 | * @see {@link https://developer.apple.com/documentation/healthkit/hkworkout Apple Docs HKWorkout} 27 | */ 28 | export interface QueryWorkoutsOptions< 29 | TEnergy extends HKUnit, 30 | TDistance extends HKUnit 31 | > extends GenericQueryOptions { 32 | readonly energyUnit?: TEnergy; 33 | readonly distanceUnit?: TDistance; 34 | } 35 | 36 | /** 37 | * Represents a category sample. 38 | * @template T The category type identifier. 39 | * @see {@link https://developer.apple.com/documentation/healthkit/hkcategorysample Apple Docs HKCategorySample} 40 | */ 41 | export interface HKCategorySample< 42 | T extends HKCategoryTypeIdentifier = HKCategoryTypeIdentifier 43 | > extends Omit, 'endDate' | 'startDate'> { 44 | readonly startDate: Date; 45 | readonly endDate: Date; 46 | } 47 | 48 | /** 49 | * Generic options for querying. 50 | */ 51 | export type GenericQueryOptions = { 52 | readonly from?: Date; 53 | readonly to?: Date; 54 | readonly limit?: number; 55 | readonly ascending?: boolean; 56 | readonly anchor?: string 57 | }; 58 | 59 | /** 60 | * Represents a workout. 61 | * @template TEnergy The energy unit type. 62 | * @template TDistance The distance unit type. 63 | * @see {@link https://developer.apple.com/documentation/healthkit/hkworkout Apple Docs HKWorkout} 64 | */ 65 | export interface HKWorkout< 66 | TEnergy extends EnergyUnit = EnergyUnit, 67 | TDistance extends LengthUnit = LengthUnit 68 | > extends Omit, 'endDate' | 'startDate'> { 69 | readonly startDate: Date; 70 | readonly endDate: Date; 71 | } 72 | 73 | /** 74 | * Represents a heartbeat series sample. 75 | * @see {@link https://developer.apple.com/documentation/healthkit/hkheartbeatseriessample Apple Docs HKHeartbeatSeriesSample} 76 | */ 77 | export interface HKHeartbeatSeriesSample extends Omit { 78 | readonly startDate: Date; 79 | readonly endDate: Date; 80 | } 81 | 82 | /** 83 | * Represents a quantity sample. 84 | * @template TIdentifier The quantity type identifier. 85 | * @template TUnit The unit for the identifier. 86 | * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitysample Apple Docs HKQuantitySample} 87 | */ 88 | export interface HKQuantitySample< 89 | TIdentifier extends HKQuantityTypeIdentifier = HKQuantityTypeIdentifier, 90 | TUnit extends UnitForIdentifier = UnitForIdentifier 91 | > { 92 | readonly uuid: string; 93 | readonly device?: HKDevice; 94 | readonly quantityType: TIdentifier; 95 | readonly quantity: number; 96 | readonly unit: TUnit; 97 | readonly metadata?: MetadataMapperForQuantityIdentifier; 98 | readonly sourceRevision?: HKSourceRevision; 99 | readonly startDate: Date; 100 | readonly endDate: Date; 101 | } 102 | 103 | /** 104 | * Represents a response from a statistics query. 105 | * @template TIdentifier The quantity type identifier. 106 | * @template TUnit The unit for the identifier. 107 | * @see {@link https://developer.apple.com/documentation/healthkit/hkstatisticsquery Apple Docs HKStatisticsQuery} 108 | */ 109 | export interface QueryStatisticsResponse = UnitForIdentifier> 110 | extends Omit< 111 | QueryStatisticsResponseRaw, 112 | 'mostRecentQuantityDateInterval' 113 | > { 114 | readonly mostRecentQuantityDateInterval?: { readonly from: Date; readonly to: Date }; 115 | } 116 | 117 | /** 118 | * Represents a category sample for saving. 119 | * @see {@link https://developer.apple.com/documentation/healthkit/hkcategorysample Apple Docs HKCategorySample} 120 | */ 121 | export type HKCategorySampleForSaving = Omit & { 122 | readonly startDate?: Date; 123 | readonly endDate?: Date; 124 | } 125 | 126 | /** 127 | * Represents a quantity sample for saving. 128 | * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitysample Apple Docs HKQuantitySample} 129 | */ 130 | export type HKQuantitySampleForSaving = Omit & { 131 | readonly startDate?: Date; 132 | readonly endDate?: Date; 133 | }; 134 | 135 | /** 136 | * Represents a correlation. 137 | * @template TIdentifier The correlation type identifier. 138 | * @see {@link https://developer.apple.com/documentation/healthkit/hkcorrelation Apple Docs HKCorrelation} 139 | */ 140 | export interface HKCorrelation 141 | extends Omit< 142 | HKCorrelationRaw, 143 | 'endDate' | 'objects' | 'startDate' 144 | > { 145 | readonly objects: readonly (HKCategorySample | HKQuantitySample)[]; 146 | readonly startDate: Date; 147 | readonly endDate: Date; 148 | } 149 | 150 | /** 151 | * Represents a location sample for saving. 152 | * @see {@link https://developer.apple.com/documentation/corelocation/cllocation Apple Docs CLLocation} 153 | */ 154 | export type CLLocationForSaving = Omit & { 155 | readonly timestamp: number; 156 | }; 157 | -------------------------------------------------------------------------------- /src/utils/deleteQuantitySample.ts: -------------------------------------------------------------------------------- 1 | import Native from '../native-types' 2 | 3 | import type { HKQuantityTypeIdentifier } from '../native-types' 4 | 5 | export type DeleteQuantitySampleFn = < 6 | TIdentifier extends HKQuantityTypeIdentifier 7 | >( 8 | identifier: TIdentifier, 9 | uuid: string 10 | ) => Promise 11 | 12 | const deleteQuantitySample: DeleteQuantitySampleFn = async (identifier, uuid) => Native.deleteQuantitySample(identifier, uuid) 13 | 14 | export default deleteQuantitySample 15 | -------------------------------------------------------------------------------- /src/utils/deleteSamples.ts: -------------------------------------------------------------------------------- 1 | import Native from '../native-types' 2 | 3 | import type { HKQuantityTypeIdentifier } from '../native-types' 4 | 5 | export type DeleteSamplesFn = < 6 | TIdentifier extends HKQuantityTypeIdentifier 7 | >( 8 | sample: { 9 | readonly identifier: TIdentifier, 10 | readonly startDate?: Date; 11 | readonly endDate?: Date; 12 | } 13 | ) => Promise 14 | 15 | const deleteSamples: DeleteSamplesFn = async (sample) => { 16 | const start = sample.startDate || new Date() 17 | const end = sample.endDate || new Date() 18 | const { identifier } = sample 19 | 20 | return Native.deleteSamples(identifier, start.toISOString(), end.toISOString()) 21 | } 22 | 23 | export default deleteSamples 24 | -------------------------------------------------------------------------------- /src/utils/deleteWorkoutSample.ts: -------------------------------------------------------------------------------- 1 | import Native from '../native-types' 2 | 3 | export type DeleteWorkoutSampleFn = (uuid: string) => Promise 4 | 5 | const deleteWorkoutSample: DeleteWorkoutSampleFn = async (uuid) => Native.deleteWorkoutSample(uuid) 6 | 7 | export default deleteWorkoutSample 8 | -------------------------------------------------------------------------------- /src/utils/deserializeCategorySample.test.ts: -------------------------------------------------------------------------------- 1 | import deserializeCategorySample from './deserializeCategorySample' 2 | 3 | import type { HKCategorySampleRaw } from '../types' 4 | 5 | describe('deserializeCategorySample', () => { 6 | it('should deserialize category sample', async () => { 7 | const { HKCategoryTypeIdentifier } = await import('../native-types') 8 | 9 | const sample: HKCategorySampleRaw = { 10 | startDate: '2020-01-01T00:00:00.000Z', 11 | endDate: '2020-01-01T00:00:00.000Z', 12 | value: 1, 13 | categoryType: HKCategoryTypeIdentifier.sexualActivity, 14 | metadata: {}, 15 | uuid: 'uuid', 16 | } 17 | 18 | expect(deserializeCategorySample(sample)).toEqual({ 19 | ...sample, 20 | startDate: new Date('2020-01-01T00:00:00.000Z'), 21 | endDate: new Date('2020-01-01T00:00:00.000Z'), 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/utils/deserializeCategorySample.ts: -------------------------------------------------------------------------------- 1 | import type { HKCategorySampleRaw, HKCategoryTypeIdentifier } from '../native-types' 2 | import type { HKCategorySample } from '../types' 3 | 4 | const deserializeCategorySample = ( 5 | sample: HKCategorySampleRaw, 6 | ): HKCategorySample => ({ 7 | ...sample, 8 | startDate: new Date(sample.startDate), 9 | endDate: new Date(sample.endDate), 10 | }) 11 | 12 | export default deserializeCategorySample 13 | -------------------------------------------------------------------------------- /src/utils/deserializeCorrelation.ts: -------------------------------------------------------------------------------- 1 | import deserializCategorySample from './deserializeCategorySample' 2 | import deserializeQuantitySample from './deserializeSample' 3 | 4 | import type { 5 | HKCategorySampleRaw, HKCorrelationRaw, HKCorrelationTypeIdentifier, HKQuantitySampleRaw, HKQuantityTypeIdentifier, 6 | } from '../native-types' 7 | import type { HKCorrelation } from '../types' 8 | 9 | function deserializeCorrelation< 10 | TIdentifier extends HKCorrelationTypeIdentifier 11 | >(s: HKCorrelationRaw): HKCorrelation { 12 | return { 13 | ...s, 14 | objects: s.objects.map((o) => { 15 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 16 | // @ts-ignore 17 | if (o.quantity !== undefined) { 18 | return deserializeQuantitySample(o as HKQuantitySampleRaw) 19 | } 20 | 21 | return deserializCategorySample(o as HKCategorySampleRaw) 22 | }), 23 | endDate: new Date(s.endDate), 24 | startDate: new Date(s.startDate), 25 | } 26 | } 27 | 28 | export default deserializeCorrelation 29 | -------------------------------------------------------------------------------- /src/utils/deserializeHeartbeatSeriesSample.ts: -------------------------------------------------------------------------------- 1 | import type { HKHeartbeatSeriesSampleRaw } from '../native-types' 2 | import type { HKHeartbeatSeriesSample } from '../types' 3 | 4 | function deserializeHeartbeatSeriesSample(sample: HKHeartbeatSeriesSampleRaw): HKHeartbeatSeriesSample { 5 | return { 6 | ...sample, 7 | startDate: new Date(sample.startDate), 8 | endDate: new Date(sample.endDate), 9 | } 10 | } 11 | 12 | export default deserializeHeartbeatSeriesSample 13 | -------------------------------------------------------------------------------- /src/utils/deserializeSample.ts: -------------------------------------------------------------------------------- 1 | import type { HKQuantitySampleRaw, HKQuantityTypeIdentifier, UnitForIdentifier } from '../native-types' 2 | import type { HKQuantitySample } from '../types' 3 | 4 | function deserializeQuantitySample< 5 | TIdentifier extends HKQuantityTypeIdentifier, 6 | TUnit extends UnitForIdentifier 7 | >( 8 | sample: HKQuantitySampleRaw, 9 | ): HKQuantitySample { 10 | return { 11 | ...sample, 12 | startDate: new Date(sample.startDate), 13 | endDate: new Date(sample.endDate), 14 | } 15 | } 16 | 17 | export default deserializeQuantitySample 18 | -------------------------------------------------------------------------------- /src/utils/deserializeWorkout.ts: -------------------------------------------------------------------------------- 1 | import type { EnergyUnit, HKWorkoutRaw, LengthUnit } from '../native-types' 2 | import type { HKWorkout } from '../types' 3 | 4 | function deserializeWorkout( 5 | sample: HKWorkoutRaw, 6 | ): HKWorkout { 7 | return { 8 | ...sample, 9 | startDate: new Date(sample.startDate), 10 | endDate: new Date(sample.endDate), 11 | } 12 | } 13 | 14 | export default deserializeWorkout 15 | -------------------------------------------------------------------------------- /src/utils/ensureMetadata.ts: -------------------------------------------------------------------------------- 1 | function ensureMetadata(metadata?: TMetadata) { 2 | return metadata || ({} as TMetadata) 3 | } 4 | 5 | export default ensureMetadata 6 | -------------------------------------------------------------------------------- /src/utils/ensureTotals.ts: -------------------------------------------------------------------------------- 1 | function ensureTotals(totals?: TTotals) { 2 | return totals || ({} as TTotals) 3 | } 4 | 5 | export default ensureTotals 6 | -------------------------------------------------------------------------------- /src/utils/ensureUnit.ts: -------------------------------------------------------------------------------- 1 | import Native from '../native-types' 2 | 3 | import type { HKQuantityTypeIdentifier, UnitForIdentifier } from '../native-types' 4 | 5 | const ensureUnit = async < 6 | TIdentifier extends HKQuantityTypeIdentifier, 7 | TUnit extends UnitForIdentifier 8 | >( 9 | type: TIdentifier, 10 | providedUnit?: TUnit, 11 | ) => { 12 | if (providedUnit) { 13 | return providedUnit 14 | } 15 | const unit = await Native.getPreferredUnits([type]) 16 | return unit[type] as TUnit 17 | } 18 | 19 | export default ensureUnit 20 | -------------------------------------------------------------------------------- /src/utils/getDateOfBirth.ts: -------------------------------------------------------------------------------- 1 | import Native from '../native-types' 2 | 3 | const getDateOfBirth = async () => { 4 | const dateOfBirth = await Native.getDateOfBirth() 5 | return new Date(dateOfBirth) 6 | } 7 | 8 | export default getDateOfBirth 9 | -------------------------------------------------------------------------------- /src/utils/getMostRecentCategorySample.ts: -------------------------------------------------------------------------------- 1 | import queryCategorySamples from './queryCategorySamples' 2 | 3 | import type { HKCategoryTypeIdentifier } from '../native-types' 4 | import type { HKCategorySample } from '../types' 5 | 6 | async function getMostRecentCategorySample< 7 | T extends HKCategoryTypeIdentifier 8 | >( 9 | identifier: T, 10 | ): Promise | null> { 11 | const samples = await queryCategorySamples(identifier, { 12 | limit: 1, 13 | ascending: false, 14 | }) 15 | 16 | return samples[0] ?? null 17 | } 18 | 19 | export default getMostRecentCategorySample 20 | -------------------------------------------------------------------------------- /src/utils/getMostRecentQuantitySample.ts: -------------------------------------------------------------------------------- 1 | import queryQuantitySamples from './queryQuantitySamples' 2 | 3 | import type { HKQuantityTypeIdentifier, UnitForIdentifier } from '../native-types' 4 | import type { HKQuantitySample } from '../types' 5 | 6 | async function getMostRecentQuantitySample< 7 | TIdentifier extends HKQuantityTypeIdentifier, 8 | TUnit extends UnitForIdentifier 9 | >( 10 | identifier: TIdentifier, 11 | unit: TUnit, 12 | ): Promise | null> { 13 | const samples = await queryQuantitySamples(identifier, { 14 | limit: 1, 15 | unit, 16 | }) 17 | 18 | const lastSample = samples[0] 19 | 20 | if (lastSample) { 21 | return lastSample as HKQuantitySample 22 | } 23 | return null 24 | } 25 | 26 | export default getMostRecentQuantitySample 27 | -------------------------------------------------------------------------------- /src/utils/getMostRecentWorkout.ts: -------------------------------------------------------------------------------- 1 | import queryWorkouts from './queryWorkouts' 2 | 3 | import type { EnergyUnit, LengthUnit } from '../native-types' 4 | import type { HKWorkout, QueryWorkoutsOptions } from '../types' 5 | 6 | export type GetMostRecentWorkoutFn = < 7 | TEnergy extends EnergyUnit, 8 | TDistance extends LengthUnit 9 | >( 10 | options?: Pick< 11 | QueryWorkoutsOptions, 12 | 'distanceUnit' | 'energyUnit' 13 | > 14 | ) => Promise | null>; 15 | 16 | const getMostRecentWorkout: GetMostRecentWorkoutFn = async (options) => { 17 | const workouts = await queryWorkouts({ 18 | limit: 1, 19 | ascending: false, 20 | energyUnit: options?.energyUnit, 21 | distanceUnit: options?.distanceUnit, 22 | }) 23 | 24 | return workouts[0] || null 25 | } 26 | 27 | export default getMostRecentWorkout 28 | -------------------------------------------------------------------------------- /src/utils/getPreferredUnit.ts: -------------------------------------------------------------------------------- 1 | import getPreferredUnits from './getPreferredUnits' 2 | 3 | import type { HKQuantityTypeIdentifier, HKUnit } from '../native-types' 4 | 5 | export type GetPreferredUnitFn = ( 6 | identifier: HKQuantityTypeIdentifier 7 | ) => Promise; 8 | 9 | const getPreferredUnit: GetPreferredUnitFn = async (type) => { 10 | const [unit] = await getPreferredUnits([type]) 11 | return unit 12 | } 13 | 14 | export default getPreferredUnit 15 | -------------------------------------------------------------------------------- /src/utils/getPreferredUnits.ts: -------------------------------------------------------------------------------- 1 | import Native from '../native-types' 2 | 3 | import type { HKQuantityTypeIdentifier, HKUnit } from '../native-types' 4 | 5 | export type GetPreferredUnitsFn = ( 6 | identifiers: readonly HKQuantityTypeIdentifier[] 7 | ) => Promise; 8 | 9 | const getPreferredUnits: GetPreferredUnitsFn = async (identifiers) => { 10 | const units = await Native.getPreferredUnits(identifiers) 11 | return identifiers.map((i) => units[i]) 12 | } 13 | 14 | export default getPreferredUnits 15 | -------------------------------------------------------------------------------- /src/utils/getPreferredUnitsTyped.ts: -------------------------------------------------------------------------------- 1 | import Native, { HKQuantityTypeIdentifier, UnitOfEnergy, UnitOfLength } from '../native-types' 2 | 3 | import type { HKUnit } from '../native-types' 4 | 5 | async function getPreferredUnitsTyped< 6 | TEnergy extends HKUnit, 7 | TDistance extends HKUnit 8 | >(options?: { readonly energyUnit?: TEnergy; readonly distanceUnit?: TDistance }) { 9 | let energyUnit = options?.energyUnit 10 | let distanceUnit = options?.distanceUnit 11 | 12 | if (!energyUnit || !distanceUnit) { 13 | const units = await Native.getPreferredUnits([ 14 | HKQuantityTypeIdentifier.distanceWalkingRunning, 15 | HKQuantityTypeIdentifier.activeEnergyBurned, 16 | ]) 17 | if (!energyUnit) { 18 | energyUnit = units[HKQuantityTypeIdentifier.activeEnergyBurned] as 19 | | TEnergy 20 | | undefined 21 | } 22 | if (!distanceUnit) { 23 | distanceUnit = units[HKQuantityTypeIdentifier.distanceWalkingRunning] as 24 | | TDistance 25 | | undefined 26 | } 27 | } 28 | 29 | if (!energyUnit) { 30 | energyUnit = UnitOfEnergy.Kilocalories as TEnergy 31 | } 32 | if (!distanceUnit) { 33 | distanceUnit = UnitOfLength.Meter as TDistance 34 | } 35 | return { energyUnit, distanceUnit } 36 | } 37 | 38 | export default getPreferredUnitsTyped 39 | -------------------------------------------------------------------------------- /src/utils/getRequestStatusForAuthorization.ts: -------------------------------------------------------------------------------- 1 | import Native from '../native-types' 2 | 3 | import type { 4 | HealthkitReadAuthorization, HealthkitWriteAuthorization, ReadPermissions, WritePermissions, 5 | } from '../native-types' 6 | 7 | const getRequestStatusForAuthorization = async ( 8 | read: readonly HealthkitReadAuthorization[], 9 | write: readonly HealthkitWriteAuthorization[] = [], 10 | ) => { 11 | const readPermissions = read.reduce((obj, cur) => ({ ...obj, [cur]: true }), {} as ReadPermissions) 12 | 13 | const writePermissions = write.reduce((obj, cur) => ({ ...obj, [cur]: true }), {} as WritePermissions) 14 | 15 | return Native.getRequestStatusForAuthorization( 16 | writePermissions, 17 | readPermissions, 18 | ) 19 | } 20 | 21 | export default getRequestStatusForAuthorization 22 | -------------------------------------------------------------------------------- /src/utils/getWorkoutPlanById.ts: -------------------------------------------------------------------------------- 1 | import Native from '../native-types' 2 | 3 | async function getWorkoutPlanById(workoutUUID: string) { 4 | return Native.getWorkoutPlanById(workoutUUID) 5 | } 6 | 7 | export default getWorkoutPlanById 8 | -------------------------------------------------------------------------------- /src/utils/prepareOptions.ts: -------------------------------------------------------------------------------- 1 | import serializeDate from './serializeDate' 2 | 3 | import type { GenericQueryOptions } from '../types' 4 | 5 | const prepareOptions = (options: GenericQueryOptions) => { 6 | const limit = !options.limit || options.limit === Infinity 7 | ? 0 8 | : options.limit 9 | const ascending = options.ascending ?? limit === 0 10 | // eslint-disable-next-line no-nested-ternary 11 | const from = serializeDate(options.from ? options.from : (limit > 0 ? new Date(0) : undefined)) 12 | const to = serializeDate(options.to) 13 | const anchor = options.anchor ?? '' 14 | return { 15 | limit, ascending, from, to, anchor, 16 | } 17 | } 18 | 19 | export default prepareOptions 20 | -------------------------------------------------------------------------------- /src/utils/queryCategorySamples.ts: -------------------------------------------------------------------------------- 1 | import deserializeCategorySample from './deserializeCategorySample' 2 | import prepareOptions from './prepareOptions' 3 | import Native from '../native-types' 4 | 5 | import type { HKCategoryTypeIdentifier } from '../native-types' 6 | import type { GenericQueryOptions, HKCategorySample } from '../types' 7 | 8 | export type QueryCategorySamplesFn = ( 9 | identifier: T, 10 | options: Omit 11 | ) => Promise[]>; 12 | 13 | const queryCategorySamples: QueryCategorySamplesFn = async ( 14 | identifier, 15 | options, 16 | ) => { 17 | const opts = prepareOptions(options) 18 | const raw = await Native.queryCategorySamples( 19 | identifier, 20 | opts.from, 21 | opts.to, 22 | opts.limit, 23 | opts.ascending, 24 | ) 25 | 26 | return raw.map(deserializeCategorySample) 27 | } 28 | 29 | export default queryCategorySamples 30 | -------------------------------------------------------------------------------- /src/utils/queryCategorySamplesWithAnchor.ts: -------------------------------------------------------------------------------- 1 | import deserializeCategorySample from './deserializeCategorySample' 2 | import prepareOptions from './prepareOptions' 3 | import Native from '../native-types' 4 | 5 | import type { HKCategoryTypeIdentifier, DeletedCategorySampleRaw } from '../native-types' 6 | import type { GenericQueryOptions, HKCategorySample } from '../types' 7 | 8 | export type QueryCategorySamplesWithAnchorResponse = { 9 | readonly samples: readonly HKCategorySample[], 10 | readonly deletedSamples: readonly DeletedCategorySampleRaw[], 11 | readonly newAnchor: string 12 | } 13 | 14 | export type QueryCategorySamplesWithAnchorFn = ( 15 | identifier: T, 16 | options: Omit 17 | ) => Promise>; 18 | 19 | const queryCategorySamplesWithAnchor: QueryCategorySamplesWithAnchorFn = async ( 20 | identifier, 21 | options, 22 | ) => { 23 | const opts = prepareOptions(options) 24 | const raw = await Native.queryCategorySamplesWithAnchor( 25 | identifier, 26 | opts.from, 27 | opts.to, 28 | opts.limit, 29 | opts.anchor, 30 | ) 31 | 32 | return { 33 | samples: raw.samples.map(deserializeCategorySample), 34 | deletedSamples: raw.deletedSamples, 35 | newAnchor: raw.newAnchor, 36 | } 37 | } 38 | 39 | export default queryCategorySamplesWithAnchor 40 | -------------------------------------------------------------------------------- /src/utils/queryCorrelationSamples.ts: -------------------------------------------------------------------------------- 1 | import deserializeCorrelation from './deserializeCorrelation' 2 | import prepareOptions from './prepareOptions' 3 | import Native from '../native-types' 4 | 5 | import type { HKCorrelationTypeIdentifier } from '../native-types' 6 | import type { GenericQueryOptions, HKCorrelation } from '../types' 7 | 8 | export type QueryCorrelationSamplesFn = < 9 | TIdentifier extends HKCorrelationTypeIdentifier 10 | >( 11 | typeIdentifier: TIdentifier, 12 | options: Omit 13 | ) => Promise[]>; 14 | 15 | const queryCorrelationSamples: QueryCorrelationSamplesFn = async ( 16 | typeIdentifier, 17 | options, 18 | ) => { 19 | const opts = prepareOptions(options) 20 | const correlations = await Native.queryCorrelationSamples( 21 | typeIdentifier, 22 | opts.from, 23 | opts.to, 24 | ) 25 | 26 | return correlations.map(deserializeCorrelation) 27 | } 28 | 29 | export default queryCorrelationSamples 30 | -------------------------------------------------------------------------------- /src/utils/queryHeartbeatSeriesSamples.ts: -------------------------------------------------------------------------------- 1 | import deserializeHeartbeatSeriesSample from './deserializeHeartbeatSeriesSample' 2 | import prepareOptions from './prepareOptions' 3 | import Native from '../native-types' 4 | 5 | import type { DeletedHeartbeatSeriesSampleRaw } from '../native-types' 6 | import type { GenericQueryOptions, HKHeartbeatSeriesSample } from '../types' 7 | 8 | export type QueryHeartbeatSeriesSamplesResponse = { 9 | readonly samples: readonly HKHeartbeatSeriesSample[], 10 | readonly deletedSamples: readonly DeletedHeartbeatSeriesSampleRaw[], 11 | readonly newAnchor: string 12 | } 13 | 14 | export type QueryHeartbeatSeriesSamplesFn = (options: Omit) => Promise; 15 | 16 | const queryHeartbeatSeriesSamples: QueryHeartbeatSeriesSamplesFn = async (options) => { 17 | const opts = prepareOptions(options) 18 | 19 | const result = await Native.queryHeartbeatSeriesSamples( 20 | opts.from, 21 | opts.to, 22 | opts.limit, 23 | opts.ascending, 24 | ) 25 | 26 | return result.map(deserializeHeartbeatSeriesSample) 27 | } 28 | 29 | export default queryHeartbeatSeriesSamples 30 | -------------------------------------------------------------------------------- /src/utils/queryHeartbeatSeriesSamplesWithAnchor.ts: -------------------------------------------------------------------------------- 1 | import deserializeHeartbeatSeriesSample from './deserializeHeartbeatSeriesSample' 2 | import prepareOptions from './prepareOptions' 3 | import Native from '../native-types' 4 | 5 | import type { DeletedHeartbeatSeriesSampleRaw } from '../native-types' 6 | import type { GenericQueryOptions, HKHeartbeatSeriesSample } from '../types' 7 | 8 | export type QueryHeartbeatSeriesSamplesResponse = { 9 | readonly samples: readonly HKHeartbeatSeriesSample[], 10 | readonly deletedSamples: readonly DeletedHeartbeatSeriesSampleRaw[], 11 | readonly newAnchor: string 12 | } 13 | 14 | export type QueryHeartbeatSeriesSamplesFn = (options: Omit) => Promise; 15 | 16 | const queryHeartbeatSeriesSamplesWithAnchor: QueryHeartbeatSeriesSamplesFn = async (options) => { 17 | const opts = prepareOptions(options) 18 | 19 | const result = await Native.queryHeartbeatSeriesSamplesWithAnchor( 20 | opts.from, 21 | opts.to, 22 | opts.limit, 23 | opts.anchor, 24 | ) 25 | 26 | return { 27 | deletedSamples: result.deletedSamples, 28 | newAnchor: result.newAnchor, 29 | samples: result.samples.map(deserializeHeartbeatSeriesSample), 30 | } 31 | } 32 | 33 | export default queryHeartbeatSeriesSamplesWithAnchor 34 | -------------------------------------------------------------------------------- /src/utils/queryQuantitySamples.ts: -------------------------------------------------------------------------------- 1 | import deserializeQuantitySample from './deserializeSample' 2 | import ensureUnit from './ensureUnit' 3 | import prepareOptions from './prepareOptions' 4 | import Native from '../native-types' 5 | 6 | import type { 7 | HKQuantityTypeIdentifier, UnitForIdentifier, 8 | } from '../native-types' 9 | import type { GenericQueryOptions, HKQuantitySample } from '../types' 10 | 11 | export type QueryQuantitySamplesFn = < 12 | TIdentifier extends HKQuantityTypeIdentifier, 13 | TUnit extends UnitForIdentifier 14 | >( 15 | identifier: TIdentifier, 16 | options: Omit & { readonly unit?: TUnit } 17 | ) => Promise[]>; 18 | 19 | const queryQuantitySamples: QueryQuantitySamplesFn = async ( 20 | identifier, 21 | options, 22 | ) => { 23 | const unit = await ensureUnit(identifier, options.unit) 24 | const opts = prepareOptions(options) 25 | 26 | const result = await Native.queryQuantitySamples( 27 | identifier, 28 | unit, 29 | opts.from, 30 | opts.to, 31 | opts.limit, 32 | opts.ascending, 33 | ) 34 | 35 | return result.map(deserializeQuantitySample) 36 | } 37 | 38 | export default queryQuantitySamples 39 | -------------------------------------------------------------------------------- /src/utils/queryQuantitySamplesWithAnchor.ts: -------------------------------------------------------------------------------- 1 | import deserializeQuantitySample from './deserializeSample' 2 | import ensureUnit from './ensureUnit' 3 | import prepareOptions from './prepareOptions' 4 | import Native from '../native-types' 5 | 6 | import type { HKQuantityTypeIdentifier, UnitForIdentifier, DeletedQuantitySampleRaw } from '../native-types' 7 | import type { GenericQueryOptions, HKQuantitySample } from '../types' 8 | 9 | export type QueryQuantitySamplesWithAnchorResponse = { 10 | readonly samples: readonly HKQuantitySample[], 11 | readonly deletedSamples: readonly DeletedQuantitySampleRaw[], 12 | readonly newAnchor: string 13 | } 14 | 15 | export type QueryQuantitySamplesWithAnchorFn = < 16 | TIdentifier extends HKQuantityTypeIdentifier, 17 | TUnit extends UnitForIdentifier 18 | >( 19 | identifier: TIdentifier, 20 | options: Omit & { readonly unit?: TUnit } 21 | ) => Promise>; 22 | 23 | const queryQuantitySamplesWithAnchor: QueryQuantitySamplesWithAnchorFn = async ( 24 | identifier, 25 | options, 26 | ) => { 27 | const unit = await ensureUnit(identifier, options.unit) 28 | const opts = prepareOptions(options) 29 | 30 | const result = await Native.queryQuantitySamplesWithAnchor( 31 | identifier, 32 | unit, 33 | opts.from, 34 | opts.to, 35 | opts.limit, 36 | opts.anchor, 37 | ) 38 | 39 | return { 40 | deletedSamples: result.deletedSamples, 41 | newAnchor: result.newAnchor, 42 | samples: result.samples.map(deserializeQuantitySample), 43 | } 44 | } 45 | 46 | export default queryQuantitySamplesWithAnchor 47 | -------------------------------------------------------------------------------- /src/utils/querySources.ts: -------------------------------------------------------------------------------- 1 | import Native from '../native-types' 2 | 3 | import type { 4 | HKQuantityTypeIdentifier, 5 | HKSource, 6 | HKCategoryTypeIdentifier, 7 | } from '../native-types' 8 | 9 | export type QuerySourcesFn = < 10 | TIdentifier extends HKCategoryTypeIdentifier | HKQuantityTypeIdentifier 11 | >( 12 | identifier: TIdentifier 13 | ) => Promise; 14 | 15 | const querySources: QuerySourcesFn = async (identifier) => { 16 | const quantitySamples = await Native.querySources(identifier) 17 | 18 | return quantitySamples 19 | } 20 | 21 | export default querySources 22 | -------------------------------------------------------------------------------- /src/utils/queryStateOfMindSamples.ts: -------------------------------------------------------------------------------- 1 | import Native from '../native-types' 2 | 3 | export const queryStateOfMindSamples = async ({ 4 | from, to, limit, ascending, 5 | }: { readonly from?: Date; readonly to?: Date; readonly limit?: number; readonly ascending?: boolean } = {}) => { 6 | const fromString = (from || new Date(0)).toISOString() 7 | const toString = (to || new Date(0)).toISOString() 8 | 9 | const res = await Native.queryStateOfMindSamples(fromString, toString, limit ?? 0, ascending ?? false) 10 | 11 | return res 12 | } 13 | 14 | export default queryStateOfMindSamples 15 | -------------------------------------------------------------------------------- /src/utils/queryStatisticsCollectionForQuantity.ts: -------------------------------------------------------------------------------- 1 | import ensureUnit from './ensureUnit' 2 | import Native from '../native-types' 3 | 4 | import type { 5 | HKQuantityTypeIdentifier, 6 | HKStatisticsOptions, 7 | UnitForIdentifier, 8 | IntervalComponents, 9 | } from '../native-types' 10 | 11 | async function queryStatisticsCollectionForQuantity< 12 | TIdentifier extends HKQuantityTypeIdentifier, 13 | TUnit extends UnitForIdentifier = UnitForIdentifier 14 | >( 15 | identifier: TIdentifier, 16 | options: readonly HKStatisticsOptions[], 17 | anchorDate: Date, 18 | intervalComponents: IntervalComponents, 19 | startDate: Date, 20 | endDate: Date, 21 | unit?: TUnit, 22 | ) { 23 | const actualUnit = await ensureUnit(identifier, unit) 24 | 25 | const rawResponse = await Native.queryStatisticsCollectionForQuantity( 26 | identifier, 27 | actualUnit, 28 | options, 29 | anchorDate.toISOString(), 30 | intervalComponents, 31 | startDate.toISOString(), 32 | endDate.toISOString(), 33 | ) 34 | 35 | return rawResponse 36 | } 37 | 38 | export default queryStatisticsCollectionForQuantity 39 | -------------------------------------------------------------------------------- /src/utils/queryStatisticsForQuantity.ts: -------------------------------------------------------------------------------- 1 | import ensureUnit from './ensureUnit' 2 | import Native from '../native-types' 3 | 4 | import type { HKQuantityTypeIdentifier, HKStatisticsOptions, UnitForIdentifier } from '../native-types' 5 | 6 | async function queryStatisticsForQuantity = UnitForIdentifier>( 7 | identifier: TIdentifier, 8 | options: readonly HKStatisticsOptions[], 9 | from: Date, 10 | to?: Date, 11 | unit?: TUnit, 12 | ) { 13 | const actualUnit = await ensureUnit(identifier, unit) 14 | const toDate = to || new Date() 15 | const { mostRecentQuantityDateInterval, ...rawResponse } = await Native.queryStatisticsForQuantity( 16 | identifier, 17 | actualUnit, 18 | from.toISOString(), 19 | toDate.toISOString(), 20 | options, 21 | ) 22 | 23 | const response = { 24 | ...rawResponse, 25 | ...(mostRecentQuantityDateInterval 26 | ? { 27 | mostRecentQuantityDateInterval: { 28 | from: new Date(mostRecentQuantityDateInterval.from), 29 | to: new Date(mostRecentQuantityDateInterval.to), 30 | }, 31 | } 32 | : {}), 33 | } 34 | 35 | return response 36 | } 37 | 38 | export default queryStatisticsForQuantity 39 | -------------------------------------------------------------------------------- /src/utils/queryWorkoutSamplesWithAnchor.ts: -------------------------------------------------------------------------------- 1 | import deserializeWorkout from './deserializeWorkout' 2 | import getPreferredUnitsTyped from './getPreferredUnitsTyped' 3 | import prepareOptions from './prepareOptions' 4 | import Native from '../native-types' 5 | 6 | import type { 7 | EnergyUnit, LengthUnit, 8 | } from '../native-types' 9 | import type { 10 | DeletedWorkoutSampleRaw, HKWorkout, QueryWorkoutsOptions, 11 | } from '../types' 12 | 13 | export type QueryWorkoutSamplesWithAnchorResponse< 14 | TEnergy extends EnergyUnit, 15 | TDistance extends LengthUnit 16 | > = { 17 | readonly samples: readonly HKWorkout[], 18 | readonly deletedSamples: readonly DeletedWorkoutSampleRaw[], 19 | readonly newAnchor: string 20 | } 21 | 22 | async function queryCategorySamplesWithAnchor< 23 | TEnergy extends EnergyUnit, 24 | TDistance extends LengthUnit 25 | >( 26 | options: Omit, 'ascending'>, 27 | ): Promise> { 28 | const opts = prepareOptions(options) 29 | const { energyUnit, distanceUnit } = await getPreferredUnitsTyped(options) 30 | const raw = await Native.queryWorkoutSamplesWithAnchor( 31 | energyUnit, 32 | distanceUnit, 33 | opts.from, 34 | opts.to, 35 | opts.limit, 36 | opts.anchor, 37 | ) 38 | 39 | return { 40 | samples: raw.samples.map(deserializeWorkout), 41 | deletedSamples: raw.deletedSamples, 42 | newAnchor: raw.newAnchor, 43 | } 44 | } 45 | 46 | export default queryCategorySamplesWithAnchor 47 | -------------------------------------------------------------------------------- /src/utils/queryWorkouts.ts: -------------------------------------------------------------------------------- 1 | import deserializeWorkout from './deserializeWorkout' 2 | import getPreferredUnitsTyped from './getPreferredUnitsTyped' 3 | import prepareOptions from './prepareOptions' 4 | import Native from '../native-types' 5 | 6 | import type { EnergyUnit, LengthUnit } from '../native-types' 7 | import type { QueryWorkoutsOptions } from '../types' 8 | 9 | async function queryWorkouts< 10 | TEnergy extends EnergyUnit, 11 | TDistance extends LengthUnit 12 | >(options: QueryWorkoutsOptions) { 13 | const { energyUnit, distanceUnit } = await getPreferredUnitsTyped(options) 14 | const opts = prepareOptions(options) 15 | 16 | const workouts = await Native.queryWorkoutSamples( 17 | energyUnit, 18 | distanceUnit, 19 | opts.from, 20 | opts.to, 21 | opts.limit, 22 | opts.ascending, 23 | ) 24 | 25 | return workouts.map(deserializeWorkout) 26 | } 27 | 28 | export default queryWorkouts 29 | -------------------------------------------------------------------------------- /src/utils/requestAuthorization.ts: -------------------------------------------------------------------------------- 1 | import Native from '../native-types' 2 | 3 | import type { 4 | HealthkitReadAuthorization, HealthkitWriteAuthorization, ReadPermissions, WritePermissions, 5 | } from '../native-types' 6 | 7 | /** See https://developer.apple.com/documentation/healthkit/hkhealthstore/1614152-requestauthorization */ 8 | const requestAuthorization = async ( 9 | read: readonly HealthkitReadAuthorization[], 10 | write: readonly HealthkitWriteAuthorization[] = [], 11 | ): Promise => { 12 | const readPermissions = read.reduce((obj, cur) => ({ ...obj, [cur]: true }), {} as ReadPermissions) 13 | 14 | const writePermissions = write.reduce((obj, cur) => ({ ...obj, [cur]: true }), {} as WritePermissions) 15 | 16 | return Native.requestAuthorization(writePermissions, readPermissions) 17 | } 18 | 19 | export default requestAuthorization 20 | -------------------------------------------------------------------------------- /src/utils/saveCategorySample.ts: -------------------------------------------------------------------------------- 1 | import Native from '../native-types' 2 | 3 | import type { HKCategoryTypeIdentifier, HKCategoryValueForIdentifier, MetadataMapperForCategoryIdentifier } from '../native-types' 4 | 5 | /** 6 | * @see {@link https://developer.apple.com/documentation/healthkit/hkhealthstore/1614168-savecategorysample save(_:withCompletion:) (Apple Docs)} 7 | * @see {@link https://developer.apple.com/documentation/healthkit/saving_data_to_healthkit Saving data to HealthKit (Apple Docs)} 8 | */ 9 | async function saveCategorySample( 10 | identifier: T, 11 | value: HKCategoryValueForIdentifier, 12 | options?: { 13 | readonly start?: Date; 14 | readonly end?: Date; 15 | readonly metadata?: MetadataMapperForCategoryIdentifier; 16 | }, 17 | ) { 18 | const start = options?.start || options?.end || new Date() 19 | const end = options?.end || options?.start || new Date() 20 | const metadata = options?.metadata || {} 21 | 22 | return Native.saveCategorySample( 23 | identifier, 24 | value, 25 | start.toISOString(), 26 | end.toISOString(), 27 | metadata || {}, 28 | ) 29 | } 30 | 31 | export default saveCategorySample 32 | -------------------------------------------------------------------------------- /src/utils/saveCorrelationSample.ts: -------------------------------------------------------------------------------- 1 | import ensureMetadata from './ensureMetadata' 2 | import Native from '../native-types' 3 | 4 | import type { MetadataMapperForCorrelationIdentifier, HKCorrelationTypeIdentifier } from '../native-types' 5 | import type { HKCategorySampleForSaving, HKQuantitySampleForSaving } from '../types' 6 | 7 | async function saveCorrelationSample< 8 | TIdentifier extends HKCorrelationTypeIdentifier, 9 | TSamples extends readonly( 10 | | HKCategorySampleForSaving 11 | | HKQuantitySampleForSaving 12 | )[] 13 | >( 14 | typeIdentifier: TIdentifier, 15 | samples: TSamples, 16 | options?: { 17 | readonly start?: Date; 18 | readonly end?: Date; 19 | readonly metadata?: MetadataMapperForCorrelationIdentifier; 20 | }, 21 | ) { 22 | const start = (options?.start || new Date()).toISOString() 23 | const end = (options?.end || new Date()).toISOString() 24 | 25 | return Native.saveCorrelationSample( 26 | typeIdentifier, 27 | samples.map((sample) => { 28 | const { startDate, endDate, ...rest } = sample 29 | const updatedSample = { 30 | ...rest, 31 | ...(startDate && { startDate: new Date(startDate).toISOString() }), 32 | ...(endDate && { endDate: new Date(endDate).toISOString() }), 33 | } 34 | 35 | return { ...updatedSample, metadata: ensureMetadata(sample.metadata) } 36 | }), 37 | start, 38 | end, 39 | ensureMetadata(options?.metadata), 40 | ) 41 | } 42 | 43 | export default saveCorrelationSample 44 | -------------------------------------------------------------------------------- /src/utils/saveQuantitySample.ts: -------------------------------------------------------------------------------- 1 | import Native from '../native-types' 2 | 3 | import type { MetadataMapperForQuantityIdentifier, HKQuantityTypeIdentifier, UnitForIdentifier } from '../native-types' 4 | 5 | async function saveQuantitySample( 6 | identifier: TType, 7 | unit: UnitForIdentifier, 8 | value: number, 9 | options?: { 10 | readonly start?: Date; 11 | readonly end?: Date; 12 | readonly metadata?: MetadataMapperForQuantityIdentifier; 13 | }, 14 | ) { 15 | const start = options?.start || options?.end || new Date() 16 | const end = options?.end || options?.start || new Date() 17 | const metadata = options?.metadata || {} 18 | 19 | return Native.saveQuantitySample( 20 | identifier, 21 | unit, 22 | value, 23 | start.toISOString(), 24 | end.toISOString(), 25 | metadata, 26 | ) 27 | } 28 | 29 | export default saveQuantitySample 30 | -------------------------------------------------------------------------------- /src/utils/saveStateOfMindSample.ts: -------------------------------------------------------------------------------- 1 | import Native from '../native-types' 2 | 3 | import type { HKStateOfMindSampleRaw } from '../native-types' 4 | 5 | /** 6 | * @see {@link https://developer.apple.com/documentation/healthkit/hkstateofmind Saving state of mind samples (Apple Docs)} 7 | * @description Saves a state of mind sample to HealthKit, including kind, mood, valence, labels (optional), and associations (optional). 8 | */ 9 | async function saveStateOfMindSample( 10 | options: Omit< 11 | HKStateOfMindSampleRaw, 12 | | 'uuid' 13 | | 'valenceClassification' 14 | | 'startDate' 15 | | 'endDate' 16 | | 'labels' 17 | | 'associations' 18 | | 'device' 19 | | 'sourceRevision' 20 | > & { 21 | // allow the user to provide a Date instead of expecting them to provide date.toISOString() 22 | readonly date: Date 23 | // omitting then redeclaring these in order to make them optional 24 | readonly associations?: HKStateOfMindSampleRaw['associations'] 25 | readonly labels?: HKStateOfMindSampleRaw['labels'] 26 | }, 27 | ) { 28 | return Native.saveStateOfMindSample( 29 | options.date.toISOString(), 30 | options.kind, 31 | options.valence, 32 | options.labels || [], 33 | options.associations || [], 34 | options.metadata || {}, 35 | ) 36 | } 37 | 38 | export default saveStateOfMindSample 39 | -------------------------------------------------------------------------------- /src/utils/saveWorkoutRoute.ts: -------------------------------------------------------------------------------- 1 | import Native from '../native-types' 2 | 3 | import type { CLLocationForSaving } from '../types' 4 | 5 | async function saveWorkoutRoute( 6 | workoutUUID: string, 7 | locations: readonly CLLocationForSaving[], 8 | ) { 9 | return Native.saveWorkoutRoute( 10 | workoutUUID, 11 | locations.map((location) => { 12 | const { timestamp, ...rest } = location 13 | return { 14 | ...rest, 15 | ...(timestamp ? { timestamp: new Date(timestamp).toISOString() } : { timestamp: '' }), 16 | } 17 | }), 18 | ) 19 | } 20 | 21 | export default saveWorkoutRoute 22 | -------------------------------------------------------------------------------- /src/utils/saveWorkoutSample.ts: -------------------------------------------------------------------------------- 1 | import ensureMetadata from './ensureMetadata' 2 | import ensureTotals from './ensureTotals' 3 | import Native from '../native-types' 4 | 5 | import type { HKWorkoutActivityType, HKWorkoutMetadata } from '../native-types' 6 | import type { HKQuantitySampleForSaving } from '../types' 7 | 8 | async function saveWorkoutSample( 9 | typeIdentifier: TIdentifier, 10 | quantities: readonly HKQuantitySampleForSaving[], 11 | _start: Date, 12 | options?: { 13 | readonly end?: Date; 14 | readonly totals?: { 15 | readonly distance?: number; 16 | readonly energyBurned?: number; 17 | } 18 | readonly metadata?: HKWorkoutMetadata; 19 | }, 20 | ) { 21 | const start = _start.toISOString() 22 | const end = (options?.end || new Date()).toISOString() 23 | 24 | return Native.saveWorkoutSample( 25 | typeIdentifier, 26 | quantities.map((quantity) => { 27 | const { startDate, endDate, ...rest } = quantity 28 | const updatedQuantity = { 29 | ...rest, 30 | ...(startDate && { startDate: startDate.toISOString() }), 31 | ...(endDate && { endDate: endDate.toISOString() }), 32 | } 33 | return { ...updatedQuantity, metadata: ensureMetadata(quantity.metadata) } 34 | }), 35 | start, 36 | end, 37 | ensureTotals(options?.totals), 38 | ensureMetadata(options?.metadata), 39 | ) 40 | } 41 | 42 | export default saveWorkoutSample 43 | -------------------------------------------------------------------------------- /src/utils/serializeDate.test.ts: -------------------------------------------------------------------------------- 1 | import serializeDate from './serializeDate' 2 | 3 | describe('serializeDate', () => { 4 | it('should serialize zero date', () => { 5 | expect(serializeDate(new Date(0))).toBe('1970-01-01T00:00:00.000Z') 6 | }) 7 | 8 | it('should serialize date', () => { 9 | const date = new Date() 10 | expect(serializeDate(date)).toBe(date.toISOString()) 11 | }) 12 | 13 | it('should serialize null date', () => { 14 | expect(serializeDate(null)).toBe('1969-12-31T23:59:59.999Z') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/utils/serializeDate.ts: -------------------------------------------------------------------------------- 1 | const serializeDate = (date?: Date | null): string => ( 2 | (date || new Date(-1)).toISOString() 3 | ) 4 | 5 | export default serializeDate 6 | -------------------------------------------------------------------------------- /src/utils/startWatchApp.ts: -------------------------------------------------------------------------------- 1 | import Native from '../native-types' 2 | 3 | import type { HKWorkoutConfiguration } from '..' 4 | 5 | const startWatchApp = (configuration: HKWorkoutConfiguration) => async () => Native.startWatchAppWithWorkoutConfiguration(configuration) 6 | 7 | export default startWatchApp 8 | -------------------------------------------------------------------------------- /src/utils/subscribeToChanges.ts: -------------------------------------------------------------------------------- 1 | import Native, { EventEmitter } from '../native-types' 2 | 3 | import type { HKSampleTypeIdentifier } from '..' 4 | 5 | const subscribeToChanges = async ( 6 | identifier: HKSampleTypeIdentifier, 7 | callback: () => void, 8 | ) => { 9 | const subscription = EventEmitter.addListener( 10 | 'onChange', 11 | ({ typeIdentifier }: { readonly typeIdentifier: HKSampleTypeIdentifier }) => { 12 | if (typeIdentifier === identifier) { 13 | callback() 14 | } 15 | }, 16 | ) 17 | 18 | const queryId = await Native.subscribeToObserverQuery(identifier).catch( 19 | async (error) => { 20 | subscription.remove() 21 | return Promise.reject(error) 22 | }, 23 | ) 24 | 25 | return async () => { 26 | subscription.remove() 27 | return Native.unsubscribeQuery(queryId) 28 | } 29 | } 30 | 31 | export default subscribeToChanges 32 | -------------------------------------------------------------------------------- /src/utils/workoutSessionMirroringStartHandler.ts: -------------------------------------------------------------------------------- 1 | import Native from '../native-types' 2 | 3 | /** 4 | * A block that the system calls when it starts a mirrored workout session between iOS and watchOS apps. 5 | * @see {@link https://developer.apple.com/documentation/healthkit/hkhealthstore/4172878-workoutsessionmirroringstarthand workoutSessionMirroringStartHandler (Apple Docs)} 6 | * @returns {Promise} A promise that resolves to true if mirroring started successfully, false otherwise. 7 | * @throws {Error} If there's an error starting the mirroring session. 8 | */ 9 | const workoutSessionMirroringStartHandler: () => Promise = async () => Native.workoutSessionMirroringStartHandler() 10 | 11 | export default workoutSessionMirroringStartHandler 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@kingstinct/react-native-healthkit": [ "./src/index" ], 6 | "@kingstinct/react-native-healthkit/*": [ "./src/*" ] 7 | }, 8 | "allowUnreachableCode": false, 9 | "allowUnusedLabels": false, 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "jsx": "react", 13 | "lib": [ "esnext" ], 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitReturns": true, 18 | "noImplicitUseStrict": false, 19 | "noUncheckedIndexedAccess": true, 20 | "noStrictGenericChecks": false, 21 | "resolveJsonModule": true, 22 | "skipLibCheck": true, 23 | "strict": true, 24 | "target": "esnext" 25 | } 26 | } 27 | --------------------------------------------------------------------------------