diff --git a/ios/App/App/CoachWorkoutObserver.swift b/ios/App/App/CoachWorkoutObserver.swift index 54b13de..eaa297a 100644 --- a/ios/App/App/CoachWorkoutObserver.swift +++ b/ios/App/App/CoachWorkoutObserver.swift @@ -33,8 +33,18 @@ public class CoachWorkoutObserverPlugin: CAPPlugin, CAPBridgedPlugin { public let pluginMethods: [CAPPluginMethod] = [ CAPPluginMethod(name: "startObserving", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "stopObserving", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "getStatus", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "testNotification", returnType: CAPPluginReturnPromise), ] + // Clés UserDefaults pour debug visible côté JS via getStatus() + private let kFireCount = "CoachWorkoutObserver.fireCount" + private let kLastFireAt = "CoachWorkoutObserver.lastFireAt" + private let kLastNotifAt = "CoachWorkoutObserver.lastNotifAt" + private let kLastError = "CoachWorkoutObserver.lastError" + private let kBgDeliveryEnabled = "CoachWorkoutObserver.bgDeliveryEnabled" + private let kObserverActive = "CoachWorkoutObserver.observerActive" + private let healthStore = HKHealthStore() private var observerQuery: HKObserverQuery? @@ -65,11 +75,54 @@ public class CoachWorkoutObserverPlugin: CAPPlugin, CAPBridgedPlugin { healthStore.stop(q) observerQuery = nil } + UserDefaults.standard.set(false, forKey: kObserverActive) let workoutType = HKObjectType.workoutType() healthStore.disableBackgroundDelivery(for: workoutType) { _, _ in } call.resolve(["observing": false]) } + // Debug — expose les compteurs persistés pour visualiser ce que fait + // l'observer sans Console.app. Appelé depuis page /settings ou /debug. + @objc func getStatus(_ call: CAPPluginCall) { + let d = UserDefaults.standard + call.resolve([ + "observerActive": d.bool(forKey: kObserverActive), + "bgDeliveryEnabled": d.bool(forKey: kBgDeliveryEnabled), + "fireCount": d.integer(forKey: kFireCount), + "lastFireAt": d.string(forKey: kLastFireAt) ?? "", + "lastNotifAt": d.string(forKey: kLastNotifAt) ?? "", + "lastWorkoutUUID": d.string(forKey: lastNotifiedKey) ?? "", + "lastError": d.string(forKey: kLastError) ?? "", + "healthDataAvailable": HKHealthStore.isHealthDataAvailable(), + ]) + } + + // Debug — force une notif locale immédiate pour valider que le pipeline + // notif Apple fonctionne, indépendamment de l'observer HealthKit. + @objc func testNotification(_ call: CAPPluginCall) { + let content = UNMutableNotificationContent() + content.title = "🧪 Test CoachWorkoutObserver" + content.body = "Si tu vois ça, le pipeline notif natif fonctionne." + content.sound = .default + if #available(iOS 15.0, *) { + content.interruptionLevel = .timeSensitive + } + let req = UNNotificationRequest( + identifier: "coach_wo_test_\(Int(Date().timeIntervalSince1970))", + content: content, + trigger: nil + ) + UNUserNotificationCenter.current().add(req) { error in + DispatchQueue.main.async { + if let error = error { + call.reject("Test notif failed: \(error.localizedDescription)") + } else { + call.resolve(["scheduled": true]) + } + } + } + } + // MARK: - HealthKit setup private func requestPermissionsIfNeeded() async throws { @@ -88,10 +141,12 @@ public class CoachWorkoutObserverPlugin: CAPPlugin, CAPBridgedPlugin { private func enableBackgroundDelivery() async throws { let workoutType = HKObjectType.workoutType() try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in - healthStore.enableBackgroundDelivery(for: workoutType, frequency: .immediate) { ok, error in + healthStore.enableBackgroundDelivery(for: workoutType, frequency: .immediate) { [weak self] ok, error in if let error = error { + UserDefaults.standard.set("bg delivery: \(error.localizedDescription)", forKey: self?.kLastError ?? "") cont.resume(throwing: error) } else { + UserDefaults.standard.set(ok, forKey: self?.kBgDeliveryEnabled ?? "") NSLog("[CoachWorkoutObserver] background delivery enabled=%@", ok ? "true" : "false") cont.resume() } @@ -106,18 +161,25 @@ public class CoachWorkoutObserverPlugin: CAPPlugin, CAPBridgedPlugin { } let workoutType = HKObjectType.workoutType() let query = HKObserverQuery(sampleType: workoutType, predicate: nil) { [weak self] _, completion, error in + guard let self = self else { completion(); return } if let error = error { + UserDefaults.standard.set("observer: \(error.localizedDescription)", forKey: self.kLastError) NSLog("[CoachWorkoutObserver] query error: %@", error.localizedDescription) completion() return } + // Persiste compteur + timestamp pour debug visible via getStatus + let d = UserDefaults.standard + d.set(d.integer(forKey: self.kFireCount) + 1, forKey: self.kFireCount) + d.set(ISO8601DateFormatter().string(from: Date()), forKey: self.kLastFireAt) NSLog("[CoachWorkoutObserver] observer fired") - self?.fetchLatestWorkout { + self.fetchLatestWorkout { completion() // signaler iOS qu'on a fini de traiter (mandatory) } } healthStore.execute(query) observerQuery = query + UserDefaults.standard.set(true, forKey: kObserverActive) NSLog("[CoachWorkoutObserver] observer query started") } @@ -174,16 +236,25 @@ public class CoachWorkoutObserverPlugin: CAPPlugin, CAPBridgedPlugin { content.title = "🎉 Séance \(typeName) terminée" content.body = "Bravo ! \(body)" content.sound = .default + // .timeSensitive : force iOS à afficher la notif même en Focus modéré + // et en foreground app. Sans ça la notif peut être suppress + // silencieusement. + if #available(iOS 15.0, *) { + content.interruptionLevel = .timeSensitive + } let request = UNNotificationRequest( identifier: "coach_workout_done_\(workout.uuid.uuidString)", content: content, trigger: nil // déclenchement immédiat ) - UNUserNotificationCenter.current().add(request) { error in + UNUserNotificationCenter.current().add(request) { [weak self] error in + guard let self = self else { return } if let error = error { + UserDefaults.standard.set("notif: \(error.localizedDescription)", forKey: self.kLastError) NSLog("[CoachWorkoutObserver] notif scheduling failed: %@", error.localizedDescription) } else { + UserDefaults.standard.set(ISO8601DateFormatter().string(from: Date()), forKey: self.kLastNotifAt) NSLog("[CoachWorkoutObserver] notif scheduled for workout %@", workout.uuid.uuidString) } }