193 lines
7.2 KiB
Swift
193 lines
7.2 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], 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)
|
|
}
|
|
}
|
|
}
|