From 4617fda8dcd2d9cf7f6bac6c769d27939d87650b Mon Sep 17 00:00:00 2001 From: Mikey Mann Date: Wed, 10 Jun 2026 08:33:16 -0700 Subject: [PATCH 1/5] feat: add ActivitySummary type --- .../src/types/ActivitySummary.ts | 32 +++++++++++++++++++ .../react-native-healthkit/src/types/index.ts | 1 + 2 files changed, 33 insertions(+) create mode 100644 packages/react-native-healthkit/src/types/ActivitySummary.ts diff --git a/packages/react-native-healthkit/src/types/ActivitySummary.ts b/packages/react-native-healthkit/src/types/ActivitySummary.ts new file mode 100644 index 00000000..eb63279a --- /dev/null +++ b/packages/react-native-healthkit/src/types/ActivitySummary.ts @@ -0,0 +1,32 @@ +/** + * One HKActivitySummary — the per-day rollup behind the Apple activity rings. + * + * Activity summaries are keyed by calendar day ({@link https://developer.apple.com/documentation/healthkit/hkactivitysummary Apple Docs}), + * not by timestamp, so the date is exposed as plain calendar components from + * `HKActivitySummary.dateComponents(for:)` rather than a `Date` — converting to a + * timestamp would force a timezone assumption onto callers. + */ +export interface ActivitySummary { + /** Calendar year of the summary day (user's current calendar). */ + readonly dateYear: number + /** Calendar month (1–12). */ + readonly dateMonth: number + /** Calendar day of month (1–31). */ + readonly dateDay: number + /** Move ring — active energy burned (kcal). */ + readonly activeEnergyBurned: number + /** Move ring goal (kcal). */ + readonly activeEnergyBurnedGoal: number + /** Exercise ring — Apple exercise time (minutes). */ + readonly appleExerciseTime: number + /** Exercise ring goal (minutes). */ + readonly appleExerciseTimeGoal: number + /** Stand ring — stand hours (count). */ + readonly appleStandHours: number + /** Stand ring goal (count). */ + readonly appleStandHoursGoal: number + /** Move-time variant (move-minutes mode) — minutes; 0 when the user's move ring tracks energy. */ + readonly appleMoveTime: number + /** Move-time goal (minutes); 0 when unused. */ + readonly appleMoveTimeGoal: number +} diff --git a/packages/react-native-healthkit/src/types/index.ts b/packages/react-native-healthkit/src/types/index.ts index 688c5c64..4722632a 100644 --- a/packages/react-native-healthkit/src/types/index.ts +++ b/packages/react-native-healthkit/src/types/index.ts @@ -1,3 +1,4 @@ +export * from './ActivitySummary' export * from './Auth' export * from './Background' export * from './CategoryType' From 78949aa666c7cda16947617a20dfaa719b06fc90 Mon Sep 17 00:00:00 2001 From: Mikey Mann Date: Wed, 10 Jun 2026 08:34:22 -0700 Subject: [PATCH 2/5] feat: add ActivitySummaryModule nitro spec --- packages/react-native-healthkit/src/modules.ts | 6 ++++++ .../src/specs/ActivitySummaryModule.nitro.ts | 13 +++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 packages/react-native-healthkit/src/specs/ActivitySummaryModule.nitro.ts diff --git a/packages/react-native-healthkit/src/modules.ts b/packages/react-native-healthkit/src/modules.ts index e695ffe0..1f3d9675 100644 --- a/packages/react-native-healthkit/src/modules.ts +++ b/packages/react-native-healthkit/src/modules.ts @@ -1,4 +1,5 @@ import { NitroModules } from 'react-native-nitro-modules' +import type { ActivitySummaryModule } from './specs/ActivitySummaryModule.nitro' import type { CategoryTypeModule, CategoryTypeModuleTyped, @@ -56,3 +57,8 @@ export const HeartbeatSeries = export const StateOfMind = NitroModules.createHybridObject('StateOfMindModule') + +export const ActivitySummaries = + NitroModules.createHybridObject( + 'ActivitySummaryModule', + ) diff --git a/packages/react-native-healthkit/src/specs/ActivitySummaryModule.nitro.ts b/packages/react-native-healthkit/src/specs/ActivitySummaryModule.nitro.ts new file mode 100644 index 00000000..fa60d73f --- /dev/null +++ b/packages/react-native-healthkit/src/specs/ActivitySummaryModule.nitro.ts @@ -0,0 +1,13 @@ +import type { HybridObject } from 'react-native-nitro-modules' +import type { ActivitySummary } from '../types/ActivitySummary' + +export interface ActivitySummaryModule extends HybridObject<{ ios: 'swift' }> { + /** + * Query HKActivitySummary rollups for the calendar days intersecting + * [startDate, endDate] (inclusive, interpreted in the user's current calendar). + */ + queryActivitySummaries( + startDate: Date, + endDate: Date, + ): Promise +} From 3ca7f055af93a537894b00f687af80c93fef4f3d Mon Sep 17 00:00:00 2001 From: Mikey Mann Date: Wed, 10 Jun 2026 08:40:52 -0700 Subject: [PATCH 3/5] feat: implement HKActivitySummaryQuery in ActivitySummaryModule --- .../ios/ActivitySummaryModule.swift | 51 +++++++++++++++++++ packages/react-native-healthkit/nitro.json | 3 ++ .../src/types/ActivitySummary.ts | 10 ++-- 3 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 packages/react-native-healthkit/ios/ActivitySummaryModule.swift diff --git a/packages/react-native-healthkit/ios/ActivitySummaryModule.swift b/packages/react-native-healthkit/ios/ActivitySummaryModule.swift new file mode 100644 index 00000000..524b34af --- /dev/null +++ b/packages/react-native-healthkit/ios/ActivitySummaryModule.swift @@ -0,0 +1,51 @@ +import HealthKit +import NitroModules + +class ActivitySummaryModule: HybridActivitySummaryModuleSpec { + func queryActivitySummaries(startDate: Date, endDate: Date) throws -> Promise<[ActivitySummary]> { + return Promise.async { + let calendar = Calendar.current + + var startComponents = calendar.dateComponents( + [.day, .month, .year, .era], from: startDate) + var endComponents = calendar.dateComponents( + [.day, .month, .year, .era], from: endDate) + // HKActivitySummaryQuery requires the calendar to be set on the predicate's + // DateComponents — without it the query returns no results. + startComponents.calendar = calendar + endComponents.calendar = calendar + + let predicate = HKQuery.predicate( + forActivitySummariesBetweenStart: startComponents, end: endComponents) + + let summaries: [HKActivitySummary] = try await withCheckedThrowingContinuation { + continuation in + let query = HKActivitySummaryQuery(predicate: predicate) { _, summaries, error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: summaries ?? []) + } + } + store.execute(query) + } + + return summaries.map { summary in + let components = summary.dateComponents(for: calendar) + return ActivitySummary( + dateYear: Double(components.year ?? 0), + dateMonth: Double(components.month ?? 0), + dateDay: Double(components.day ?? 0), + activeEnergyBurned: summary.activeEnergyBurned.doubleValue(for: .kilocalorie()), + activeEnergyBurnedGoal: summary.activeEnergyBurnedGoal.doubleValue(for: .kilocalorie()), + appleExerciseTime: summary.appleExerciseTime.doubleValue(for: .minute()), + appleExerciseTimeGoal: summary.appleExerciseTimeGoal.doubleValue(for: .minute()), + appleStandHours: summary.appleStandHours.doubleValue(for: .count()), + appleStandHoursGoal: summary.appleStandHoursGoal.doubleValue(for: .count()), + appleMoveTime: summary.appleMoveTime.doubleValue(for: .minute()), + appleMoveTimeGoal: summary.appleMoveTimeGoal.doubleValue(for: .minute()) + ) + } + } + } +} diff --git a/packages/react-native-healthkit/nitro.json b/packages/react-native-healthkit/nitro.json index 9e38fec1..6519a89a 100644 --- a/packages/react-native-healthkit/nitro.json +++ b/packages/react-native-healthkit/nitro.json @@ -38,6 +38,9 @@ }, "MedicationModule": { "swift": "MedicationModule" + }, + "ActivitySummaryModule": { + "swift": "ActivitySummaryModule" } }, "ignorePaths": ["**/node_modules"] diff --git a/packages/react-native-healthkit/src/types/ActivitySummary.ts b/packages/react-native-healthkit/src/types/ActivitySummary.ts index eb63279a..8aba3b5b 100644 --- a/packages/react-native-healthkit/src/types/ActivitySummary.ts +++ b/packages/react-native-healthkit/src/types/ActivitySummary.ts @@ -1,10 +1,12 @@ /** * One HKActivitySummary — the per-day rollup behind the Apple activity rings. * - * Activity summaries are keyed by calendar day ({@link https://developer.apple.com/documentation/healthkit/hkactivitysummary Apple Docs}), - * not by timestamp, so the date is exposed as plain calendar components from - * `HKActivitySummary.dateComponents(for:)` rather than a `Date` — converting to a - * timestamp would force a timezone assumption onto callers. + * Activity summaries are keyed by calendar day, not by timestamp, so the date is + * exposed as plain calendar components from `HKActivitySummary.dateComponents(for:)` + * rather than a `Date` — converting to a timestamp would force a timezone assumption + * onto callers. + * + * @see {@link https://developer.apple.com/documentation/healthkit/hkactivitysummary Apple Docs } */ export interface ActivitySummary { /** Calendar year of the summary day (user's current calendar). */ From 9e95efe573396a1e96c04825ec2d8e6e0d0923ef Mon Sep 17 00:00:00 2001 From: Mikey Mann Date: Wed, 10 Jun 2026 08:49:58 -0700 Subject: [PATCH 4/5] feat: export queryActivitySummaries with platform fallback --- .../src/healthkit.ios.ts | 19 +++++++++++++++++++ .../react-native-healthkit/src/healthkit.ts | 7 +++++++ 2 files changed, 26 insertions(+) diff --git a/packages/react-native-healthkit/src/healthkit.ios.ts b/packages/react-native-healthkit/src/healthkit.ios.ts index d55c45a1..853bbb5b 100644 --- a/packages/react-native-healthkit/src/healthkit.ios.ts +++ b/packages/react-native-healthkit/src/healthkit.ios.ts @@ -10,6 +10,7 @@ import useSubscribeToCategorySamples from './hooks/useSubscribeToCategorySamples import useSubscribeToChanges from './hooks/useSubscribeToChanges' import useSubscribeToQuantitySamples from './hooks/useSubscribeToQuantitySamples' import { + ActivitySummaries, CategoryTypes, Characteristics, Core, @@ -38,6 +39,7 @@ import type { MedicationDoseEventTyped, } from './types/Medication' import type { QuantityTypeIdentifier } from './types/QuantityTypeIdentifier' +import type { ActivitySummary } from './types/ActivitySummary' import type { StateOfMindSamplesWithAnchorResponseTyped, StateOfMindSampleTyped, @@ -296,6 +298,19 @@ const MedicationBindings = { > } +const ActivitySummaryBindings = { + queryActivitySummaries: bindRetypedMethod< + typeof ActivitySummaries, + typeof ActivitySummaries.queryActivitySummaries, + Promise + >(ActivitySummaries, ActivitySummaries.queryActivitySummaries), +} satisfies { + queryActivitySummaries: BoundMethod< + typeof ActivitySummaries.queryActivitySummaries, + Promise + > +} + // Named exports - all functions bound to their respective modules export const authorizationStatusFor = Core.authorizationStatusFor.bind(Core) export const requestPerObjectReadAuthorization = @@ -390,6 +405,9 @@ export const queryMedicationEvents = MedicationBindings.queryMedicationEvents export const queryMedicationEventsWithAnchor = MedicationBindings.queryMedicationEventsWithAnchor +export const queryActivitySummaries = + ActivitySummaryBindings.queryActivitySummaries + export const currentAppSource = Core.currentAppSource.bind(Core) export const getBiologicalSexAsync = @@ -461,6 +479,7 @@ export default { subscribeToQuantitySamples, startWatchApp, isProtectedDataAvailable, + queryActivitySummaries, queryStateOfMindSamples, queryStateOfMindSamplesWithAnchor, saveStateOfMindSample, diff --git a/packages/react-native-healthkit/src/healthkit.ts b/packages/react-native-healthkit/src/healthkit.ts index 11df17f4..9dc4fef1 100644 --- a/packages/react-native-healthkit/src/healthkit.ts +++ b/packages/react-native-healthkit/src/healthkit.ts @@ -390,6 +390,12 @@ export const startWatchApp = UnavailableFnFromModule( Promise.resolve(false), ) +// ActivitySummaryModule functions +export const queryActivitySummaries = UnavailableFnFromModule( + 'queryActivitySummaries', + Promise.resolve([]), +) + // StateOfMindModule functions export const queryStateOfMindSamples = UnavailableFnFromModule( 'queryStateOfMindSamples', @@ -630,6 +636,7 @@ const HealthkitModule = { subscribeToChanges, startWatchApp, isProtectedDataAvailable, + queryActivitySummaries, queryStateOfMindSamples, queryStateOfMindSamplesWithAnchor, saveStateOfMindSample, From 4608b7c2c0da9b021cd342e0ee948193049a7add Mon Sep 17 00:00:00 2001 From: Mikey Mann Date: Wed, 10 Jun 2026 09:28:00 -0700 Subject: [PATCH 5/5] chore: add changeset --- .changeset/itchy-pandas-applaud.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/itchy-pandas-applaud.md diff --git a/.changeset/itchy-pandas-applaud.md b/.changeset/itchy-pandas-applaud.md new file mode 100644 index 00000000..c45f233f --- /dev/null +++ b/.changeset/itchy-pandas-applaud.md @@ -0,0 +1,5 @@ +--- +"@kingstinct/react-native-healthkit": minor +--- + +Add `queryActivitySummaries(startDate, endDate)` exposing `HKActivitySummaryQuery` — per-day activity ring values and goals (move, exercise, stand)