import ExpoModulesCore import WatchConnectivity private class SessionDelegate: NSObject, WCSessionDelegate { var onActivationComplete: ((WCSessionActivationState) -> Void)? var onMessageReceived: (([String: Any]) -> Void)? var onUserInfoReceived: (([String: Any]) -> Void)? func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { if let error = error { print("[WatchConnectivity] Activation failed: \(error)") } else { print("[WatchConnectivity] Activation complete: state=\(activationState.rawValue)") } onActivationComplete?(activationState) } func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) { print("[WatchConnectivity] Received message with reply: \(message["type"] ?? "unknown")") replyHandler(["status": "received"]) onMessageReceived?(message) } func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) { print("[WatchConnectivity] Received userInfo: \(userInfo["type"] ?? "unknown")") onUserInfoReceived?(userInfo) } func sessionDidBecomeInactive(_ session: WCSession) {} func sessionDidDeactivate(_ session: WCSession) { session.activate() } } public class WatchConnectivityModule: Module { private var session: WCSession? private let delegate = SessionDelegate() private var isActivated = false private var pendingPromises: [(() -> Void)] = [] private var pendingEvents: [[String: Any]] = [] // WCSession only supports NSString, NSNumber, NSData, NSArray, NSDictionary private func cleanForWCSession(_ obj: Any) -> Any { if let dict = obj as? [String: Any] { var cleaned: [String: Any] = [:] for (key, value) in dict { if value is NSNull { continue } cleaned[key] = cleanForWCSession(value) } return cleaned } else if let arr = obj as? [Any] { return arr.filter { !($0 is NSNull) }.map { cleanForWCSession($0) } } return obj } public func definition() -> ModuleDefinition { Name("WatchConnectivity") OnCreate { guard WCSession.isSupported() else { return } let session = WCSession.default session.delegate = self.delegate self.delegate.onActivationComplete = { [weak self] state in DispatchQueue.main.async { self?.isActivated = (state == .activated) print("[WatchConnectivity] isActivated = \(self?.isActivated ?? false)") let pending = self?.pendingPromises ?? [] self?.pendingPromises = [] pending.forEach { $0() } } } self.delegate.onMessageReceived = { [weak self] message in self?.handleIncomingMessage(message) } self.delegate.onUserInfoReceived = { [weak self] userInfo in self?.handleIncomingMessage(userInfo) } session.activate() self.session = session } AsyncFunction("isSupported") { () -> Bool in return WCSession.isSupported() } AsyncFunction("isPaired") { () -> Bool in guard let session = self.session else { return false } return session.isPaired } AsyncFunction("isWatchAppInstalled") { () -> Bool in guard let session = self.session else { return false } return session.isWatchAppInstalled } AsyncFunction("isReachable") { () -> Bool in guard let session = self.session else { return false } return session.isReachable } AsyncFunction("sendWorkoutPlan") { (plan: [String: Any], promise: Promise) in guard let session = self.session else { print("[WatchConnectivity] sendWorkoutPlan: NO_SESSION") promise.reject("NO_SESSION", "WCSession not available") return } func doSend() { var message = plan message["type"] = "workoutPlan" print("[WatchConnectivity] sendWorkoutPlan: isReachable=\(session.isReachable), activationState=\(session.activationState.rawValue)") guard let cleaned = self.cleanForWCSession(message) as? [String: Any] else { promise.resolve(["status": "sent"]) return } session.sendMessage(cleaned, replyHandler: nil, errorHandler: { error in print("[WatchConnectivity] sendMessage failed: \(error), falling back to transferUserInfo") session.transferUserInfo(cleaned) }) promise.resolve(["status": "sent"]) } if session.activationState == .activated { doSend() } else { print("[WatchConnectivity] Session not activated yet (state=\(session.activationState.rawValue)), queuing...") self.pendingPromises.append(doSend) } } AsyncFunction("sendWorkoutCompletion") { (data: [String: Any], promise: Promise) in guard let session = self.session else { promise.reject("NO_SESSION", "WCSession not available") return } func doSend() { var message = data message["type"] = "workoutCompleted" guard let cleaned = self.cleanForWCSession(message) as? [String: Any] else { promise.resolve(["status": "sent"]) return } if session.isReachable { session.sendMessage(cleaned, replyHandler: nil, errorHandler: { error in print("[WatchConnectivity] sendMessage failed: \(error), falling back to transferUserInfo") session.transferUserInfo(cleaned) }) } else { session.transferUserInfo(cleaned) } promise.resolve(["status": "sent"]) } if self.isActivated { doSend() } else { self.pendingPromises.append(doSend) } } AsyncFunction("getApplicationContext") { () -> [String: Any]? in return self.session?.receivedApplicationContext as? [String: Any] } AsyncFunction("updateApplicationContext") { (context: [String: Any]) in try? self.session?.updateApplicationContext(context) } AsyncFunction("pollEvents") { () -> [[String: Any]] in let events = self.pendingEvents self.pendingEvents = [] return events } } private func handleIncomingMessage(_ message: [String: Any]) { guard let type = message["type"] as? String else { return } print("[WatchConnectivity] handleIncomingMessage type=\(type)") DispatchQueue.main.async { self.pendingEvents.append(message) } } }