Files
SwimBuddy/modules/watch-connectivity/index.swift
T

198 lines
7.4 KiB
Swift

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]) {
print("[WatchConnectivity] Received message: \(message["type"] ?? "unknown")")
onMessageReceived?(message)
}
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)
}
}
}