feat(workout-observer): bypass @capgo via requestAllHealthPermissions

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<HKObjectType> 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.
This commit is contained in:
Sylvain Bettinelli
2026-05-18 16:35:42 +00:00
parent 1393cc3dcf
commit f315c5471e

View File

@@ -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<HKObjectType>()
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.