fix(watch-connectivity): improve WCSession lifecycle and message reliability

This commit is contained in:
2026-08-07 15:32:40 +02:00
parent e64e2353d9
commit 00a8220096
8 changed files with 266 additions and 71 deletions
@@ -23,8 +23,9 @@ export function PreSwimScreen() {
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const route = useRoute<RouteProp<RootStackParamList, "PreSwim">>();
const [workout, setWorkout] = useState<WorkoutWithSets | null>(null);
const [syncResult, setSyncResult] = useState<"sent" | "error" | null>(null);
const { isSyncing, sendWorkoutToWatch } = useSyncWorkout();
const { status, isPaired, isInstalled } = useWatchConnection();
const { status, isPaired, isInstalled, isReady } = useWatchConnection();
useEffect(() => {
loadWorkout();
@@ -38,7 +39,7 @@ export function PreSwimScreen() {
const handleStartSwim = async () => {
if (!workout) return;
if (status !== "connected") {
if (!isReady) {
navigation.navigate("PostSwim", { workoutId: route.params.workoutId });
return;
}
@@ -51,11 +52,11 @@ export function PreSwimScreen() {
);
if ("error" in result) {
navigation.navigate("PostSwim", { workoutId: route.params.workoutId });
setSyncResult("error");
return;
}
navigation.navigate("PostSwim", { workoutId: route.params.workoutId });
setSyncResult("sent");
};
if (!workout) {
@@ -66,6 +67,46 @@ export function PreSwimScreen() {
);
}
if (syncResult === "sent") {
return (
<View className="flex-1 bg-white p-4 items-center justify-center">
<View className="bg-green-50 rounded-2xl p-6 items-center mb-6 w-full">
<Text className="text-4xl mb-3">⌚</Text>
<Text className="text-xl font-bold text-green-800 mb-2">Workout sent to Watch!</Text>
<Text className="text-green-600 text-center">
Open the SwimBuddy app on your Apple Watch to start tracking.
</Text>
</View>
<Pressable
onPress={() => navigation.popToTop()}
className="bg-slate-900 rounded-2xl py-4 items-center w-full"
>
<Text className="text-white font-bold text-lg">Done</Text>
</Pressable>
</View>
);
}
if (syncResult === "error") {
return (
<View className="flex-1 bg-white p-4 items-center justify-center">
<View className="bg-red-50 rounded-2xl p-6 items-center mb-6 w-full">
<Text className="text-4xl mb-3">⚠️</Text>
<Text className="text-xl font-bold text-red-800 mb-2">Failed to send</Text>
<Text className="text-red-600 text-center">
Could not send workout to Watch. Starting without Watch instead.
</Text>
</View>
<Pressable
onPress={() => navigation.navigate("PostSwim", { workoutId: route.params.workoutId })}
className="bg-sky-500 rounded-2xl py-4 items-center w-full"
>
<Text className="text-white font-bold text-lg">Start Without Watch</Text>
</Pressable>
</View>
);
}
return (
<View className="flex-1 bg-white p-4">
<View className="items-center mb-6">
@@ -112,14 +153,16 @@ export function PreSwimScreen() {
<View className="bg-slate-50 rounded-2xl p-3 mb-6 flex-row items-center gap-3">
<View
className={`w-3 h-3 rounded-full ${
status === "connected" ? "bg-green-500" : "bg-slate-300"
isReady ? "bg-green-500" : isPaired ? "bg-yellow-500" : "bg-slate-300"
}`}
/>
<Text className="text-sm text-slate-600">
{status === "connected"
? "Apple Watch connected"
{isReady
? "Apple Watch ready"
: isPaired
? "Watch paired but not connected"
? isInstalled
? "Watch paired but not in range"
: "Watch paired, app not installed"
: "No Watch paired"}
</Text>
</View>
@@ -135,14 +178,14 @@ export function PreSwimScreen() {
<ActivityIndicator color="#fff" />
) : (
<Text className="text-white font-bold text-lg">
{status === "connected" ? "Send to Watch & Start" : "Start Without Watch"}
{isReady ? "Send to Watch & Start" : "Start Without Watch"}
</Text>
)}
</Pressable>
<Text className="text-slate-400 text-xs text-center mt-3">
{status === "connected"
? "Workout will be sent to your Apple Watch"
{isReady
? "Workout will be synced to your Apple Watch"
: "Workout will be tracked on this device"}
</Text>
</View>
@@ -15,6 +15,20 @@ export function useSyncWorkout() {
): Promise<SyncResult> => {
setIsSyncing(true);
try {
console.log("[useSyncWorkout] Checking module availability...");
const supported = await WatchConnectivity.isSupported();
console.log("[useSyncWorkout] isSupported:", supported);
const paired = await WatchConnectivity.isPaired();
console.log("[useSyncWorkout] isPaired:", paired);
const installed = await WatchConnectivity.isWatchAppInstalled();
console.log("[useSyncWorkout] isWatchAppInstalled:", installed);
const reachable = await WatchConnectivity.isReachable();
console.log("[useSyncWorkout] isReachable:", reachable);
const plan = {
id: workoutId,
name: name ?? "Workout",
@@ -29,9 +43,12 @@ export function useSyncWorkout() {
})),
};
console.log("[useSyncWorkout] Sending plan:", JSON.stringify(plan).slice(0, 200));
const result = await WatchConnectivity.sendWorkoutPlan(plan);
console.log("[useSyncWorkout] Result:", JSON.stringify(result));
return result as SyncResult;
} catch (error: any) {
console.log("[useSyncWorkout] Error:", error.message, error.code);
return { error: error.message ?? "Failed to sync" };
} finally {
setIsSyncing(false);
@@ -28,11 +28,13 @@ export function useWatchConnection() {
setIsPaired(paired);
setIsInstalled(installed);
setStatus(reachable ? "connected" : "disconnected");
setStatus(reachable ? "connected" : paired ? "disconnected" : "disconnected");
} catch {
setStatus("disconnected");
}
};
return { status, isPaired, isInstalled };
const isReady = isPaired && isInstalled;
return { status, isPaired, isInstalled, isReady };
}