// // HealthKitManager.swift // digitalpilates // // Native module for HealthKit authorization and sleep data access // import Foundation import React import HealthKit @objc(HealthKitManager) class HealthKitManager: NSObject, RCTBridgeModule { // HealthKit store instance private let healthStore = HKHealthStore() static func moduleName() -> String! { return "HealthKitManager" } static func requiresMainQueueSetup() -> Bool { return true } // MARK: - Authorization @objc func requestAuthorization( _ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { // Check if HealthKit is available on the device guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } // Define the data types we want to read let readTypes: Set = [ HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!, HKObjectType.quantityType(forIdentifier: .stepCount)!, HKObjectType.quantityType(forIdentifier: .heartRate)!, HKObjectType.quantityType(forIdentifier: .restingHeartRate)!, HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)!, HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)! ] // Define the data types we want to write (if any) let writeTypes: Set = [ HKObjectType.quantityType(forIdentifier: .bodyMass)! ] // Request authorization healthStore.requestAuthorization(toShare: writeTypes, read: readTypes) { [weak self] (success, error) in DispatchQueue.main.async { if let error = error { rejecter("AUTHORIZATION_ERROR", "Failed to authorize HealthKit: \(error.localizedDescription)", error) return } if success { // Check individual permissions var permissions: [String: Any] = [:] for type in readTypes { let status = self?.healthStore.authorizationStatus(for: type) let statusString = self?.authorizationStatusToString(status) permissions[type.identifier] = statusString } let result: [String: Any] = [ "success": true, "permissions": permissions ] resolver(result) } else { rejecter("AUTHORIZATION_DENIED", "User denied HealthKit authorization", nil) } } } } // MARK: - Sleep Data @objc func getSleepData( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } // Check authorization status for sleep analysis let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis)! let authStatus = healthStore.authorizationStatus(for: sleepType) guard authStatus == .sharingAuthorized else { rejecter("NOT_AUTHORIZED", "Not authorized to read sleep data", nil) return } // Parse options let startDate = parseDate(from: options["startDate"] as? String) ?? Calendar.current.date(byAdding: .day, value: -7, to: Date())! let endDate = parseDate(from: options["endDate"] as? String) ?? Date() let limit = options["limit"] as? Int ?? 100 // Create predicate for date range let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) // Create sort descriptor to get latest data first let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) // Create query let query = HKSampleQuery( sampleType: sleepType, predicate: predicate, limit: limit, sortDescriptors: [sortDescriptor] ) { [weak self] (query, samples, error) in DispatchQueue.main.async { if let error = error { rejecter("QUERY_ERROR", "Failed to query sleep data: \(error.localizedDescription)", error) return } guard let sleepSamples = samples as? [HKCategorySample] else { resolver([]) return } let sleepData = sleepSamples.map { sample in return [ "id": sample.uuid.uuidString, "startDate": self?.dateToISOString(sample.startDate) ?? "", "endDate": self?.dateToISOString(sample.endDate) ?? "", "value": sample.value, "categoryType": self?.sleepValueToString(sample.value) ?? "unknown", "duration": sample.endDate.timeIntervalSince(sample.startDate), "source": [ "name": sample.sourceRevision.source.name, "bundleIdentifier": sample.sourceRevision.source.bundleIdentifier ], "metadata": sample.metadata ?? [:] ] } let result: [String: Any] = [ "data": sleepData, "count": sleepData.count, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ] resolver(result) } } healthStore.execute(query) } // MARK: - Helper Methods private func authorizationStatusToString(_ status: HKAuthorizationStatus?) -> String { guard let status = status else { return "notDetermined" } switch status { case .notDetermined: return "notDetermined" case .sharingDenied: return "denied" case .sharingAuthorized: return "authorized" @unknown default: return "unknown" } } private func sleepValueToString(_ value: Int) -> String { switch value { case HKCategoryValueSleepAnalysis.inBed.rawValue: return "inBed" case HKCategoryValueSleepAnalysis.asleepUnspecified.rawValue: return "asleep" case HKCategoryValueSleepAnalysis.awake.rawValue: return "awake" case HKCategoryValueSleepAnalysis.asleepCore.rawValue: return "core" case HKCategoryValueSleepAnalysis.asleepDeep.rawValue: return "deep" case HKCategoryValueSleepAnalysis.asleepREM.rawValue: return "rem" default: return "unknown" } } private func parseDate(from string: String?) -> Date? { guard let string = string else { return nil } let formatter = ISO8601DateFormatter() return formatter.date(from: string) } private func dateToISOString(_ date: Date) -> String { let formatter = ISO8601DateFormatter() return formatter.string(from: date) } }