feat(workout-observer): debug status + test notif + timeSensitive
Instrumentation pour debug visible côté JS sans Console.app.
Nouvelles méthodes exposées :
- getStatus() → { observerActive, bgDeliveryEnabled, fireCount,
lastFireAt, lastNotifAt, lastWorkoutUUID, lastError,
healthDataAvailable }
- testNotification() → force une notif locale immédiate via
UNUserNotificationCenter pour valider le pipeline notif natif,
indépendant de l'observer HealthKit.
Persistance UserDefaults :
- fireCount incrémenté à chaque observer.update callback
- lastFireAt / lastNotifAt / lastError timestamps ISO8601
- observerActive / bgDeliveryEnabled bools pour état courant
Fix notif :
- interruptionLevel = .timeSensitive (iOS 15+) sur les notifs
"Séance terminée" pour qu'iOS les affiche même en foreground app
ou Focus modéré. Sans ça les bannières peuvent être suppress
silencieusement par iOS.
Bump version requis côté Mac avant rebuild TestFlight.
This commit is contained in:
@@ -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<Void, Error>) 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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user