Skip to content

Commit

Permalink
Merge pull request #112 from runna-app/feat/start-watch-app-support
Browse files Browse the repository at this point in the history
  • Loading branch information
robertherber authored Nov 13, 2024
2 parents 8706c91 + b29c6c7 commit 8bef672
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 0 deletions.
3 changes: 3 additions & 0 deletions ios/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ let HKWorkoutTypeIdentifier = "HKWorkoutTypeIdentifier"
let HKWorkoutRouteTypeIdentifier = "HKWorkoutRouteTypeIdentifier"
let HKDataTypeIdentifierHeartbeatSeries = "HKDataTypeIdentifierHeartbeatSeries"

let HKWorkoutActivityTypePropertyName = "activityType"
let HKWorkoutSessionLocationTypePropertyName = "locationType"

let SpeedUnit = HKUnit(from: "m/s") // HKUnit.meter().unitDivided(by: HKUnit.second())
// Support for MET data: HKAverageMETs 8.24046 kcal/hr·kg
let METUnit = HKUnit(from: "kcal/hr·kg")
16 changes: 16 additions & 0 deletions ios/Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,19 @@ func objectTypeFromString(typeIdentifier: String) -> HKObjectType? {

return nil
}

func parseWorkoutConfiguration(_ dict: NSDictionary) -> HKWorkoutConfiguration {
let configuration = HKWorkoutConfiguration()

if let activityTypeRaw = dict[HKWorkoutActivityTypePropertyName] as? UInt,
let activityType = HKWorkoutActivityType(rawValue: activityTypeRaw) {
configuration.activityType = activityType
}

if let locationTypeRaw = dict[HKWorkoutSessionLocationTypePropertyName] as? Int,
let locationType = HKWorkoutSessionLocationType(rawValue: locationTypeRaw) {
configuration.locationType = locationType
}

return configuration
}
4 changes: 4 additions & 0 deletions ios/ReactNativeHealthkit.m
Original file line number Diff line number Diff line change
Expand Up @@ -230,5 +230,9 @@ @interface RCT_EXTERN_MODULE(ReactNativeHealthkit, RCTEventEmitter)
RCT_EXTERN_METHOD(isProtectedDataAvailable:(RCTPromiseResolveBlock)resolve
withRejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(startWatchAppWithWorkoutConfiguration:(NSDictionary)workoutConfiguration
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
)

@end
23 changes: 23 additions & 0 deletions ios/ReactNativeHealthkit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2003,4 +2003,27 @@ class ReactNativeHealthkit: RCTEventEmitter {
}
}

@available(iOS 17.0.0, *)
@objc(startWatchAppWithWorkoutConfiguration:resolve:reject:)
func startWatchAppWithWorkoutConfiguration(
_ workoutConfiguration: NSDictionary,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock
) {
guard let store = _store else {
return reject(INIT_ERROR, INIT_ERROR_MESSAGE, nil)
}

let configuration = parseWorkoutConfiguration(workoutConfiguration)

store.startWatchApp(with: configuration) { success, error in
if let error {
reject(INIT_ERROR, INIT_ERROR_MESSAGE, error)
return
}

resolve(success)
}
}

}
4 changes: 4 additions & 0 deletions src/index.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import saveCorrelationSample from './utils/saveCorrelationSample'
import saveQuantitySample from './utils/saveQuantitySample'
import saveWorkoutRoute from './utils/saveWorkoutRoute'
import saveWorkoutSample from './utils/saveWorkoutSample'
import startWatchApp from './utils/startWatchApp'
import subscribeToChanges from './utils/subscribeToChanges'

