2026-04-21 22:07:29 +08:00
|
|
|
import Foundation
|
|
|
|
|
|
|
|
|
|
@MainActor
|
2026-05-01 21:27:39 +08:00
|
|
|
public final class XuqmSDK: NSObject {
|
2026-04-21 22:07:29 +08:00
|
|
|
|
|
|
|
|
public static let shared = XuqmSDK()
|
|
|
|
|
private(set) var config: SDKConfig?
|
|
|
|
|
private(set) var tokenStore: TokenStore?
|
|
|
|
|
|
2026-05-01 21:27:39 +08:00
|
|
|
public private(set) var currentUserId: String?
|
|
|
|
|
public var onUserSigExpired: (() -> Void)?
|
|
|
|
|
|
|
|
|
|
private var userSig: String?
|
|
|
|
|
private var userSigTimer: Timer?
|
|
|
|
|
private var cachedDeviceToken: String?
|
|
|
|
|
|
|
|
|
|
private override init() {
|
|
|
|
|
super.init()
|
|
|
|
|
}
|
2026-04-21 22:07:29 +08:00
|
|
|
|
|
|
|
|
public func initialize(config: SDKConfig) {
|
|
|
|
|
self.config = config
|
|
|
|
|
self.tokenStore = TokenStore()
|
|
|
|
|
ApiClient.shared.configure(with: config)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func requireConfig() -> SDKConfig {
|
|
|
|
|
guard let config else {
|
|
|
|
|
fatalError("XuqmSDK not initialized. Call XuqmSDK.shared.initialize() first.")
|
|
|
|
|
}
|
|
|
|
|
return config
|
|
|
|
|
}
|
2026-05-01 21:27:39 +08:00
|
|
|
|
|
|
|
|
public func login(userId: String, userSig: String) async {
|
|
|
|
|
self.currentUserId = userId
|
|
|
|
|
self.userSig = userSig
|
|
|
|
|
startUserSigExpirationTimer(userSig: userSig)
|
|
|
|
|
|
|
|
|
|
do {
|
|
|
|
|
try await ImSDK.shared.loginWithUserSig(userId, userSig)
|
|
|
|
|
} catch {
|
|
|
|
|
// IM login failed; silently ignored per facade pattern
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let cachedDeviceToken {
|
|
|
|
|
do {
|
|
|
|
|
try await PushSDK.shared.registerToken(cachedDeviceToken, userId: userId)
|
|
|
|
|
} catch {
|
|
|
|
|
// Push registration failed
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func logout() async {
|
|
|
|
|
userSigTimer?.invalidate()
|
|
|
|
|
userSigTimer = nil
|
|
|
|
|
|
|
|
|
|
ImSDK.shared.disconnect()
|
|
|
|
|
|
|
|
|
|
if let userId = currentUserId {
|
|
|
|
|
do {
|
|
|
|
|
try await PushSDK.shared.unregisterToken(userId: userId)
|
|
|
|
|
} catch {
|
|
|
|
|
// Push unregistration failed
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tokenStore?.clear()
|
|
|
|
|
currentUserId = nil
|
|
|
|
|
userSig = nil
|
|
|
|
|
cachedDeviceToken = nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func registerDeviceToken(_ deviceToken: Data) {
|
|
|
|
|
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
|
|
|
|
|
self.cachedDeviceToken = token
|
|
|
|
|
Task { @MainActor in
|
|
|
|
|
if let userId = self.currentUserId {
|
|
|
|
|
try? await PushSDK.shared.registerToken(token, userId: userId)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func startUserSigExpirationTimer(userSig: String) {
|
|
|
|
|
userSigTimer?.invalidate()
|
|
|
|
|
guard let expDate = extractExpirationDate(from: userSig) else { return }
|
|
|
|
|
let interval = expDate.timeIntervalSinceNow - 300 // 5 minutes before expiry
|
|
|
|
|
guard interval > 0 else {
|
|
|
|
|
onUserSigExpired?()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
userSigTimer = Timer.scheduledTimer(
|
|
|
|
|
timeInterval: interval,
|
|
|
|
|
target: self,
|
|
|
|
|
selector: #selector(userSigDidExpire),
|
|
|
|
|
userInfo: nil,
|
|
|
|
|
repeats: false
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc private func userSigDidExpire() {
|
|
|
|
|
onUserSigExpired?()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func extractExpirationDate(from userSig: String) -> Date? {
|
|
|
|
|
let parts = userSig.split(separator: ".")
|
|
|
|
|
guard parts.count >= 2 else { return nil }
|
|
|
|
|
var base64 = String(parts[1])
|
|
|
|
|
.replacingOccurrences(of: "-", with: "+")
|
|
|
|
|
.replacingOccurrences(of: "_", with: "/")
|
|
|
|
|
let padding = 4 - base64.count % 4
|
|
|
|
|
if padding != 4 {
|
|
|
|
|
base64 += String(repeating: "=", count: padding)
|
|
|
|
|
}
|
|
|
|
|
guard let data = Data(base64Encoded: base64),
|
|
|
|
|
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
|
|
|
let exp = json["exp"] as? TimeInterval else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return Date(timeIntervalSince1970: exp)
|
|
|
|
|
}
|
2026-04-21 22:07:29 +08:00
|
|
|
}
|