react-native-workouts 0.1.0 → 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/src/hooks.ts ADDED
@@ -0,0 +1,295 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+
3
+ import ReactNativeWorkouts from "./ReactNativeWorkoutsModule";
4
+ import type {
5
+ AuthorizationStatus,
6
+ CustomWorkoutConfig,
7
+ DateComponents,
8
+ PacerWorkoutConfig,
9
+ ScheduledWorkout,
10
+ SingleGoalWorkoutConfig,
11
+ SwimBikeRunWorkoutConfig,
12
+ WorkoutPlan,
13
+ } from "./ReactNativeWorkouts.types";
14
+
15
+ type UseWorkoutPlanResult = {
16
+ /**
17
+ * A stateful `WorkoutPlan` shared object, or `null` when config is null/invalid.
18
+ */
19
+ plan: WorkoutPlan | null;
20
+ /**
21
+ * `true` while the hook is (re)creating the plan in native.
22
+ */
23
+ isLoading: boolean;
24
+ /**
25
+ * Any error that occurred while creating the plan.
26
+ */
27
+ error: Error | null;
28
+ };
29
+
30
+ function useWorkoutPlan<TConfig>(
31
+ config: TConfig | null | undefined,
32
+ createPlan: (config: TConfig) => Promise<WorkoutPlan>,
33
+ ): UseWorkoutPlanResult {
34
+ const [plan, setPlan] = useState<WorkoutPlan | null>(null);
35
+ const [isLoading, setIsLoading] = useState(false);
36
+ const [error, setError] = useState<Error | null>(null);
37
+ const planRef = useRef<WorkoutPlan | null>(null);
38
+
39
+ // We want to recreate the plan when the config meaningfully changes.
40
+ // Consumers should keep config stable (useMemo) for best results.
41
+ const configKey = useMemo(() => JSON.stringify(config ?? null), [config]);
42
+
43
+ useEffect(() => {
44
+ let cancelled = false;
45
+
46
+ const run = async () => {
47
+ if (!config) {
48
+ setPlan(null);
49
+ planRef.current?.release();
50
+ planRef.current = null;
51
+ setError(null);
52
+ setIsLoading(false);
53
+ return;
54
+ }
55
+
56
+ setIsLoading(true);
57
+ setError(null);
58
+
59
+ try {
60
+ const nextPlan = await createPlan(config);
61
+ if (cancelled) {
62
+ // If the effect already cleaned up, ensure we don't leak the native object.
63
+ nextPlan.release();
64
+ return;
65
+ }
66
+
67
+ planRef.current?.release();
68
+ planRef.current = nextPlan;
69
+ setPlan(nextPlan);
70
+ } catch (e) {
71
+ if (!cancelled) {
72
+ setPlan(null);
73
+ planRef.current?.release();
74
+ planRef.current = null;
75
+ setError(e instanceof Error ? e : new Error(String(e)));
76
+ }
77
+ } finally {
78
+ if (!cancelled) {
79
+ setIsLoading(false);
80
+ }
81
+ }
82
+ };
83
+
84
+ void run();
85
+
86
+ return () => {
87
+ cancelled = true;
88
+ planRef.current?.release();
89
+ planRef.current = null;
90
+ };
91
+ }, [configKey, createPlan]);
92
+
93
+ return { plan, isLoading, error };
94
+ }
95
+
96
+ export function useCustomWorkout(
97
+ config: CustomWorkoutConfig | null,
98
+ ): UseWorkoutPlanResult {
99
+ return useWorkoutPlan(config, ReactNativeWorkouts.createCustomWorkoutPlan);
100
+ }
101
+
102
+ export function useSingleGoalWorkout(
103
+ config: SingleGoalWorkoutConfig | null,
104
+ ): UseWorkoutPlanResult {
105
+ return useWorkoutPlan(
106
+ config,
107
+ ReactNativeWorkouts.createSingleGoalWorkoutPlan,
108
+ );
109
+ }
110
+
111
+ export function usePacerWorkout(
112
+ config: PacerWorkoutConfig | null,
113
+ ): UseWorkoutPlanResult {
114
+ return useWorkoutPlan(config, ReactNativeWorkouts.createPacerWorkoutPlan);
115
+ }
116
+
117
+ export function useSwimBikeRunWorkout(
118
+ config: SwimBikeRunWorkoutConfig | null,
119
+ ): UseWorkoutPlanResult {
120
+ return useWorkoutPlan(
121
+ config,
122
+ ReactNativeWorkouts.createSwimBikeRunWorkoutPlan,
123
+ );
124
+ }
125
+
126
+ export type UseWorkoutAuthorizationResult = {
127
+ /**
128
+ * Current authorization status (fetched on mount).
129
+ */
130
+ status: AuthorizationStatus | null;
131
+ isLoading: boolean;
132
+ error: Error | null;
133
+ /**
134
+ * Re-reads the authorization status.
135
+ */
136
+ refresh: () => Promise<AuthorizationStatus>;
137
+ /**
138
+ * Prompts for authorization (if needed) and returns the new status.
139
+ */
140
+ request: () => Promise<AuthorizationStatus>;
141
+ };
142
+
143
+ /**
144
+ * Hook to read/request WorkoutKit authorization.
145
+ */
146
+ export function useWorkoutAuthorization(): UseWorkoutAuthorizationResult {
147
+ const [status, setStatus] = useState<AuthorizationStatus | null>(null);
148
+ const [isLoading, setIsLoading] = useState(false);
149
+ const [error, setError] = useState<Error | null>(null);
150
+
151
+ const refresh = useCallback(async () => {
152
+ setIsLoading(true);
153
+ setError(null);
154
+ try {
155
+ const next = await ReactNativeWorkouts.getAuthorizationStatus();
156
+ setStatus(next);
157
+ return next;
158
+ } catch (e) {
159
+ const err = e instanceof Error ? e : new Error(String(e));
160
+ setError(err);
161
+ throw err;
162
+ } finally {
163
+ setIsLoading(false);
164
+ }
165
+ }, []);
166
+
167
+ const request = useCallback(async () => {
168
+ setIsLoading(true);
169
+ setError(null);
170
+ try {
171
+ const next = await ReactNativeWorkouts.requestAuthorization();
172
+ setStatus(next);
173
+ return next;
174
+ } catch (e) {
175
+ const err = e instanceof Error ? e : new Error(String(e));
176
+ setError(err);
177
+ throw err;
178
+ } finally {
179
+ setIsLoading(false);
180
+ }
181
+ }, []);
182
+
183
+ useEffect(() => {
184
+ void refresh();
185
+ }, [refresh]);
186
+
187
+ return { status, isLoading, error, refresh, request };
188
+ }
189
+
190
+ export type UseScheduledWorkoutsResult = {
191
+ workouts: ScheduledWorkout[];
192
+ isLoading: boolean;
193
+ error: Error | null;
194
+ /**
195
+ * Reloads scheduled workouts from native.
196
+ */
197
+ reload: () => Promise<ScheduledWorkout[]>;
198
+ /**
199
+ * Removes all scheduled workouts created by this app.
200
+ */
201
+ removeAll: () => Promise<void>;
202
+ /**
203
+ * Removes a single scheduled workout by ID.
204
+ */
205
+ remove: (id: string) => Promise<void>;
206
+ /**
207
+ * Schedules (syncs) a plan for the given date components, then reloads the list.
208
+ *
209
+ * Under the hood this calls `plan.scheduleAndSync(date)`.
210
+ */
211
+ schedule: (
212
+ plan: WorkoutPlan,
213
+ date: DateComponents,
214
+ ) => Promise<{ id: string }>;
215
+ };
216
+
217
+ /**
218
+ * Hook to manage scheduled workouts.
219
+ */
220
+ export function useScheduledWorkouts(): UseScheduledWorkoutsResult {
221
+ const [workouts, setWorkouts] = useState<ScheduledWorkout[]>([]);
222
+ const [isLoading, setIsLoading] = useState(false);
223
+ const [error, setError] = useState<Error | null>(null);
224
+
225
+ const reload = useCallback(async () => {
226
+ setIsLoading(true);
227
+ setError(null);
228
+ try {
229
+ const next = await ReactNativeWorkouts.getScheduledWorkouts();
230
+ setWorkouts(next);
231
+ return next;
232
+ } catch (e) {
233
+ const err = e instanceof Error ? e : new Error(String(e));
234
+ setError(err);
235
+ throw err;
236
+ } finally {
237
+ setIsLoading(false);
238
+ }
239
+ }, []);
240
+
241
+ const removeAll = useCallback(async () => {
242
+ setIsLoading(true);
243
+ setError(null);
244
+ try {
245
+ await ReactNativeWorkouts.removeAllScheduledWorkouts();
246
+ setWorkouts([]);
247
+ } catch (e) {
248
+ const err = e instanceof Error ? e : new Error(String(e));
249
+ setError(err);
250
+ throw err;
251
+ } finally {
252
+ setIsLoading(false);
253
+ }
254
+ }, []);
255
+
256
+ const remove = useCallback(async (id: string) => {
257
+ setIsLoading(true);
258
+ setError(null);
259
+ try {
260
+ await ReactNativeWorkouts.removeScheduledWorkout(id);
261
+ setWorkouts((prev) => prev.filter((w) => w.id !== id));
262
+ } catch (e) {
263
+ const err = e instanceof Error ? e : new Error(String(e));
264
+ setError(err);
265
+ throw err;
266
+ } finally {
267
+ setIsLoading(false);
268
+ }
269
+ }, []);
270
+
271
+ const schedule = useCallback(
272
+ async (plan: WorkoutPlan, date: DateComponents) => {
273
+ setIsLoading(true);
274
+ setError(null);
275
+ try {
276
+ const result = await plan.scheduleAndSync(date);
277
+ await reload();
278
+ return { id: result.id };
279
+ } catch (e) {
280
+ const err = e instanceof Error ? e : new Error(String(e));
281
+ setError(err);
282
+ throw err;
283
+ } finally {
284
+ setIsLoading(false);
285
+ }
286
+ },
287
+ [reload],
288
+ );
289
+
290
+ useEffect(() => {
291
+ void reload();
292
+ }, [reload]);
293
+
294
+ return { workouts, isLoading, error, reload, removeAll, remove, schedule };
295
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
- export { default } from './ReactNativeWorkoutsModule';
2
- export * from './ReactNativeWorkouts.types';
1
+ export { default } from "./ReactNativeWorkoutsModule";
2
+ export * from "./ReactNativeWorkouts.types";
3
+ export * from "./hooks";