const currentMajorVersionIOS = Platform.OS === 'ios' ? parseInt(Platform.Version, 10) : 0
Expand Down Expand Up @@ -187,6 +188,8 @@ export default {
// subscriptions
subscribeToChanges,

startWatchApp,

/**
* @returns the most recent sample for the given category type.
*/
Expand Down Expand Up @@ -262,6 +265,7 @@ export {
saveWorkoutSample,
saveWorkoutRoute,
subscribeToChanges,
startWatchApp,
useMostRecentCategorySample,
useMostRecentQuantitySample,
useMostRecentWorkout,
Expand Down
3 changes: 3 additions & 0 deletions src/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const authorizationStatusFor = UnavailableFn(Promise.resolve(HKAuthorizationStat
saveWorkoutSample = UnavailableFn(Promise.resolve(null)),
saveWorkoutRoute = UnavailableFn(Promise.resolve(false)),
subscribeToChanges = UnavailableFn(Promise.resolve(async () => Promise.resolve(false))),
startWatchApp = UnavailableFn(async () => Promise.resolve(false)),
useMostRecentCategorySample = UnavailableFn(null),
useMostRecentQuantitySample = UnavailableFn(null),
useMostRecentWorkout = UnavailableFn(null),
Expand Down Expand Up @@ -141,6 +142,7 @@ const Healthkit: typeof ReactNativeHealthkit = {
saveWorkoutRoute,
saveWorkoutSample,
subscribeToChanges,
startWatchApp,
useHealthkitAuthorization,
useIsHealthDataAvailable,
useMostRecentCategorySample,
Expand Down Expand Up @@ -193,6 +195,7 @@ export {
saveWorkoutRoute,
saveWorkoutSample,
subscribeToChanges,
startWatchApp,
useHealthkitAuthorization,
useIsHealthDataAvailable,
useMostRecentCategorySample,
Expand Down
24 changes: 24 additions & 0 deletions src/native-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2008,6 +2008,23 @@ type QueryWorkoutSamplesWithAnchorResponseRaw<
readonly newAnchor: string
}

/**
* @see {@link https://developer.apple.com/documentation/healthkit/hkworkoutconfiguration Apple Docs }
*/
export type HKWorkoutConfiguration = {
readonly activityType: HKWorkoutActivityType;
readonly locationType?: HKWorkoutSessionLocationType;
};

/**
* @see {@link https://developer.apple.com/documentation/healthkit/hkworkoutsessionlocationtype Apple Docs }
*/
export enum HKWorkoutSessionLocationType {
unknown = 1,
indoor = 2,
outdoor = 3
}

type ReactNativeHealthkitTypeNative = {
/**
* @see {@link https://developer.apple.com/documentation/healthkit/hkhealthstore/1614180-ishealthdataavailable Apple Docs }
Expand Down Expand Up @@ -2219,6 +2236,13 @@ type ReactNativeHealthkitTypeNative = {
workoutUUID: string
) => Promise<readonly WorkoutRoute[]>;
readonly getWorkoutPlanById: (workoutUUID: string) => Promise<{readonly id: string, readonly activityType: HKWorkoutActivityType} | null>;

/**
* @see {@link https://developer.apple.com/documentation/healthkit/hkhealthstore/1648358-startwatchapp Apple Docs }
*/
readonly startWatchAppWithWorkoutConfiguration: (
workoutConfiguration: HKWorkoutConfiguration
) => Promise<boolean>;
};

const Native = NativeModules.ReactNativeHealthkit as ReactNativeHealthkitTypeNative
Expand Down
1 change: 1 addition & 0 deletions src/test-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ beforeAll(async () => {
unsubscribeQuery: jest.fn(),
saveWorkoutRoute: jest.fn(),
getWorkoutPlanById: jest.fn(),
startWatchAppWithWorkoutConfiguration: jest.fn(),
}

await mock.module('react-native', () => ({
Expand Down
7 changes: 7 additions & 0 deletions src/utils/startWatchApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Native from '../native-types'

import type { HKWorkoutConfiguration } from '..'

const startWatchApp = (configuration: HKWorkoutConfiguration) => async () => Native.startWatchAppWithWorkoutConfiguration(configuration)

export default startWatchApp

0 comments on commit 8bef672

Please sign in to comment.