diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index c3a268a..99591cf 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 01468471ECA28EA66AFE2D6C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0EB6C76F68679844EC72178F /* Foundation.framework */; }; + 21B242F8E61FB07C476C477C /* CoachLiveBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DF3346C49A690C3C3DF2F3 /* CoachLiveBridge.swift */; }; 290D0F17978B7EF3DDB3A792 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A06CDFF5B4F7852B6329862 /* ContentView.swift */; }; 2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; }; 3E6740C521EF14892687BCF7 /* CoachWatch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 2517374227A4072A4D6AFC15 /* CoachWatch.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -71,6 +72,7 @@ 8AC9C9B9971F55027D227A06 /* WorkoutManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WorkoutManager.swift; sourceTree = ""; }; 958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; }; A420239FC6D065C8A599B61C /* ConnectivityManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConnectivityManager.swift; sourceTree = ""; }; + A9DF3346C49A690C3C3DF2F3 /* CoachLiveBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoachLiveBridge.swift; sourceTree = ""; }; C0AC4A07811751FB78AA0002 /* CoachAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachAuth.swift; sourceTree = ""; }; D09B589FA572B2C14E8CD835 /* CoachWatch.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = CoachWatch.entitlements; sourceTree = ""; }; D1BD5B1990CF3F2B9C5D0002 /* CoachWorkoutKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachWorkoutKit.swift; sourceTree = ""; }; @@ -144,6 +146,7 @@ 504EC3131FED79650016851F /* Info.plist */, 2FAD9762203C412B000D30F8 /* config.xml */, 50B271D01FEDC1A000F3C39B /* public */, + A9DF3346C49A690C3C3DF2F3 /* CoachLiveBridge.swift */, ); path = App; sourceTree = ""; @@ -284,6 +287,7 @@ F1ABCDEF0123456789AB0001 /* CoachHealthRoute.swift in Sources */, 88F28EB12FBB330E00E8306E /* CoachWorkoutObserver.swift in Sources */, E2CE6C2AA1DF4F3CAD6E0001 /* MainViewController.swift in Sources */, + 21B242F8E61FB07C476C477C /* CoachLiveBridge.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/App/App/CoachLiveBridge.swift b/ios/App/App/CoachLiveBridge.swift new file mode 100644 index 0000000..bb1fed8 --- /dev/null +++ b/ios/App/App/CoachLiveBridge.swift @@ -0,0 +1,80 @@ +// CoachLiveBridge.swift +// Plugin Capacitor (côté iPhone) : reçoit les samples de séance poussés par +// l'app watchOS CoachWatch via WatchConnectivity et les relaie au JS via +// l'event "liveSample" (consommé par la page web /live). +// +// Enregistrement : MainViewController.capacitorDidLoad() -> +// bridge?.registerPluginInstance(CoachLiveBridgePlugin()) +// +// Usage JS : +// const CoachLiveBridge = registerPlugin('CoachLiveBridge'); +// CoachLiveBridge.addListener('liveSample', (s) => { ... }); +// // s.heartRate, s.activeEnergyKcal, s.distanceMeters, s.elapsedSec, s.ts + +import Foundation +import Capacitor +import WatchConnectivity + +@objc(CoachLiveBridgePlugin) +public class CoachLiveBridgePlugin: CAPPlugin, CAPBridgedPlugin { + public let identifier = "CoachLiveBridgePlugin" + public let jsName = "CoachLiveBridge" + public let pluginMethods: [CAPPluginMethod] = [ + CAPPluginMethod(name: "getStatus", returnType: CAPPluginReturnPromise) + ] + + override public func load() { + super.load() + guard WCSession.isSupported() else { + CAPLog.print("⚡️ CoachLiveBridge: WCSession non supporté sur cet appareil") + return + } + let session = WCSession.default + session.delegate = self + session.activate() + } + + @objc func getStatus(_ call: CAPPluginCall) { + let session = WCSession.default + call.resolve([ + "supported": WCSession.isSupported(), + "activated": session.activationState == .activated, + "paired": session.isPaired, + "watchAppInstalled": session.isWatchAppInstalled, + "reachable": session.isReachable + ]) + } + + private func emit(_ sample: [String: Any]) { + DispatchQueue.main.async { [weak self] in + self?.notifyListeners("liveSample", data: sample) + } + } +} + +extension CoachLiveBridgePlugin: WCSessionDelegate { + public func session(_ session: WCSession, + activationDidCompleteWith activationState: WCSessionActivationState, + error: Error?) {} + + public func sessionDidBecomeInactive(_ session: WCSession) {} + + public func sessionDidDeactivate(_ session: WCSession) { + // Réactiver pour les sessions futures (ex. changement d'Apple Watch). + WCSession.default.activate() + } + + // Temps réel : un sample envoyé via sendMessage. + public func session(_ session: WCSession, didReceiveMessage message: [String: Any]) { + emit(message) + } + + // Livraison différée : lot de samples envoyé via transferUserInfo. + public func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) { + if let samples = userInfo["samples"] as? [[String: Any]] { + for sample in samples { emit(sample) } + } else { + emit(userInfo) + } + } +} diff --git a/ios/App/App/MainViewController.swift b/ios/App/App/MainViewController.swift index 8d94cb2..f0e5737 100644 --- a/ios/App/App/MainViewController.swift +++ b/ios/App/App/MainViewController.swift @@ -18,6 +18,10 @@ class MainViewController: CAPBridgeViewController { // Plugin natif observer HealthKit — détecte fin de séance Watch et // push notif locale "🎉 Séance terminée" (cf. CoachWorkoutObserver.swift). bridge?.registerPluginInstance(CoachWorkoutObserverPlugin()) + // Plugin natif live workout — reçoit les samples temps réel de l'app + // watchOS CoachWatch via WatchConnectivity, relaie au JS (event + // "liveSample") pour la page /live (cf. CoachLiveBridge.swift). + bridge?.registerPluginInstance(CoachLiveBridgePlugin()) } override func viewDidLoad() {