fix(watch-connectivity): improve WCSession lifecycle and message reliability
This commit is contained in:
@@ -1,64 +1,158 @@
|
||||
import ExpoModulesCore
|
||||
import WatchConnectivity
|
||||
|
||||
private class SessionDelegate: NSObject, WCSessionDelegate {
|
||||
var onActivationComplete: ((WCSessionActivationState) -> 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 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)] = []
|
||||
|
||||
// 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() }
|
||||
}
|
||||
}
|
||||
session.activate()
|
||||
self.session = session
|
||||
}
|
||||
|
||||
AsyncFunction("isSupported") { () -> Bool in
|
||||
return WCSession.isSupported()
|
||||
}
|
||||
|
||||
AsyncFunction("isPaired") { () -> Bool in
|
||||
return WCSession.default.isPaired
|
||||
guard let session = self.session else { return false }
|
||||
return session.isPaired
|
||||
}
|
||||
|
||||
AsyncFunction("isWatchAppInstalled") { () -> Bool in
|
||||
return WCSession.default.isWatchAppInstalled
|
||||
guard let session = self.session else { return false }
|
||||
return session.isWatchAppInstalled
|
||||
}
|
||||
|
||||
AsyncFunction("isReachable") { () -> Bool in
|
||||
return WCSession.default.isReachable
|
||||
guard let session = self.session else { return false }
|
||||
return session.isReachable
|
||||
}
|
||||
|
||||
AsyncFunction("sendWorkoutPlan") { (plan: [String: Any], promise: Promise) in
|
||||
var message = plan
|
||||
message["type"] = "workoutPlan"
|
||||
guard let session = self.session else {
|
||||
print("[WatchConnectivity] sendWorkoutPlan: NO_SESSION")
|
||||
promise.reject("NO_SESSION", "WCSession not available")
|
||||
return
|
||||
}
|
||||
|
||||
if WCSession.default.isReachable {
|
||||
WCSession.default.sendMessage(message, replyHandler: { reply in
|
||||
promise.resolve(reply)
|
||||
}, errorHandler: { error in
|
||||
promise.reject(error)
|
||||
func doSend() {
|
||||
var message = plan
|
||||
message["type"] = "workoutPlan"
|
||||
|
||||
print("[WatchConnectivity] sendWorkoutPlan: isReachable=\(session.isReachable), activationState=\(session.activationState.rawValue)")
|
||||
|
||||
// WCSession only supports NSString, NSNumber, NSData, NSArray, NSDictionary
|
||||
// Remove NSNull values (from JS null/undefined)
|
||||
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"])
|
||||
}
|
||||
|
||||
// Check actual session state, not our flag (which has a race condition)
|
||||
if session.activationState == .activated {
|
||||
doSend()
|
||||
} else {
|
||||
WCSession.default.transferUserInfo(message)
|
||||
promise.resolve(["status": "queued"])
|
||||
print("[WatchConnectivity] Session not activated yet (state=\(session.activationState.rawValue)), queuing...")
|
||||
self.pendingPromises.append(doSend)
|
||||
}
|
||||
}
|
||||
|
||||
AsyncFunction("sendWorkoutCompletion") { (data: [String: Any], promise: Promise) in
|
||||
var message = data
|
||||
message["type"] = "workoutCompleted"
|
||||
guard let session = self.session else {
|
||||
promise.reject("NO_SESSION", "WCSession not available")
|
||||
return
|
||||
}
|
||||
|
||||
if WCSession.default.isReachable {
|
||||
WCSession.default.sendMessage(message, replyHandler: { reply in
|
||||
promise.resolve(reply)
|
||||
}, errorHandler: { error in
|
||||
promise.reject(error)
|
||||
})
|
||||
func doSend() {
|
||||
var message = data
|
||||
message["type"] = "workoutCompleted"
|
||||
|
||||
if session.isReachable {
|
||||
session.sendMessage(message, replyHandler: { reply in
|
||||
promise.resolve(reply)
|
||||
}, errorHandler: { error in
|
||||
print("[WatchConnectivity] sendMessage failed, falling back to transferUserInfo: \(error)")
|
||||
session.transferUserInfo(message)
|
||||
promise.resolve(["status": "queued"])
|
||||
})
|
||||
} else {
|
||||
session.transferUserInfo(message)
|
||||
promise.resolve(["status": "queued"])
|
||||
}
|
||||
}
|
||||
|
||||
if self.isActivated {
|
||||
doSend()
|
||||
} else {
|
||||
WCSession.default.transferUserInfo(message)
|
||||
promise.resolve(["status": "queued"])
|
||||
self.pendingPromises.append(doSend)
|
||||
}
|
||||
}
|
||||
|
||||
AsyncFunction("getApplicationContext") { () -> [String: Any]? in
|
||||
return WCSession.default.receivedApplicationContext as [String: Any]
|
||||
return self.session?.receivedApplicationContext as? [String: Any]
|
||||
}
|
||||
|
||||
AsyncFunction("updateApplicationContext") { (context: [String: Any]) in
|
||||
try? WCSession.default.updateApplicationContext(context)
|
||||
try? self.session?.updateApplicationContext(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user