From f315c5471edd507fbdc492f10633a8c2c3166332 Mon Sep 17 00:00:00 2001 From: Sylvain Bettinelli Date: Mon, 18 May 2026 16:35:42 +0000 Subject: [PATCH] feat(workout-observer): bypass @capgo via requestAllHealthPermissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le plugin tiers @capgo/capacitor-health v8.4.9 a un bug iOS 26 : Health.requestAuthorization pend indéfiniment (Promise jamais résolue, confirmé empiriquement 2026-05-18 — timeout 8s même avec 1 seul type 'steps' + désinstall/réinstall complet inefficace). Workaround : nouvelle méthode requestAllHealthPermissions exposée sur notre plugin custom CoachWorkoutObserver (qui fonctionne empiriquement — fireCount=10 confirme que HealthKit native marche). Appelle directement HKHealthStore.requestAuthorization avec un Set construit en Swift (14 quantity + 1 category + workout). Pas de string→enum mapping intermédiaire, pas de parsing TypeScript→Swift, donc pas de chaîne où ça pourrait silently fail comme dans @capgo. Bump build requis côté Mac avant rebuild TestFlight build 22. --- ios/App/App/CoachWorkoutObserver.swift | 68 ++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/ios/App/App/CoachWorkoutObserver.swift b/ios/App/App/CoachWorkoutObserver.swift index a166af7..279b3c8 100644 --- a/ios/App/App/CoachWorkoutObserver.swift +++ b/ios/App/App/CoachWorkoutObserver.swift @@ -36,6 +36,7 @@ public class CoachWorkoutObserverPlugin: CAPPlugin, CAPBridgedPlugin { CAPPluginMethod(name: "getStatus", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "testNotification", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "resetAnchor", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "requestAllHealthPermissions", returnType: CAPPluginReturnPromise), ] // Clés UserDefaults pour debug visible côté JS via getStatus() @@ -103,6 +104,73 @@ public class CoachWorkoutObserverPlugin: CAPPlugin, CAPBridgedPlugin { ]) } + // Bypass complet de @capgo/capacitor-health pour requestAuthorization. + // Le plugin tiers semble pendre indéfiniment sur iOS 26 (Promise jamais + // résolue, confirmé empiriquement 2026-05-18 avec timeout 8s même + // avec 1 seul type 'steps'). On appelle directement HKHealthStore + // depuis notre plugin custom qui fonctionne (CoachWorkoutObserver + // a fireCount=10 = preuve que HealthKit native marche pour cette app). + @objc func requestAllHealthPermissions(_ call: CAPPluginCall) { + guard HKHealthStore.isHealthDataAvailable() else { + call.reject("HealthKit not available on this device") + return + } + + // Types lus par coach.hypnotruck.ch — alignés sur READ_SCOPES JS + var readTypes = Set() + + let quantityIds: [HKQuantityTypeIdentifier] = [ + .stepCount, + .heartRate, + .restingHeartRate, + .heartRateVariabilitySDNN, + .bodyMass, + .height, + .bodyFatPercentage, + .oxygenSaturation, + .distanceWalkingRunning, + .distanceCycling, + .activeEnergyBurned, + .basalEnergyBurned, + .appleExerciseTime, + .flightsClimbed, + ] + for id in quantityIds { + if let t = HKObjectType.quantityType(forIdentifier: id) { + readTypes.insert(t) + } + } + + let categoryIds: [HKCategoryTypeIdentifier] = [ + .sleepAnalysis, + ] + for id in categoryIds { + if let t = HKObjectType.categoryType(forIdentifier: id) { + readTypes.insert(t) + } + } + + readTypes.insert(HKObjectType.workoutType()) + + let totalTypes = readTypes.count + NSLog("[CoachWorkoutObserver] requestAllHealthPermissions: requesting %d types", totalTypes) + + healthStore.requestAuthorization(toShare: nil, read: readTypes) { success, error in + DispatchQueue.main.async { + if let error = error { + NSLog("[CoachWorkoutObserver] requestAuthorization error: %@", error.localizedDescription) + call.reject("requestAuthorization failed: \(error.localizedDescription)") + } else { + NSLog("[CoachWorkoutObserver] requestAuthorization OK success=%@", success ? "true" : "false") + call.resolve([ + "success": success, + "typesRequested": totalTypes, + ]) + } + } + } + } + // Debug — reset l'anchor stocké. Au prochain observer fire, on // récupèrera TOUS les workouts (initial sync). Utile pour tester // le pipeline avec un workout pré-existant.