react-native-workouts 0.2.1 β 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -256
- package/app.json +2 -4
- package/build/ReactNativeWorkoutsModule.d.ts +4 -4
- package/build/ReactNativeWorkoutsModule.d.ts.map +1 -1
- package/build/ReactNativeWorkoutsModule.js.map +1 -1
- package/ios/ReactNativeWorkouts.podspec +8 -2
- package/ios/ReactNativeWorkoutsModule.swift +197 -106
- package/package.json +2 -4
- package/public/react-native-workouts-banner.png +0 -0
- package/src/ReactNativeWorkoutsModule.ts +4 -4
package/README.md
CHANGED
|
@@ -1,42 +1,25 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
1
3
|
# react-native-workouts
|
|
2
4
|
|
|
3
|
-
React Native Expo module for Apple WorkoutKit
|
|
5
|
+
ποΈββοΈ React Native Expo module for Apple WorkoutKit β create, preview, and sync
|
|
4
6
|
workouts to Apple Watch.
|
|
5
7
|
|
|
6
8
|
## Features
|
|
7
9
|
|
|
8
|
-
- Create custom interval workouts
|
|
9
|
-
|
|
10
|
-
- Create
|
|
11
|
-
- Create
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
## API Features
|
|
17
|
-
|
|
18
|
-
### Workout Creation
|
|
19
|
-
|
|
20
|
-
- **Custom Workouts** - Complex interval workouts with warmup, blocks
|
|
21
|
-
(iterations, work/recovery steps), cooldown, and alerts
|
|
22
|
-
- **Single Goal Workouts** - Simple distance, time, or energy-based workouts
|
|
23
|
-
- **Pacer Workouts** - Speed or pace target workouts
|
|
24
|
-
- **Preview Workouts** - Present Appleβs system workout preview modal (includes
|
|
25
|
-
add/send to Apple Watch)
|
|
26
|
-
|
|
27
|
-
### Scheduling
|
|
28
|
-
|
|
29
|
-
- **scheduleWorkout()** - Schedule custom workouts to Apple Watch
|
|
30
|
-
- **scheduleSingleGoalWorkout()** - Schedule single goal workouts
|
|
31
|
-
- **schedulePacerWorkout()** - Schedule pacer workouts
|
|
32
|
-
- **getScheduledWorkouts()** - List all scheduled workouts
|
|
33
|
-
- **removeScheduledWorkout(id)** - Remove specific workout
|
|
34
|
-
- **removeAllScheduledWorkouts()** - Clear all scheduled workouts
|
|
10
|
+
- β¨ Create custom interval workouts (warmup, blocks, cooldown, alerts)
|
|
11
|
+
- π― Create single-goal workouts (distance / time / energy)
|
|
12
|
+
- πββοΈ Create pacer workouts (pace / speed targets)
|
|
13
|
+
- π§© Create multisport Swim / Bike / Run workouts
|
|
14
|
+
- π Preview with Appleβs system UI (includes βAdd to Watchβ / βSend to Watchβ)
|
|
15
|
+
- βοΈ Schedule & sync to the Apple Watch Workout app
|
|
16
|
+
- π§ Hooks-first API + stateful `WorkoutPlan` handle (Expo Shared Object)
|
|
17
|
+
- β
Full TypeScript support
|
|
35
18
|
|
|
36
19
|
## Requirements
|
|
37
20
|
|
|
38
|
-
- iOS 17.0+
|
|
39
|
-
- Expo SDK 54+
|
|
21
|
+
- iOS 17.0+ **at runtime** (WorkoutKit)
|
|
22
|
+
- Tested with Expo SDK 54+
|
|
40
23
|
- Apple Watch paired with iPhone (for workout sync)
|
|
41
24
|
|
|
42
25
|
## Installation
|
|
@@ -45,18 +28,8 @@ workouts to Apple Watch.
|
|
|
45
28
|
npm install react-native-workouts
|
|
46
29
|
# or
|
|
47
30
|
yarn add react-native-workouts
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
### Expo Configuration
|
|
51
|
-
|
|
52
|
-
Add the module to your app.json:
|
|
53
|
-
|
|
54
|
-
```json
|
|
55
|
-
{
|
|
56
|
-
"expo": {
|
|
57
|
-
"plugins": ["react-native-workouts"]
|
|
58
|
-
}
|
|
59
|
-
}
|
|
31
|
+
# or
|
|
32
|
+
pnpm add react-native-workouts
|
|
60
33
|
```
|
|
61
34
|
|
|
62
35
|
### Info.plist Configuration
|
|
@@ -72,85 +45,84 @@ Add the following keys to your Info.plist for HealthKit access:
|
|
|
72
45
|
|
|
73
46
|
## Usage
|
|
74
47
|
|
|
75
|
-
### Authorization
|
|
48
|
+
### π Authorization (hook)
|
|
76
49
|
|
|
77
|
-
Before scheduling
|
|
50
|
+
Before previewing/scheduling, request authorization (HealthKit / WorkoutKit):
|
|
78
51
|
|
|
79
52
|
```typescript
|
|
80
|
-
import
|
|
81
|
-
|
|
82
|
-
// Check current authorization status
|
|
83
|
-
const status = await ReactNativeWorkouts.getAuthorizationStatus();
|
|
84
|
-
// Returns: 'authorized' | 'notDetermined' | 'denied' | 'unknown'
|
|
85
|
-
|
|
86
|
-
// Request authorization
|
|
87
|
-
const newStatus = await ReactNativeWorkouts.requestAuthorization();
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Object-oriented API (WorkoutPlan + Hooks)
|
|
53
|
+
import { useWorkoutAuthorization } from "react-native-workouts";
|
|
91
54
|
|
|
92
|
-
|
|
93
|
-
|
|
55
|
+
export function MyScreen() {
|
|
56
|
+
const { status, request, isLoading, error } = useWorkoutAuthorization();
|
|
94
57
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
- `plan.scheduleAndSync(date)` β **schedules** the plan using Apple's
|
|
98
|
-
`WorkoutScheduler` (this is how the plan is synced to the Apple Watch Workout
|
|
99
|
-
app)
|
|
100
|
-
- `plan.export()` β returns `{ id, kind, config }` so you can **persist/share**
|
|
101
|
-
the plan in your own backend
|
|
58
|
+
// status: 'authorized' | 'notDetermined' | 'denied' | 'unknown' | null
|
|
59
|
+
// request(): prompts the system dialog (if needed) and returns the new status
|
|
102
60
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
can import.
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
107
64
|
|
|
108
|
-
|
|
65
|
+
### π Quick start (hooks-first `WorkoutPlan`)
|
|
109
66
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
- `config`: the original config used to create the plan (so you can store it and
|
|
113
|
-
recreate the plan later)
|
|
67
|
+
Hooks create a **stateful `WorkoutPlan` shared object** (Expo Shared Object).
|
|
68
|
+
You call methods on the plan:
|
|
114
69
|
|
|
115
|
-
|
|
70
|
+
- `plan.preview()` β opens Appleβs workout preview UI (includes βAdd/Send to
|
|
71
|
+
Watchβ)
|
|
72
|
+
- `plan.scheduleAndSync(date)` β schedules using Appleβs `WorkoutScheduler`
|
|
73
|
+
(this is what syncs it to the Watch)
|
|
74
|
+
- `plan.export()` β returns `{ id, kind, config }` so you can persist/share and
|
|
75
|
+
recreate later
|
|
116
76
|
|
|
117
77
|
```typescript
|
|
118
|
-
import
|
|
78
|
+
import { useMemo } from "react";
|
|
79
|
+
import {
|
|
119
80
|
type CustomWorkoutConfig,
|
|
120
81
|
useCustomWorkout,
|
|
82
|
+
useWorkoutAuthorization,
|
|
121
83
|
} from "react-native-workouts";
|
|
122
84
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
goal: { type: "distance", value: 400, unit: "meters" },
|
|
134
|
-
},
|
|
85
|
+
export function MyWorkoutScreen() {
|
|
86
|
+
const { status, request } = useWorkoutAuthorization();
|
|
87
|
+
|
|
88
|
+
const config = useMemo<CustomWorkoutConfig>(
|
|
89
|
+
() => ({
|
|
90
|
+
activityType: "running",
|
|
91
|
+
locationType: "outdoor",
|
|
92
|
+
displayName: "Morning Intervals",
|
|
93
|
+
warmup: { goal: { type: "time", value: 5, unit: "minutes" } },
|
|
94
|
+
blocks: [
|
|
135
95
|
{
|
|
136
|
-
|
|
137
|
-
|
|
96
|
+
iterations: 4,
|
|
97
|
+
steps: [
|
|
98
|
+
{
|
|
99
|
+
purpose: "work",
|
|
100
|
+
goal: { type: "distance", value: 400, unit: "meters" },
|
|
101
|
+
alert: { type: "pace", min: 4, max: 5, unit: "min/km" },
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
purpose: "recovery",
|
|
105
|
+
goal: { type: "time", value: 90, unit: "seconds" },
|
|
106
|
+
},
|
|
107
|
+
],
|
|
138
108
|
},
|
|
139
109
|
],
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
110
|
+
cooldown: { goal: { type: "time", value: 5, unit: "minutes" } },
|
|
111
|
+
}),
|
|
112
|
+
[],
|
|
113
|
+
);
|
|
143
114
|
|
|
144
|
-
export function MyWorkoutScreen() {
|
|
145
115
|
const { plan, isLoading, error } = useCustomWorkout(config);
|
|
146
116
|
|
|
147
117
|
const preview = async () => {
|
|
148
118
|
if (!plan) return;
|
|
119
|
+
if (status !== "authorized") await request();
|
|
149
120
|
await plan.preview();
|
|
150
121
|
};
|
|
151
122
|
|
|
152
|
-
const
|
|
123
|
+
const scheduleTomorrowMorning = async () => {
|
|
153
124
|
if (!plan) return;
|
|
125
|
+
if (status !== "authorized") await request();
|
|
154
126
|
await plan.scheduleAndSync({
|
|
155
127
|
year: 2026,
|
|
156
128
|
month: 1,
|
|
@@ -164,191 +136,100 @@ export function MyWorkoutScreen() {
|
|
|
164
136
|
}
|
|
165
137
|
```
|
|
166
138
|
|
|
167
|
-
###
|
|
168
|
-
|
|
169
|
-
Create complex interval workouts with warmup, multiple blocks, and cooldown:
|
|
170
|
-
|
|
171
|
-
```typescript
|
|
172
|
-
import ReactNativeWorkouts from "react-native-workouts";
|
|
173
|
-
import type { CustomWorkoutConfig } from "react-native-workouts";
|
|
174
|
-
|
|
175
|
-
const workout: CustomWorkoutConfig = {
|
|
176
|
-
activityType: "running",
|
|
177
|
-
locationType: "outdoor",
|
|
178
|
-
displayName: "Morning Intervals",
|
|
179
|
-
warmup: {
|
|
180
|
-
goal: { type: "time", value: 5, unit: "minutes" },
|
|
181
|
-
},
|
|
182
|
-
blocks: [
|
|
183
|
-
{
|
|
184
|
-
iterations: 4,
|
|
185
|
-
steps: [
|
|
186
|
-
{
|
|
187
|
-
purpose: "work",
|
|
188
|
-
goal: { type: "distance", value: 400, unit: "meters" },
|
|
189
|
-
alert: { type: "pace", min: 4, max: 5, unit: "min/km" },
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
purpose: "recovery",
|
|
193
|
-
goal: { type: "time", value: 90, unit: "seconds" },
|
|
194
|
-
},
|
|
195
|
-
],
|
|
196
|
-
},
|
|
197
|
-
],
|
|
198
|
-
cooldown: {
|
|
199
|
-
goal: { type: "time", value: 5, unit: "minutes" },
|
|
200
|
-
},
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
// Validate the workout
|
|
204
|
-
const result = await ReactNativeWorkouts.createCustomWorkout(workout);
|
|
205
|
-
console.log(result.valid); // true
|
|
206
|
-
|
|
207
|
-
// Preview (system modal with "Add to Watch"/"Send to Watch")
|
|
208
|
-
await ReactNativeWorkouts.previewWorkout(workout);
|
|
209
|
-
|
|
210
|
-
// Schedule for tomorrow at 7 AM
|
|
211
|
-
const tomorrow = new Date();
|
|
212
|
-
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
213
|
-
|
|
214
|
-
const scheduleResult = await ReactNativeWorkouts.scheduleWorkout(workout, {
|
|
215
|
-
year: tomorrow.getFullYear(),
|
|
216
|
-
month: tomorrow.getMonth() + 1,
|
|
217
|
-
day: tomorrow.getDate(),
|
|
218
|
-
hour: 7,
|
|
219
|
-
minute: 0,
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
console.log(scheduleResult.id); // UUID of scheduled workout
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### Single Goal Workout
|
|
226
|
-
|
|
227
|
-
Create simple goal-based workouts:
|
|
139
|
+
### π― Single goal workouts (hook)
|
|
228
140
|
|
|
229
141
|
```typescript
|
|
230
|
-
import
|
|
142
|
+
import { useMemo } from "react";
|
|
231
143
|
import type { SingleGoalWorkoutConfig } from "react-native-workouts";
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// 500 calorie workout
|
|
250
|
-
const calorieBurn: SingleGoalWorkoutConfig = {
|
|
251
|
-
activityType: "highIntensityIntervalTraining",
|
|
252
|
-
displayName: "Calorie Burner",
|
|
253
|
-
goal: { type: "energy", value: 500, unit: "kilocalories" },
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
await ReactNativeWorkouts.createSingleGoalWorkout(fiveK);
|
|
257
|
-
|
|
258
|
-
// Preview
|
|
259
|
-
await ReactNativeWorkouts.previewSingleGoalWorkout(fiveK);
|
|
144
|
+
import { useSingleGoalWorkout } from "react-native-workouts";
|
|
145
|
+
|
|
146
|
+
export function FiveKScreen() {
|
|
147
|
+
const config = useMemo<SingleGoalWorkoutConfig>(
|
|
148
|
+
() => ({
|
|
149
|
+
activityType: "running",
|
|
150
|
+
locationType: "outdoor",
|
|
151
|
+
displayName: "5K Run",
|
|
152
|
+
goal: { type: "distance", value: 5, unit: "kilometers" },
|
|
153
|
+
}),
|
|
154
|
+
[],
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const { plan } = useSingleGoalWorkout(config);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
260
160
|
```
|
|
261
161
|
|
|
262
|
-
### Pacer
|
|
263
|
-
|
|
264
|
-
Create pace-based workouts:
|
|
162
|
+
### πββοΈ Pacer workouts (hook)
|
|
265
163
|
|
|
266
164
|
```typescript
|
|
267
|
-
import
|
|
165
|
+
import { useMemo } from "react";
|
|
268
166
|
import type { PacerWorkoutConfig } from "react-native-workouts";
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
167
|
+
import { usePacerWorkout } from "react-native-workouts";
|
|
168
|
+
|
|
169
|
+
export function TempoRunScreen() {
|
|
170
|
+
const config = useMemo<PacerWorkoutConfig>(
|
|
171
|
+
() => ({
|
|
172
|
+
activityType: "running",
|
|
173
|
+
locationType: "outdoor",
|
|
174
|
+
displayName: "Tempo Run",
|
|
175
|
+
target: { type: "pace", value: 5, unit: "min/km" },
|
|
176
|
+
}),
|
|
177
|
+
[],
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const { plan } = usePacerWorkout(config);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
285
183
|
```
|
|
286
184
|
|
|
287
|
-
### Swim / Bike / Run (
|
|
288
|
-
|
|
289
|
-
Create a multisport workout (triathlon-like) with an ordered list of activities:
|
|
185
|
+
### π§© Swim / Bike / Run (multisport) (hook)
|
|
290
186
|
|
|
291
187
|
```typescript
|
|
188
|
+
import { useMemo } from "react";
|
|
292
189
|
import type { SwimBikeRunWorkoutConfig } from "react-native-workouts";
|
|
190
|
+
import { useSwimBikeRunWorkout } from "react-native-workouts";
|
|
191
|
+
|
|
192
|
+
export function TriathlonScreen() {
|
|
193
|
+
const config = useMemo<SwimBikeRunWorkoutConfig>(
|
|
194
|
+
() => ({
|
|
195
|
+
displayName: "Sprint Triathlon",
|
|
196
|
+
activities: [
|
|
197
|
+
{ type: "swimming", locationType: "indoor" },
|
|
198
|
+
{ type: "cycling", locationType: "outdoor" },
|
|
199
|
+
{ type: "running", locationType: "outdoor" },
|
|
200
|
+
],
|
|
201
|
+
}),
|
|
202
|
+
[],
|
|
203
|
+
);
|
|
293
204
|
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
{ type: "swimming", locationType: "indoor" },
|
|
298
|
-
{ type: "cycling", locationType: "outdoor" },
|
|
299
|
-
{ type: "running", locationType: "outdoor" },
|
|
300
|
-
],
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
// Create a plan handle (recommended)
|
|
304
|
-
const plan = await ReactNativeWorkouts.createSwimBikeRunWorkoutPlan(triathlon);
|
|
305
|
-
|
|
306
|
-
// Preview
|
|
307
|
-
await plan.preview();
|
|
205
|
+
const { plan } = useSwimBikeRunWorkout(config);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
308
208
|
```
|
|
309
209
|
|
|
310
|
-
###
|
|
311
|
-
|
|
312
|
-
```typescript
|
|
313
|
-
// Get all scheduled workouts
|
|
314
|
-
const workouts = await ReactNativeWorkouts.getScheduledWorkouts();
|
|
315
|
-
|
|
316
|
-
// Remove a specific workout
|
|
317
|
-
await ReactNativeWorkouts.removeScheduledWorkout(workouts[0].id);
|
|
210
|
+
### π¦ Persist / share a plan (`plan.export()`)
|
|
318
211
|
|
|
319
|
-
|
|
320
|
-
await ReactNativeWorkouts.removeAllScheduledWorkouts();
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
### Check Goal Support
|
|
212
|
+
`plan.export()` does **not** export a `.workout` file.
|
|
324
213
|
|
|
325
|
-
|
|
214
|
+
It returns a small JSON payload you can store in your backend and later
|
|
215
|
+
recreate:
|
|
326
216
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
"indoor",
|
|
331
|
-
"distance",
|
|
332
|
-
);
|
|
333
|
-
```
|
|
217
|
+
- `id`: UUID of the `WorkoutPlan` instance
|
|
218
|
+
- `kind`: `"custom" | "singleGoal" | "pacer" | "swimBikeRun"`
|
|
219
|
+
- `config`: the original config used to create the plan
|
|
334
220
|
|
|
335
|
-
###
|
|
221
|
+
### π
Managing scheduled workouts (hook)
|
|
336
222
|
|
|
337
223
|
```typescript
|
|
338
|
-
|
|
339
|
-
const activities = ReactNativeWorkouts.getSupportedActivityTypes();
|
|
340
|
-
// ['running', 'cycling', 'walking', 'hiking', ...]
|
|
341
|
-
|
|
342
|
-
// Get supported goal types
|
|
343
|
-
const goals = ReactNativeWorkouts.getSupportedGoalTypes();
|
|
344
|
-
// ['open', 'distance', 'time', 'energy']
|
|
224
|
+
import { useScheduledWorkouts } from "react-native-workouts";
|
|
345
225
|
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
226
|
+
export function ScheduledWorkoutsScreen() {
|
|
227
|
+
const { workouts, remove, removeAll, schedule, isLoading, error } =
|
|
228
|
+
useScheduledWorkouts();
|
|
349
229
|
|
|
350
|
-
//
|
|
351
|
-
|
|
230
|
+
// schedule(plan, date) calls plan.scheduleAndSync(date) and reloads the list
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
352
233
|
```
|
|
353
234
|
|
|
354
235
|
## API Reference
|
|
@@ -444,12 +325,23 @@ type PaceUnit = "minutesPerKilometer" | "min/km" | "minutesPerMile" | "min/mi";
|
|
|
444
325
|
|
|
445
326
|
- Swimming workouts do not support custom intervals, use SingleGoalWorkout
|
|
446
327
|
instead
|
|
447
|
-
- Not all activity types support all goal types - use `supportsGoal()` to check
|
|
448
328
|
- Scheduled workouts appear in the Workout app on Apple Watch and Fitness app on
|
|
449
329
|
iPhone
|
|
450
330
|
- Workouts display your app's icon and name in the Workout app
|
|
451
331
|
- The preview APIs use Appleβs system UI (`workoutPreview`) and require iOS 17+
|
|
452
332
|
|
|
333
|
+
### Deployment target note
|
|
334
|
+
|
|
335
|
+
This library is built on Apple's `WorkoutKit` (introduced in iOS 17). To avoid
|
|
336
|
+
forcing apps to raise their deployment target just to install the package, the
|
|
337
|
+
iOS native code:
|
|
338
|
+
|
|
339
|
+
- **weak-links** `WorkoutKit`
|
|
340
|
+
- gates all exported APIs behind `#available(iOS 17.0, *)`
|
|
341
|
+
|
|
342
|
+
On **iOS < 17**, calling any WorkoutKit API will reject/throw with an
|
|
343
|
+
`Unavailable` error.
|
|
344
|
+
|
|
453
345
|
## License
|
|
454
346
|
|
|
455
347
|
MIT
|
package/app.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NativeModule } from "expo";
|
|
2
|
-
import type { ActivityType, AuthorizationStatus, CustomWorkoutConfig, DateComponents, LocationType, PacerWorkoutConfig,
|
|
2
|
+
import type { ActivityType, AuthorizationStatus, CustomWorkoutConfig, DateComponents, LocationType, PacerWorkoutConfig, ReactNativeWorkoutsModuleEvents, ScheduledWorkout, ScheduleResult, SingleGoalWorkoutConfig, SwimBikeRunWorkoutConfig, WorkoutPlan, WorkoutValidationResult } from "./ReactNativeWorkouts.types";
|
|
3
3
|
declare class ReactNativeWorkoutsModule extends NativeModule<ReactNativeWorkoutsModuleEvents> {
|
|
4
4
|
/**
|
|
5
5
|
* Whether Health data is available on this device.
|
|
@@ -29,7 +29,7 @@ declare class ReactNativeWorkoutsModule extends NativeModule<ReactNativeWorkouts
|
|
|
29
29
|
/**
|
|
30
30
|
* Schedules a custom workout (syncs it to the Apple Watch Workout app).
|
|
31
31
|
*
|
|
32
|
-
* Prefer using `
|
|
32
|
+
* Prefer using `useCustomWorkout(...)` + `plan.scheduleAndSync(...)` for new code.
|
|
33
33
|
*/
|
|
34
34
|
scheduleWorkout(config: CustomWorkoutConfig, date: DateComponents): Promise<ScheduleResult>;
|
|
35
35
|
/**
|
|
@@ -43,7 +43,7 @@ declare class ReactNativeWorkoutsModule extends NativeModule<ReactNativeWorkouts
|
|
|
43
43
|
/**
|
|
44
44
|
* Schedules a single-goal workout (syncs it to the Apple Watch Workout app).
|
|
45
45
|
*
|
|
46
|
-
* Prefer using `
|
|
46
|
+
* Prefer using `useSingleGoalWorkout(...)` + `plan.scheduleAndSync(...)` for new code.
|
|
47
47
|
*/
|
|
48
48
|
scheduleSingleGoalWorkout(config: SingleGoalWorkoutConfig, date: DateComponents): Promise<ScheduleResult>;
|
|
49
49
|
/**
|
|
@@ -57,7 +57,7 @@ declare class ReactNativeWorkoutsModule extends NativeModule<ReactNativeWorkouts
|
|
|
57
57
|
/**
|
|
58
58
|
* Schedules a pacer workout (syncs it to the Apple Watch Workout app).
|
|
59
59
|
*
|
|
60
|
-
* Prefer using `
|
|
60
|
+
* Prefer using `usePacerWorkout(...)` + `plan.scheduleAndSync(...)` for new code.
|
|
61
61
|
*/
|
|
62
62
|
schedulePacerWorkout(config: PacerWorkoutConfig, date: DateComponents): Promise<ScheduleResult>;
|
|
63
63
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactNativeWorkoutsModule.d.ts","sourceRoot":"","sources":["../src/ReactNativeWorkoutsModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB
|
|
1
|
+
{"version":3,"file":"ReactNativeWorkoutsModule.d.ts","sourceRoot":"","sources":["../src/ReactNativeWorkoutsModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,+BAA+B,EAC/B,gBAAgB,EAChB,cAAc,EACd,uBAAuB,EACvB,wBAAwB,EACxB,WAAW,EACX,uBAAuB,EACxB,MAAM,6BAA6B,CAAC;AAErC,OAAO,OAAO,yBACZ,SAAQ,YAAY,CAAC,+BAA+B,CAAC;IAErD;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAG9B;;OAEG;IACH,sBAAsB,IAAI,OAAO,CAAC,mBAAmB,CAAC;IACtD;;OAEG;IACH,oBAAoB,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAGpD;;OAEG;IACH,YAAY,CACV,YAAY,EAAE,YAAY,EAC1B,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC;IAGnB;;OAEG;IACH,mBAAmB,CACjB,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,uBAAuB,CAAC;IACnC;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC;IAC7D;;;;OAIG;IACH,eAAe,CACb,MAAM,EAAE,mBAAmB,EAC3B,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,cAAc,CAAC;IAG1B;;OAEG;IACH,uBAAuB,CACrB,MAAM,EAAE,uBAAuB,GAC9B,OAAO,CAAC,uBAAuB,CAAC;IACnC;;OAEG;IACH,wBAAwB,CAAC,MAAM,EAAE,uBAAuB,GAAG,OAAO,CAAC,OAAO,CAAC;IAC3E;;;;OAIG;IACH,yBAAyB,CACvB,MAAM,EAAE,uBAAuB,EAC/B,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,cAAc,CAAC;IAG1B;;OAEG;IACH,kBAAkB,CAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,uBAAuB,CAAC;IACnC;;OAEG;IACH,mBAAmB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IACjE;;;;OAIG;IACH,oBAAoB,CAClB,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,cAAc,CAAC;IAG1B;;OAEG;IACH,uBAAuB,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,WAAW,CAAC;IAC1E;;OAEG;IACH,2BAA2B,CACzB,MAAM,EAAE,uBAAuB,GAC9B,OAAO,CAAC,WAAW,CAAC;IACvB;;OAEG;IACH,sBAAsB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,WAAW,CAAC;IACxE;;OAEG;IACH,4BAA4B,CAC1B,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,WAAW,CAAC;IAGvB;;OAEG;IACH,oBAAoB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IACnD;;OAEG;IACH,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IACpD;;OAEG;IACH,0BAA0B,IAAI,OAAO,CAAC,OAAO,CAAC;IAG9C,yBAAyB,IAAI,YAAY,EAAE;IAC3C,qBAAqB,IAAI,MAAM,EAAE;IACjC,yBAAyB,IAAI,YAAY,EAAE;CAC5C;;AAED,wBAEE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactNativeWorkoutsModule.js","sourceRoot":"","sources":["../src/ReactNativeWorkoutsModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAwJzD,eAAe,mBAAmB,CAChC,qBAAqB,CACtB,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\n\nimport type {\n ActivityType,\n AuthorizationStatus,\n CustomWorkoutConfig,\n DateComponents,\n LocationType,\n PacerWorkoutConfig,\n
|
|
1
|
+
{"version":3,"file":"ReactNativeWorkoutsModule.js","sourceRoot":"","sources":["../src/ReactNativeWorkoutsModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAwJzD,eAAe,mBAAmB,CAChC,qBAAqB,CACtB,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\n\nimport type {\n ActivityType,\n AuthorizationStatus,\n CustomWorkoutConfig,\n DateComponents,\n LocationType,\n PacerWorkoutConfig,\n ReactNativeWorkoutsModuleEvents,\n ScheduledWorkout,\n ScheduleResult,\n SingleGoalWorkoutConfig,\n SwimBikeRunWorkoutConfig,\n WorkoutPlan,\n WorkoutValidationResult,\n} from \"./ReactNativeWorkouts.types\";\n\ndeclare class ReactNativeWorkoutsModule\n extends NativeModule<ReactNativeWorkoutsModuleEvents> {\n // Constants\n /**\n * Whether Health data is available on this device.\n * On iOS simulators this is typically `false`.\n */\n readonly isAvailable: boolean;\n\n // Authorization\n /**\n * Returns the current WorkoutKit authorization status.\n */\n getAuthorizationStatus(): Promise<AuthorizationStatus>;\n /**\n * Prompts the user for WorkoutKit authorization (if needed).\n */\n requestAuthorization(): Promise<AuthorizationStatus>;\n\n // Validation\n /**\n * Returns whether a given goal type is supported for the provided activity + location.\n */\n supportsGoal(\n activityType: ActivityType,\n locationType: LocationType,\n goalType: string,\n ): Promise<boolean>;\n\n // Custom Workouts\n /**\n * Validates a custom workout config. (Legacy name: this does not persist anything.)\n */\n createCustomWorkout(\n config: CustomWorkoutConfig,\n ): Promise<WorkoutValidationResult>;\n /**\n * Previews a custom workout via Apple's system modal.\n */\n previewWorkout(config: CustomWorkoutConfig): Promise<boolean>;\n /**\n * Schedules a custom workout (syncs it to the Apple Watch Workout app).\n *\n * Prefer using `useCustomWorkout(...)` + `plan.scheduleAndSync(...)` for new code.\n */\n scheduleWorkout(\n config: CustomWorkoutConfig,\n date: DateComponents,\n ): Promise<ScheduleResult>;\n\n // Single Goal Workouts\n /**\n * Validates a single-goal workout config. (Legacy name: this does not persist anything.)\n */\n createSingleGoalWorkout(\n config: SingleGoalWorkoutConfig,\n ): Promise<WorkoutValidationResult>;\n /**\n * Previews a single-goal workout via Apple's system modal.\n */\n previewSingleGoalWorkout(config: SingleGoalWorkoutConfig): Promise<boolean>;\n /**\n * Schedules a single-goal workout (syncs it to the Apple Watch Workout app).\n *\n * Prefer using `useSingleGoalWorkout(...)` + `plan.scheduleAndSync(...)` for new code.\n */\n scheduleSingleGoalWorkout(\n config: SingleGoalWorkoutConfig,\n date: DateComponents,\n ): Promise<ScheduleResult>;\n\n // Pacer Workouts\n /**\n * Validates a pacer workout config. (Legacy name: this does not persist anything.)\n */\n createPacerWorkout(\n config: PacerWorkoutConfig,\n ): Promise<WorkoutValidationResult>;\n /**\n * Previews a pacer workout via Apple's system modal.\n */\n previewPacerWorkout(config: PacerWorkoutConfig): Promise<boolean>;\n /**\n * Schedules a pacer workout (syncs it to the Apple Watch Workout app).\n *\n * Prefer using `usePacerWorkout(...)` + `plan.scheduleAndSync(...)` for new code.\n */\n schedulePacerWorkout(\n config: PacerWorkoutConfig,\n date: DateComponents,\n ): Promise<ScheduleResult>;\n\n // Shared WorkoutPlan factories (object-oriented API)\n /**\n * Creates a stateful `WorkoutPlan` shared object (recommended API for new code).\n */\n createCustomWorkoutPlan(config: CustomWorkoutConfig): Promise<WorkoutPlan>;\n /**\n * Creates a stateful `WorkoutPlan` shared object (recommended API for new code).\n */\n createSingleGoalWorkoutPlan(\n config: SingleGoalWorkoutConfig,\n ): Promise<WorkoutPlan>;\n /**\n * Creates a stateful `WorkoutPlan` shared object (recommended API for new code).\n */\n createPacerWorkoutPlan(config: PacerWorkoutConfig): Promise<WorkoutPlan>;\n /**\n * Creates a stateful multisport `WorkoutPlan` shared object (recommended API for new code).\n */\n createSwimBikeRunWorkoutPlan(\n config: SwimBikeRunWorkoutConfig,\n ): Promise<WorkoutPlan>;\n\n // Scheduled Workouts Management\n /**\n * Lists scheduled workouts created by this app.\n */\n getScheduledWorkouts(): Promise<ScheduledWorkout[]>;\n /**\n * Removes a scheduled workout by ID.\n */\n removeScheduledWorkout(id: string): Promise<boolean>;\n /**\n * Removes all scheduled workouts created by this app.\n */\n removeAllScheduledWorkouts(): Promise<boolean>;\n\n // Utility\n getSupportedActivityTypes(): ActivityType[];\n getSupportedGoalTypes(): string[];\n getSupportedLocationTypes(): LocationType[];\n}\n\nexport default requireNativeModule<ReactNativeWorkoutsModule>(\n \"ReactNativeWorkouts\",\n);\n"]}
|
|
@@ -11,7 +11,10 @@ Pod::Spec.new do |s|
|
|
|
11
11
|
s.author = package['author']
|
|
12
12
|
s.homepage = package['homepage']
|
|
13
13
|
s.platforms = {
|
|
14
|
-
|
|
14
|
+
# The module APIs require iOS 17+ at runtime (WorkoutKit), but we keep the
|
|
15
|
+
# deployment target lower so apps can still build with e.g. iOS 15 targets.
|
|
16
|
+
# Calls on iOS < 17 will throw a descriptive "Unavailable" error.
|
|
17
|
+
:ios => '15.1'
|
|
15
18
|
}
|
|
16
19
|
s.swift_version = '5.9'
|
|
17
20
|
s.source = { git: 'https://github.com/Janjiran/react-native-workouts' }
|
|
@@ -19,7 +22,10 @@ Pod::Spec.new do |s|
|
|
|
19
22
|
|
|
20
23
|
s.dependency 'ExpoModulesCore'
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
# HealthKit exists on older iOS versions, keep it strongly linked.
|
|
26
|
+
s.frameworks = 'HealthKit'
|
|
27
|
+
# WorkoutKit does NOT exist on iOS < 17; weak-link it so the app can still load.
|
|
28
|
+
s.weak_frameworks = 'WorkoutKit'
|
|
23
29
|
|
|
24
30
|
s.pod_target_xcconfig = {
|
|
25
31
|
'DEFINES_MODULE' => 'YES',
|
|
@@ -6,14 +6,17 @@ import UIKit
|
|
|
6
6
|
|
|
7
7
|
// MARK: - Shared Objects
|
|
8
8
|
|
|
9
|
-
@available(iOS 17.0, *)
|
|
10
9
|
public final class WorkoutPlanObject: SharedObject {
|
|
11
|
-
|
|
10
|
+
// Keep the plan opaque so this object can be referenced in ModuleDefinition on iOS < 17.
|
|
11
|
+
// We only cast/use WorkoutKit types behind runtime availability checks.
|
|
12
|
+
fileprivate let planHandle: Any
|
|
13
|
+
fileprivate let planId: String
|
|
12
14
|
fileprivate let kind: String
|
|
13
15
|
fileprivate let sourceConfig: [String: Any]
|
|
14
16
|
|
|
15
|
-
fileprivate init(
|
|
16
|
-
self.
|
|
17
|
+
fileprivate init(planHandle: Any, planId: String, kind: String, sourceConfig: [String: Any]) {
|
|
18
|
+
self.planHandle = planHandle
|
|
19
|
+
self.planId = planId
|
|
17
20
|
self.kind = kind
|
|
18
21
|
self.sourceConfig = sourceConfig
|
|
19
22
|
super.init()
|
|
@@ -21,7 +24,7 @@ public final class WorkoutPlanObject: SharedObject {
|
|
|
21
24
|
|
|
22
25
|
fileprivate func export() -> [String: Any] {
|
|
23
26
|
return [
|
|
24
|
-
"id":
|
|
27
|
+
"id": planId,
|
|
25
28
|
"kind": kind,
|
|
26
29
|
"config": sourceConfig
|
|
27
30
|
]
|
|
@@ -29,19 +32,32 @@ public final class WorkoutPlanObject: SharedObject {
|
|
|
29
32
|
|
|
30
33
|
@MainActor
|
|
31
34
|
fileprivate func preview() async throws {
|
|
35
|
+
guard #available(iOS 17.0, *) else {
|
|
36
|
+
throw Exception(name: "Unavailable", description: "WorkoutKit requires iOS 17+. This API is unavailable on the current OS version.")
|
|
37
|
+
}
|
|
38
|
+
let plan = try self.getWorkoutPlan()
|
|
32
39
|
try await presentWorkoutPreview(plan)
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
fileprivate func schedule(at date: DateComponents) async throws -> [String: Any] {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return [
|
|
39
|
-
"success": true,
|
|
40
|
-
"id": plan.id.uuidString
|
|
41
|
-
]
|
|
42
|
-
} catch {
|
|
43
|
-
throw Exception(name: "ScheduleError", description: error.localizedDescription)
|
|
43
|
+
guard #available(iOS 17.0, *) else {
|
|
44
|
+
throw Exception(name: "Unavailable", description: "WorkoutKit requires iOS 17+. This API is unavailable on the current OS version.")
|
|
44
45
|
}
|
|
46
|
+
|
|
47
|
+
let plan = try self.getWorkoutPlan()
|
|
48
|
+
await WorkoutScheduler.shared.schedule(plan, at: date)
|
|
49
|
+
return [
|
|
50
|
+
"success": true,
|
|
51
|
+
"id": planId
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@available(iOS 17.0, *)
|
|
56
|
+
private func getWorkoutPlan() throws -> WorkoutPlan {
|
|
57
|
+
guard let plan = planHandle as? WorkoutPlan else {
|
|
58
|
+
throw Exception(name: "InvalidState", description: "Workout plan handle is invalid")
|
|
59
|
+
}
|
|
60
|
+
return plan
|
|
45
61
|
}
|
|
46
62
|
}
|
|
47
63
|
|
|
@@ -61,10 +77,12 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
61
77
|
|
|
62
78
|
Events("onAuthorizationChange")
|
|
63
79
|
|
|
80
|
+
let workoutKitUnavailableMessage = "WorkoutKit requires iOS 17+. This API is unavailable on the current OS version."
|
|
81
|
+
|
|
64
82
|
// MARK: - Shared Object API (WorkoutPlan)
|
|
65
83
|
Class("WorkoutPlan", WorkoutPlanObject.self) {
|
|
66
84
|
Property("id") { planObject in
|
|
67
|
-
return planObject.
|
|
85
|
+
return planObject.planId
|
|
68
86
|
}
|
|
69
87
|
|
|
70
88
|
Property("kind") { planObject in
|
|
@@ -87,15 +105,60 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
87
105
|
}
|
|
88
106
|
}
|
|
89
107
|
|
|
90
|
-
//
|
|
108
|
+
// MARK: - Authorization
|
|
109
|
+
|
|
110
|
+
AsyncFunction("getAuthorizationStatus") { () async throws -> String in
|
|
111
|
+
guard #available(iOS 17.0, *) else {
|
|
112
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let status = await WorkoutScheduler.shared.authorizationState
|
|
116
|
+
return self.authorizationStateToString(status)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
AsyncFunction("requestAuthorization") { () async throws -> String in
|
|
120
|
+
guard #available(iOS 17.0, *) else {
|
|
121
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let status = await WorkoutScheduler.shared.requestAuthorization()
|
|
125
|
+
return self.authorizationStateToString(status)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// MARK: - Workout Validation
|
|
129
|
+
|
|
130
|
+
AsyncFunction("supportsGoal") { (activityType: String, locationType: String, goalType: String) throws -> Bool in
|
|
131
|
+
guard #available(iOS 17.0, *) else {
|
|
132
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
guard let activity = self.parseActivityType(activityType),
|
|
136
|
+
let location = self.parseLocationType(locationType),
|
|
137
|
+
let goal = self.parseGoalTypeForValidation(goalType) else {
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return CustomWorkout.supportsGoal(goal, activity: activity, location: location)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// MARK: - Plan factories (return a WorkoutPlanObject handle to JS)
|
|
145
|
+
|
|
91
146
|
AsyncFunction("createCustomWorkoutPlan") { (config: [String: Any]) throws -> WorkoutPlanObject in
|
|
147
|
+
guard #available(iOS 17.0, *) else {
|
|
148
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
149
|
+
}
|
|
150
|
+
|
|
92
151
|
let workout = try self.buildCustomWorkout(from: config)
|
|
93
152
|
try self.validateCustomWorkout(workout)
|
|
94
153
|
let plan = WorkoutPlan(.custom(workout))
|
|
95
|
-
return WorkoutPlanObject(plan: plan, kind: "custom", sourceConfig: config)
|
|
154
|
+
return WorkoutPlanObject(planHandle: plan, planId: plan.id.uuidString, kind: "custom", sourceConfig: config)
|
|
96
155
|
}
|
|
97
156
|
|
|
98
157
|
AsyncFunction("createSingleGoalWorkoutPlan") { (config: [String: Any]) throws -> WorkoutPlanObject in
|
|
158
|
+
guard #available(iOS 17.0, *) else {
|
|
159
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
160
|
+
}
|
|
161
|
+
|
|
99
162
|
guard let activityTypeStr = config["activityType"] as? String,
|
|
100
163
|
let activity = self.parseActivityType(activityTypeStr),
|
|
101
164
|
let goalConfig = config["goal"] as? [String: Any] else {
|
|
@@ -111,10 +174,14 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
111
174
|
|
|
112
175
|
let workout = SingleGoalWorkout(activity: activity, location: location, goal: goal)
|
|
113
176
|
let plan = WorkoutPlan(.goal(workout))
|
|
114
|
-
return WorkoutPlanObject(plan: plan, kind: "singleGoal", sourceConfig: config)
|
|
177
|
+
return WorkoutPlanObject(planHandle: plan, planId: plan.id.uuidString, kind: "singleGoal", sourceConfig: config)
|
|
115
178
|
}
|
|
116
179
|
|
|
117
180
|
AsyncFunction("createPacerWorkoutPlan") { (config: [String: Any]) throws -> WorkoutPlanObject in
|
|
181
|
+
guard #available(iOS 17.0, *) else {
|
|
182
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
183
|
+
}
|
|
184
|
+
|
|
118
185
|
guard let activityTypeStr = config["activityType"] as? String,
|
|
119
186
|
let activity = self.parseActivityType(activityTypeStr),
|
|
120
187
|
let targetConfig = config["target"] as? [String: Any] else {
|
|
@@ -126,10 +193,14 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
126
193
|
|
|
127
194
|
let workout = PacerWorkout(activity: activity, location: location, distance: distance, time: time)
|
|
128
195
|
let plan = WorkoutPlan(.pacer(workout))
|
|
129
|
-
return WorkoutPlanObject(plan: plan, kind: "pacer", sourceConfig: config)
|
|
196
|
+
return WorkoutPlanObject(planHandle: plan, planId: plan.id.uuidString, kind: "pacer", sourceConfig: config)
|
|
130
197
|
}
|
|
131
198
|
|
|
132
199
|
AsyncFunction("createSwimBikeRunWorkoutPlan") { (config: [String: Any]) throws -> WorkoutPlanObject in
|
|
200
|
+
guard #available(iOS 17.0, *) else {
|
|
201
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
202
|
+
}
|
|
203
|
+
|
|
133
204
|
guard let activitiesConfig = config["activities"] as? [[String: Any]] else {
|
|
134
205
|
throw Exception(name: "InvalidConfig", description: "Missing required field: activities")
|
|
135
206
|
}
|
|
@@ -143,52 +214,32 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
143
214
|
|
|
144
215
|
let workout = SwimBikeRunWorkout(activities: activities, displayName: displayName)
|
|
145
216
|
let plan = WorkoutPlan(.swimBikeRun(workout))
|
|
146
|
-
return WorkoutPlanObject(plan: plan, kind: "swimBikeRun", sourceConfig: config)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// MARK: - Authorization
|
|
150
|
-
|
|
151
|
-
AsyncFunction("getAuthorizationStatus") { () async -> String in
|
|
152
|
-
let status = await WorkoutScheduler.shared.authorizationState
|
|
153
|
-
return self.authorizationStateToString(status)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
AsyncFunction("requestAuthorization") { () async -> String in
|
|
157
|
-
let status = await WorkoutScheduler.shared.requestAuthorization()
|
|
158
|
-
return self.authorizationStateToString(status)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// MARK: - Workout Validation
|
|
162
|
-
|
|
163
|
-
AsyncFunction("supportsGoal") { (activityType: String, locationType: String, goalType: String) -> Bool in
|
|
164
|
-
guard let activity = self.parseActivityType(activityType),
|
|
165
|
-
let location = self.parseLocationType(locationType),
|
|
166
|
-
let goal = self.parseGoalTypeForValidation(goalType) else {
|
|
167
|
-
return false
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return CustomWorkout.supportsGoal(goal, activity: activity, location: location)
|
|
217
|
+
return WorkoutPlanObject(planHandle: plan, planId: plan.id.uuidString, kind: "swimBikeRun", sourceConfig: config)
|
|
171
218
|
}
|
|
172
219
|
|
|
173
220
|
// MARK: - Custom Workout Creation
|
|
174
221
|
|
|
175
222
|
AsyncFunction("createCustomWorkout") { (config: [String: Any]) throws -> [String: Any] in
|
|
223
|
+
guard #available(iOS 17.0, *) else {
|
|
224
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
225
|
+
}
|
|
226
|
+
|
|
176
227
|
let workout = try self.buildCustomWorkout(from: config)
|
|
177
228
|
try self.validateCustomWorkout(workout)
|
|
178
229
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
]
|
|
184
|
-
} catch {
|
|
185
|
-
throw Exception(name: "ValidationError", description: error.localizedDescription)
|
|
186
|
-
}
|
|
230
|
+
return [
|
|
231
|
+
"valid": true,
|
|
232
|
+
"displayName": workout.displayName ?? (config["displayName"] as? String ?? "")
|
|
233
|
+
]
|
|
187
234
|
}
|
|
188
235
|
|
|
189
236
|
// MARK: - Workout Preview (system modal)
|
|
190
237
|
|
|
191
238
|
AsyncFunction("previewWorkout") { (config: [String: Any]) async throws -> Bool in
|
|
239
|
+
guard #available(iOS 17.0, *) else {
|
|
240
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
241
|
+
}
|
|
242
|
+
|
|
192
243
|
let workout = try self.buildCustomWorkout(from: config)
|
|
193
244
|
try self.validateCustomWorkout(workout)
|
|
194
245
|
let plan = WorkoutPlan(.custom(workout))
|
|
@@ -198,6 +249,10 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
198
249
|
}
|
|
199
250
|
|
|
200
251
|
AsyncFunction("previewSingleGoalWorkout") { (config: [String: Any]) async throws -> Bool in
|
|
252
|
+
guard #available(iOS 17.0, *) else {
|
|
253
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
254
|
+
}
|
|
255
|
+
|
|
201
256
|
guard let activityTypeStr = config["activityType"] as? String,
|
|
202
257
|
let activity = self.parseActivityType(activityTypeStr),
|
|
203
258
|
let goalConfig = config["goal"] as? [String: Any] else {
|
|
@@ -219,6 +274,10 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
219
274
|
}
|
|
220
275
|
|
|
221
276
|
AsyncFunction("previewPacerWorkout") { (config: [String: Any]) async throws -> Bool in
|
|
277
|
+
guard #available(iOS 17.0, *) else {
|
|
278
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
279
|
+
}
|
|
280
|
+
|
|
222
281
|
guard let activityTypeStr = config["activityType"] as? String,
|
|
223
282
|
let activity = self.parseActivityType(activityTypeStr),
|
|
224
283
|
let targetConfig = config["target"] as? [String: Any] else {
|
|
@@ -238,35 +297,42 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
238
297
|
// MARK: - Scheduled Workouts
|
|
239
298
|
|
|
240
299
|
AsyncFunction("scheduleWorkout") { (config: [String: Any], date: [String: Any]) async throws -> [String: Any] in
|
|
300
|
+
guard #available(iOS 17.0, *) else {
|
|
301
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
302
|
+
}
|
|
303
|
+
|
|
241
304
|
let workout = try self.buildCustomWorkout(from: config)
|
|
242
305
|
try self.validateCustomWorkout(workout)
|
|
243
306
|
|
|
244
307
|
let plan = WorkoutPlan(.custom(workout))
|
|
245
|
-
|
|
246
308
|
let dateComponents = self.parseDateComponents(from: date)
|
|
247
309
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
]
|
|
254
|
-
} catch {
|
|
255
|
-
throw Exception(name: "ScheduleError", description: error.localizedDescription)
|
|
256
|
-
}
|
|
310
|
+
await WorkoutScheduler.shared.schedule(plan, at: dateComponents)
|
|
311
|
+
return [
|
|
312
|
+
"success": true,
|
|
313
|
+
"id": plan.id.uuidString
|
|
314
|
+
]
|
|
257
315
|
}
|
|
258
316
|
|
|
259
317
|
AsyncFunction("getScheduledWorkouts") { () async throws -> [[String: Any]] in
|
|
318
|
+
guard #available(iOS 17.0, *) else {
|
|
319
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
320
|
+
}
|
|
321
|
+
|
|
260
322
|
let workouts = await WorkoutScheduler.shared.scheduledWorkouts
|
|
261
|
-
return workouts.map {
|
|
323
|
+
return workouts.map { scheduled in
|
|
262
324
|
return [
|
|
263
|
-
"id":
|
|
264
|
-
"date": self.dateComponentsToDict(
|
|
325
|
+
"id": scheduled.plan.id.uuidString,
|
|
326
|
+
"date": self.dateComponentsToDict(scheduled.date)
|
|
265
327
|
]
|
|
266
328
|
}
|
|
267
329
|
}
|
|
268
330
|
|
|
269
331
|
AsyncFunction("removeScheduledWorkout") { (id: String) async throws -> Bool in
|
|
332
|
+
guard #available(iOS 17.0, *) else {
|
|
333
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
334
|
+
}
|
|
335
|
+
|
|
270
336
|
guard let uuid = UUID(uuidString: id) else {
|
|
271
337
|
throw Exception(name: "InvalidID", description: "Invalid workout ID format")
|
|
272
338
|
}
|
|
@@ -276,27 +342,26 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
276
342
|
throw Exception(name: "NotFound", description: "Workout not found")
|
|
277
343
|
}
|
|
278
344
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
return true
|
|
282
|
-
} catch {
|
|
283
|
-
throw Exception(name: "RemoveError", description: error.localizedDescription)
|
|
284
|
-
}
|
|
345
|
+
await WorkoutScheduler.shared.remove(workout.plan, at: workout.date)
|
|
346
|
+
return true
|
|
285
347
|
}
|
|
286
348
|
|
|
287
349
|
AsyncFunction("removeAllScheduledWorkouts") { () async throws -> Bool in
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
} catch {
|
|
291
|
-
throw Exception(name: "RemoveError", description: error.localizedDescription)
|
|
350
|
+
guard #available(iOS 17.0, *) else {
|
|
351
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
292
352
|
}
|
|
293
353
|
|
|
354
|
+
await WorkoutScheduler.shared.removeAllWorkouts()
|
|
294
355
|
return true
|
|
295
356
|
}
|
|
296
357
|
|
|
297
358
|
// MARK: - Single Goal Workout
|
|
298
359
|
|
|
299
360
|
AsyncFunction("createSingleGoalWorkout") { (config: [String: Any]) throws -> [String: Any] in
|
|
361
|
+
guard #available(iOS 17.0, *) else {
|
|
362
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
363
|
+
}
|
|
364
|
+
|
|
300
365
|
guard let activityTypeStr = config["activityType"] as? String,
|
|
301
366
|
let activity = self.parseActivityType(activityTypeStr),
|
|
302
367
|
let goalConfig = config["goal"] as? [String: Any] else {
|
|
@@ -318,6 +383,10 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
318
383
|
}
|
|
319
384
|
|
|
320
385
|
AsyncFunction("scheduleSingleGoalWorkout") { (config: [String: Any], date: [String: Any]) async throws -> [String: Any] in
|
|
386
|
+
guard #available(iOS 17.0, *) else {
|
|
387
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
388
|
+
}
|
|
389
|
+
|
|
321
390
|
guard let activityTypeStr = config["activityType"] as? String,
|
|
322
391
|
let activity = self.parseActivityType(activityTypeStr),
|
|
323
392
|
let goalConfig = config["goal"] as? [String: Any] else {
|
|
@@ -326,7 +395,6 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
326
395
|
|
|
327
396
|
let goal = try self.parseWorkoutGoal(from: goalConfig)
|
|
328
397
|
let location = self.parseLocationType(config["locationType"] as? String ?? "outdoor") ?? .outdoor
|
|
329
|
-
_ = config["displayName"] as? String
|
|
330
398
|
|
|
331
399
|
guard SingleGoalWorkout.supportsGoal(goal, activity: activity, location: location) else {
|
|
332
400
|
throw Exception(name: "ValidationError", description: "Single goal workout not supported for this activity/location/goal")
|
|
@@ -336,21 +404,20 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
336
404
|
let plan = WorkoutPlan(.goal(workout))
|
|
337
405
|
|
|
338
406
|
let dateComponents = self.parseDateComponents(from: date)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
"id": plan.id.uuidString
|
|
345
|
-
]
|
|
346
|
-
} catch {
|
|
347
|
-
throw Exception(name: "ScheduleError", description: error.localizedDescription)
|
|
348
|
-
}
|
|
407
|
+
await WorkoutScheduler.shared.schedule(plan, at: dateComponents)
|
|
408
|
+
return [
|
|
409
|
+
"success": true,
|
|
410
|
+
"id": plan.id.uuidString
|
|
411
|
+
]
|
|
349
412
|
}
|
|
350
413
|
|
|
351
414
|
// MARK: - Pacer Workout
|
|
352
415
|
|
|
353
416
|
AsyncFunction("createPacerWorkout") { (config: [String: Any]) throws -> [String: Any] in
|
|
417
|
+
guard #available(iOS 17.0, *) else {
|
|
418
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
419
|
+
}
|
|
420
|
+
|
|
354
421
|
guard let activityTypeStr = config["activityType"] as? String,
|
|
355
422
|
let activity = self.parseActivityType(activityTypeStr),
|
|
356
423
|
let targetConfig = config["target"] as? [String: Any] else {
|
|
@@ -370,6 +437,10 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
370
437
|
}
|
|
371
438
|
|
|
372
439
|
AsyncFunction("schedulePacerWorkout") { (config: [String: Any], date: [String: Any]) async throws -> [String: Any] in
|
|
440
|
+
guard #available(iOS 17.0, *) else {
|
|
441
|
+
throw Exception(name: "Unavailable", description: workoutKitUnavailableMessage)
|
|
442
|
+
}
|
|
443
|
+
|
|
373
444
|
guard let activityTypeStr = config["activityType"] as? String,
|
|
374
445
|
let activity = self.parseActivityType(activityTypeStr),
|
|
375
446
|
let targetConfig = config["target"] as? [String: Any] else {
|
|
@@ -377,23 +448,17 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
377
448
|
}
|
|
378
449
|
|
|
379
450
|
let location = self.parseLocationType(config["locationType"] as? String ?? "outdoor") ?? .outdoor
|
|
380
|
-
_ = config["displayName"] as? String
|
|
381
451
|
|
|
382
452
|
let (distance, time) = try self.parsePacerDistanceAndTime(from: targetConfig)
|
|
383
453
|
let workout = PacerWorkout(activity: activity, location: location, distance: distance, time: time)
|
|
384
454
|
let plan = WorkoutPlan(.pacer(workout))
|
|
385
455
|
|
|
386
456
|
let dateComponents = self.parseDateComponents(from: date)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
"id": plan.id.uuidString
|
|
393
|
-
]
|
|
394
|
-
} catch {
|
|
395
|
-
throw Exception(name: "ScheduleError", description: error.localizedDescription)
|
|
396
|
-
}
|
|
457
|
+
await WorkoutScheduler.shared.schedule(plan, at: dateComponents)
|
|
458
|
+
return [
|
|
459
|
+
"success": true,
|
|
460
|
+
"id": plan.id.uuidString
|
|
461
|
+
]
|
|
397
462
|
}
|
|
398
463
|
|
|
399
464
|
// MARK: - Activity Types
|
|
@@ -443,6 +508,7 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
443
508
|
|
|
444
509
|
// MARK: - Helper Methods
|
|
445
510
|
|
|
511
|
+
@available(iOS 17.0, *)
|
|
446
512
|
private func authorizationStateToString(_ state: WorkoutScheduler.AuthorizationState) -> String {
|
|
447
513
|
switch state {
|
|
448
514
|
case .authorized:
|
|
@@ -451,7 +517,7 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
451
517
|
return "notDetermined"
|
|
452
518
|
case .denied:
|
|
453
519
|
return "denied"
|
|
454
|
-
|
|
520
|
+
default:
|
|
455
521
|
return "unknown"
|
|
456
522
|
}
|
|
457
523
|
}
|
|
@@ -531,6 +597,7 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
531
597
|
}
|
|
532
598
|
}
|
|
533
599
|
|
|
600
|
+
@available(iOS 17.0, *)
|
|
534
601
|
private func parseGoalTypeForValidation(_ type: String) -> WorkoutGoal? {
|
|
535
602
|
switch type.lowercased() {
|
|
536
603
|
case "open": return .open
|
|
@@ -541,6 +608,7 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
541
608
|
}
|
|
542
609
|
}
|
|
543
610
|
|
|
611
|
+
@available(iOS 17.0, *)
|
|
544
612
|
private func parseWorkoutGoal(from config: [String: Any]) throws -> WorkoutGoal {
|
|
545
613
|
guard let type = config["type"] as? String else {
|
|
546
614
|
throw Exception(name: "InvalidGoal", description: "Goal type is required")
|
|
@@ -671,6 +739,7 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
671
739
|
}
|
|
672
740
|
}
|
|
673
741
|
|
|
742
|
+
@available(iOS 17.0, *)
|
|
674
743
|
private func parseStepPurpose(_ purpose: String) -> IntervalStep.Purpose {
|
|
675
744
|
switch purpose.lowercased() {
|
|
676
745
|
case "work": return .work
|
|
@@ -679,7 +748,8 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
679
748
|
}
|
|
680
749
|
}
|
|
681
750
|
|
|
682
|
-
|
|
751
|
+
@available(iOS 17.0, *)
|
|
752
|
+
private func parseWorkoutAlert(from config: [String: Any]) throws -> (any WorkoutAlert)? {
|
|
683
753
|
guard let type = config["type"] as? String else {
|
|
684
754
|
return nil
|
|
685
755
|
}
|
|
@@ -730,7 +800,8 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
730
800
|
return nil
|
|
731
801
|
}
|
|
732
802
|
|
|
733
|
-
|
|
803
|
+
@available(iOS 17.0, *)
|
|
804
|
+
private func paceRangeAlert(minMinutesPerUnit: Double, maxMinutesPerUnit: Double, unit: UnitLength) throws -> (any WorkoutAlert)? {
|
|
734
805
|
guard minMinutesPerUnit > 0, maxMinutesPerUnit > 0 else {
|
|
735
806
|
throw Exception(name: "InvalidAlert", description: "Pace min/max must be > 0")
|
|
736
807
|
}
|
|
@@ -746,6 +817,7 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
746
817
|
return SpeedRangeAlert.speed(lowSpeed...highSpeed, unit: .metersPerSecond)
|
|
747
818
|
}
|
|
748
819
|
|
|
820
|
+
@available(iOS 17.0, *)
|
|
749
821
|
private func validateCustomWorkout(_ workout: CustomWorkout) throws {
|
|
750
822
|
let activity = workout.activity
|
|
751
823
|
let location = workout.location
|
|
@@ -775,6 +847,7 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
775
847
|
}
|
|
776
848
|
}
|
|
777
849
|
|
|
850
|
+
@available(iOS 17.0, *)
|
|
778
851
|
private func buildCustomWorkout(from config: [String: Any]) throws -> CustomWorkout {
|
|
779
852
|
guard let activityTypeStr = config["activityType"] as? String,
|
|
780
853
|
let activity = self.parseActivityType(activityTypeStr) else {
|
|
@@ -812,6 +885,7 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
812
885
|
)
|
|
813
886
|
}
|
|
814
887
|
|
|
888
|
+
@available(iOS 17.0, *)
|
|
815
889
|
private func parseWorkoutStep(from config: [String: Any]) throws -> WorkoutStep {
|
|
816
890
|
let goalConfig = config["goal"] as? [String: Any]
|
|
817
891
|
let goal: WorkoutGoal
|
|
@@ -831,6 +905,7 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
831
905
|
return step
|
|
832
906
|
}
|
|
833
907
|
|
|
908
|
+
@available(iOS 17.0, *)
|
|
834
909
|
private func parseIntervalStep(from config: [String: Any]) throws -> IntervalStep {
|
|
835
910
|
let purposeStr = config["purpose"] as? String ?? "work"
|
|
836
911
|
let purpose = self.parseStepPurpose(purposeStr)
|
|
@@ -848,6 +923,7 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
848
923
|
return step
|
|
849
924
|
}
|
|
850
925
|
|
|
926
|
+
@available(iOS 17.0, *)
|
|
851
927
|
private func parseIntervalBlock(from config: [String: Any]) throws -> IntervalBlock {
|
|
852
928
|
var block = IntervalBlock()
|
|
853
929
|
|
|
@@ -915,11 +991,8 @@ public class ReactNativeWorkoutsModule: Module {
|
|
|
915
991
|
// MARK: - Workout Preview presentation helpers
|
|
916
992
|
|
|
917
993
|
@MainActor
|
|
994
|
+
@available(iOS 17.0, *)
|
|
918
995
|
fileprivate func presentWorkoutPreview(_ plan: WorkoutPlan) async throws {
|
|
919
|
-
guard #available(iOS 17.0, *) else {
|
|
920
|
-
throw Exception(name: "Unavailable", description: "Workout preview requires iOS 17+")
|
|
921
|
-
}
|
|
922
|
-
|
|
923
996
|
guard let viewController = topMostViewController() else {
|
|
924
997
|
throw Exception(name: "NoViewController", description: "Unable to find a view controller to present from")
|
|
925
998
|
}
|
|
@@ -932,9 +1005,27 @@ fileprivate func presentWorkoutPreview(_ plan: WorkoutPlan) async throws {
|
|
|
932
1005
|
}
|
|
933
1006
|
|
|
934
1007
|
fileprivate func topMostViewController() -> UIViewController? {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
let
|
|
1008
|
+
// React Native / Expo apps can temporarily be in `.foregroundInactive` during transitions.
|
|
1009
|
+
// Also, `isKeyWindow` isn't always set early, so we fall back to a normal-level window.
|
|
1010
|
+
let windowScenes = UIApplication.shared.connectedScenes
|
|
1011
|
+
.compactMap { $0 as? UIWindowScene }
|
|
1012
|
+
.filter { scene in
|
|
1013
|
+
switch scene.activationState {
|
|
1014
|
+
case .foregroundActive, .foregroundInactive:
|
|
1015
|
+
return true
|
|
1016
|
+
default:
|
|
1017
|
+
return false
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
let candidateWindows = windowScenes.flatMap { $0.windows }
|
|
1022
|
+
let keyWindow =
|
|
1023
|
+
candidateWindows.first(where: { $0.isKeyWindow }) ??
|
|
1024
|
+
candidateWindows.first(where: { $0.windowLevel == .normal }) ??
|
|
1025
|
+
candidateWindows.first ??
|
|
1026
|
+
UIApplication.shared.windows.first(where: { $0.isKeyWindow }) ??
|
|
1027
|
+
UIApplication.shared.windows.first
|
|
1028
|
+
|
|
938
1029
|
guard let root = keyWindow?.rootViewController else { return nil }
|
|
939
1030
|
return topMostViewController(from: root)
|
|
940
1031
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-workouts",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "React Native Expo module for Apple WorkoutKit - create, preview, and sync custom workouts to Apple Watch",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -46,9 +46,7 @@
|
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=18"
|
|
48
48
|
},
|
|
49
|
-
"dependencies": {
|
|
50
|
-
"expo-build-properties": "~1.0.10"
|
|
51
|
-
},
|
|
49
|
+
"dependencies": {},
|
|
52
50
|
"devDependencies": {
|
|
53
51
|
"@types/react": "~19.1.0",
|
|
54
52
|
"expo": "^54.0.27",
|
|
Binary file
|
|
@@ -7,11 +7,11 @@ import type {
|
|
|
7
7
|
DateComponents,
|
|
8
8
|
LocationType,
|
|
9
9
|
PacerWorkoutConfig,
|
|
10
|
-
SwimBikeRunWorkoutConfig,
|
|
11
10
|
ReactNativeWorkoutsModuleEvents,
|
|
12
11
|
ScheduledWorkout,
|
|
13
12
|
ScheduleResult,
|
|
14
13
|
SingleGoalWorkoutConfig,
|
|
14
|
+
SwimBikeRunWorkoutConfig,
|
|
15
15
|
WorkoutPlan,
|
|
16
16
|
WorkoutValidationResult,
|
|
17
17
|
} from "./ReactNativeWorkouts.types";
|
|
@@ -59,7 +59,7 @@ declare class ReactNativeWorkoutsModule
|
|
|
59
59
|
/**
|
|
60
60
|
* Schedules a custom workout (syncs it to the Apple Watch Workout app).
|
|
61
61
|
*
|
|
62
|
-
* Prefer using `
|
|
62
|
+
* Prefer using `useCustomWorkout(...)` + `plan.scheduleAndSync(...)` for new code.
|
|
63
63
|
*/
|
|
64
64
|
scheduleWorkout(
|
|
65
65
|
config: CustomWorkoutConfig,
|
|
@@ -80,7 +80,7 @@ declare class ReactNativeWorkoutsModule
|
|
|
80
80
|
/**
|
|
81
81
|
* Schedules a single-goal workout (syncs it to the Apple Watch Workout app).
|
|
82
82
|
*
|
|
83
|
-
* Prefer using `
|
|
83
|
+
* Prefer using `useSingleGoalWorkout(...)` + `plan.scheduleAndSync(...)` for new code.
|
|
84
84
|
*/
|
|
85
85
|
scheduleSingleGoalWorkout(
|
|
86
86
|
config: SingleGoalWorkoutConfig,
|
|
@@ -101,7 +101,7 @@ declare class ReactNativeWorkoutsModule
|
|
|
101
101
|
/**
|
|
102
102
|
* Schedules a pacer workout (syncs it to the Apple Watch Workout app).
|
|
103
103
|
*
|
|
104
|
-
* Prefer using `
|
|
104
|
+
* Prefer using `usePacerWorkout(...)` + `plan.scheduleAndSync(...)` for new code.
|
|
105
105
|
*/
|
|
106
106
|
schedulePacerWorkout(
|
|
107
107
|
config: PacerWorkoutConfig,
|