refactor: remove deprecated license module
这个提交包含在:
父节点
0201ff8b27
当前提交
0d872e803c
2
Jenkinsfile
vendored
2
Jenkinsfile
vendored
@ -10,7 +10,6 @@ pipeline {
|
||||
booleanParam(name: 'MOD_IM', defaultValue: false, description: '本次包含 XuqmImSDK 改动')
|
||||
booleanParam(name: 'MOD_PUSH', defaultValue: false, description: '本次包含 XuqmPushSDK 改动')
|
||||
booleanParam(name: 'MOD_UPDATE', defaultValue: false, description: '本次包含 XuqmUpdateSDK 改动')
|
||||
booleanParam(name: 'MOD_LICENSE', defaultValue: false, description: '本次包含 XuqmLicenseSDK 改动')
|
||||
booleanParam(name: 'MOD_FILE', defaultValue: false, description: '本次包含 XuqmFileSDK 改动')
|
||||
booleanParam(name: 'MOD_WEBVIEW', defaultValue: false, description: '本次包含 XuqmWebViewSDK 改动')
|
||||
booleanParam(name: 'MOD_BUGCOLLECT', defaultValue: false, description: '本次包含 XuqmBugCollectSDK 改动')
|
||||
@ -41,7 +40,6 @@ pipeline {
|
||||
'XuqmImSDK': params.MOD_IM,
|
||||
'XuqmPushSDK': params.MOD_PUSH,
|
||||
'XuqmUpdateSDK': params.MOD_UPDATE,
|
||||
'XuqmLicenseSDK': params.MOD_LICENSE,
|
||||
'XuqmFileSDK': params.MOD_FILE,
|
||||
'XuqmWebViewSDK': params.MOD_WEBVIEW,
|
||||
'XuqmBugCollectSDK': params.MOD_BUGCOLLECT,
|
||||
|
||||
@ -10,7 +10,6 @@ let package = Package(
|
||||
.library(name: "XuqmImSDK", targets: ["XuqmImSDK"]),
|
||||
.library(name: "XuqmPushSDK", targets: ["XuqmPushSDK"]),
|
||||
.library(name: "XuqmUpdateSDK", targets: ["XuqmUpdateSDK"]),
|
||||
.library(name: "XuqmLicenseSDK", targets: ["XuqmLicenseSDK"]),
|
||||
.library(name: "XuqmFileSDK", targets: ["XuqmFileSDK"]),
|
||||
.library(name: "XuqmWebViewSDK", targets: ["XuqmWebViewSDK"]),
|
||||
.library(name: "XuqmBugCollectSDK", targets: ["XuqmBugCollectSDK"]),
|
||||
@ -41,12 +40,6 @@ let package = Package(
|
||||
path: "Sources/XuqmUpdateSDK",
|
||||
swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]
|
||||
),
|
||||
.target(
|
||||
name: "XuqmLicenseSDK",
|
||||
dependencies: ["XuqmCoreSDK"],
|
||||
path: "Sources/XuqmLicenseSDK",
|
||||
swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]
|
||||
),
|
||||
.target(
|
||||
name: "XuqmFileSDK",
|
||||
dependencies: ["XuqmCoreSDK"],
|
||||
@ -69,7 +62,7 @@ let package = Package(
|
||||
name: "XuqmSDK",
|
||||
dependencies: [
|
||||
"XuqmCoreSDK", "XuqmImSDK", "XuqmPushSDK",
|
||||
"XuqmUpdateSDK", "XuqmLicenseSDK", "XuqmFileSDK",
|
||||
"XuqmUpdateSDK", "XuqmFileSDK",
|
||||
"XuqmBugCollectSDK",
|
||||
],
|
||||
path: "Sources/XuqmSDK",
|
||||
|
||||
@ -14,7 +14,6 @@ XuqmGroup-iOSSDK/
|
||||
├── Sources/XuqmImSDK/ # IM:WebSocket 实时通信
|
||||
├── Sources/XuqmPushSDK/ # 推送:APNs Token 注册
|
||||
├── Sources/XuqmUpdateSDK/ # 版本管理:检查更新
|
||||
├── Sources/XuqmLicenseSDK/ # 授权管理
|
||||
├── Sources/XuqmFileSDK/ # 文件上传、下载、本地打开
|
||||
└── Sources/XuqmWebViewSDK/ # WebView 嵌入式组件 / 独立页面
|
||||
├── XWebViewBridge.swift # 页面配置 / 控制器桥接
|
||||
|
||||
@ -2,7 +2,7 @@ import Foundation
|
||||
|
||||
/// Reads and decrypts the init config file from the app bundle.
|
||||
/// Looks for xuqm/config.xuqm first, then falls back to any *.xuqmconfig file.
|
||||
/// This is used by XuqmSDK.autoInitialize() and does NOT depend on XuqmLicenseSDK.
|
||||
/// This is used by XuqmSDK.autoInitialize().
|
||||
public enum ConfigFileReader {
|
||||
|
||||
public static func read() -> ConfigFileData? {
|
||||
|
||||
@ -5,7 +5,6 @@ import UIKit
|
||||
|
||||
/// Decrypted content of the init config file (xuqm/config.xuqm).
|
||||
/// Contains the appKey and optional server URL needed to bootstrap the SDK.
|
||||
/// This is separate from XuqmLicenseSDK's LicenseFile (device activation).
|
||||
public struct ConfigFileData: Codable, Sendable {
|
||||
public let appKey: String
|
||||
public let appName: String?
|
||||
@ -78,7 +77,7 @@ public final class XuqmSDK: NSObject {
|
||||
}
|
||||
let bundleId = Bundle.main.bundleIdentifier ?? ""
|
||||
if let configBundle = file.iosBundleId, !configBundle.isEmpty, configBundle != bundleId {
|
||||
throw XuqmSDKError.packageMismatch(license: configBundle, local: bundleId)
|
||||
throw XuqmSDKError.packageMismatch(configured: configBundle, local: bundleId)
|
||||
}
|
||||
if let baseUrl = file.baseUrl {
|
||||
SDKEndpoints.configure(apiBaseURL: baseUrl)
|
||||
@ -218,6 +217,5 @@ public final class XuqmSDK: NSObject {
|
||||
|
||||
public enum XuqmSDKError: Error {
|
||||
case noConfigFile
|
||||
case noLicenseFile
|
||||
case packageMismatch(license: String, local: String)
|
||||
case packageMismatch(configured: String, local: String)
|
||||
}
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
import XuqmCoreSDK
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
import Foundation
|
||||
|
||||
enum DeviceInfoProvider {
|
||||
|
||||
static func getDeviceId(store: LicenseStore) -> String {
|
||||
if let existing = store.deviceId { return existing }
|
||||
#if canImport(UIKit)
|
||||
let id = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
|
||||
#else
|
||||
let id = UUID().uuidString
|
||||
#endif
|
||||
store.deviceId = id
|
||||
return id
|
||||
}
|
||||
|
||||
static func getDeviceName() -> String {
|
||||
#if canImport(UIKit)
|
||||
return UIDevice.current.name
|
||||
#else
|
||||
return Host.current().localizedName ?? "Mac"
|
||||
#endif
|
||||
}
|
||||
|
||||
static func getDeviceModel() -> String {
|
||||
#if canImport(UIKit)
|
||||
return UIDevice.current.model
|
||||
#else
|
||||
return "Mac"
|
||||
#endif
|
||||
}
|
||||
|
||||
static func getDeviceVendor() -> String { "Apple" }
|
||||
|
||||
static func getOsVersion() -> String {
|
||||
#if canImport(UIKit)
|
||||
return "iOS \(UIDevice.current.systemVersion)"
|
||||
#else
|
||||
let v = ProcessInfo.processInfo.operatingSystemVersion
|
||||
return "macOS \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)"
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
import Foundation
|
||||
import XuqmCoreSDK
|
||||
import CryptoKit
|
||||
import CommonCrypto
|
||||
|
||||
enum LicenseFileCryptoError: Error {
|
||||
case invalidFormat
|
||||
case keyDerivationFailed
|
||||
case decryptionFailed
|
||||
}
|
||||
|
||||
enum LicenseFileCrypto {
|
||||
private static let magic = "XUQM-LICENSE-V1"
|
||||
private static let passphrase = "xuqm-license-file-v1.2026.internal"
|
||||
private static let keyByteCount = 32
|
||||
private static let pbkdf2Iterations: UInt32 = 120_000
|
||||
|
||||
// Format: MAGIC.base64UrlSalt.base64UrlIV.base64UrlCiphertext
|
||||
// Base64: URL-safe, no padding, no wrap
|
||||
// Algorithm: AES/GCM/NoPadding, PBKDF2WithHmacSHA256, 256-bit key, 128-bit tag
|
||||
static func decrypt(_ content: String) throws -> String {
|
||||
let parts = content.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: ".")
|
||||
guard parts.count == 4, parts[0] == magic else {
|
||||
throw LicenseFileCryptoError.invalidFormat
|
||||
}
|
||||
guard let salt = base64UrlDecode(parts[1]),
|
||||
let iv = base64UrlDecode(parts[2]),
|
||||
let ciphertext = base64UrlDecode(parts[3]) else {
|
||||
throw LicenseFileCryptoError.invalidFormat
|
||||
}
|
||||
let key = try deriveKey(salt: salt)
|
||||
// CryptoKit SealedBox(combined:) expects nonce(12) || ciphertext || tag(16)
|
||||
// Android JCE appends the 16-byte GCM tag at end of ciphertext — matches this format exactly
|
||||
let combined = iv + ciphertext
|
||||
let sealedBox = try AES.GCM.SealedBox(combined: combined)
|
||||
let decrypted = try AES.GCM.open(sealedBox, using: key)
|
||||
guard let plaintext = String(data: decrypted, encoding: .utf8) else {
|
||||
throw LicenseFileCryptoError.decryptionFailed
|
||||
}
|
||||
return plaintext
|
||||
}
|
||||
|
||||
private static func deriveKey(salt: Data) throws -> SymmetricKey {
|
||||
let passphraseBytes = Array(passphrase.utf8)
|
||||
let saltBytes = Array(salt)
|
||||
var derivedKey = [UInt8](repeating: 0, count: keyByteCount)
|
||||
let status = CCKeyDerivationPBKDF(
|
||||
CCPBKDFAlgorithm(kCCPBKDF2),
|
||||
passphrase, passphraseBytes.count,
|
||||
saltBytes, saltBytes.count,
|
||||
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
|
||||
pbkdf2Iterations,
|
||||
&derivedKey, keyByteCount
|
||||
)
|
||||
guard status == kCCSuccess else {
|
||||
throw LicenseFileCryptoError.keyDerivationFailed
|
||||
}
|
||||
return SymmetricKey(data: Data(derivedKey))
|
||||
}
|
||||
|
||||
private static func base64UrlDecode(_ value: String) -> Data? {
|
||||
var s = value.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
||||
let rem = s.count % 4
|
||||
if rem > 0 { s += String(repeating: "=", count: 4 - rem) }
|
||||
return Data(base64Encoded: s)
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
import Foundation
|
||||
import XuqmCoreSDK
|
||||
|
||||
public enum LicenseFileReader {
|
||||
|
||||
public static func read() -> LicenseFile? {
|
||||
guard let url = Bundle.main.url(forResource: "license", withExtension: "xuqm", subdirectory: "xuqm"),
|
||||
let encrypted = try? String(contentsOf: url, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
return parse(encrypted)
|
||||
}
|
||||
|
||||
static func parse(_ encrypted: String) -> LicenseFile? {
|
||||
guard let json = try? LicenseFileCrypto.decrypt(encrypted),
|
||||
let data = json.data(using: .utf8),
|
||||
let file = try? JSONDecoder().decode(LicenseFile.self, from: data) else {
|
||||
return nil
|
||||
}
|
||||
return file
|
||||
}
|
||||
}
|
||||
@ -1,99 +0,0 @@
|
||||
import Foundation
|
||||
import XuqmCoreSDK
|
||||
|
||||
public struct LicenseFile: Codable, Sendable {
|
||||
public let appKey: String
|
||||
public let appName: String?
|
||||
public let companyName: String?
|
||||
public let packageName: String?
|
||||
public let iosBundleId: String?
|
||||
public let harmonyBundleName: String?
|
||||
public let baseUrl: String?
|
||||
public let serverUrl: String?
|
||||
public let issuedAt: String?
|
||||
public let expiresAt: String?
|
||||
|
||||
public init(
|
||||
appKey: String,
|
||||
appName: String? = nil,
|
||||
companyName: String? = nil,
|
||||
packageName: String? = nil,
|
||||
iosBundleId: String? = nil,
|
||||
harmonyBundleName: String? = nil,
|
||||
baseUrl: String? = nil,
|
||||
serverUrl: String? = nil,
|
||||
issuedAt: String? = nil,
|
||||
expiresAt: String? = nil
|
||||
) {
|
||||
self.appKey = appKey
|
||||
self.appName = appName
|
||||
self.companyName = companyName
|
||||
self.packageName = packageName
|
||||
self.iosBundleId = iosBundleId
|
||||
self.harmonyBundleName = harmonyBundleName
|
||||
self.baseUrl = baseUrl
|
||||
self.serverUrl = serverUrl
|
||||
self.issuedAt = issuedAt
|
||||
self.expiresAt = expiresAt
|
||||
}
|
||||
}
|
||||
|
||||
public struct LicenseUserInfo: Sendable {
|
||||
public let userId: String?
|
||||
public let name: String?
|
||||
public let email: String?
|
||||
public let phone: String?
|
||||
|
||||
public init(userId: String? = nil, name: String? = nil, email: String? = nil, phone: String? = nil) {
|
||||
self.userId = userId
|
||||
self.name = name
|
||||
self.email = email
|
||||
self.phone = phone
|
||||
}
|
||||
}
|
||||
|
||||
public enum LicenseStatus: Sendable {
|
||||
case ok
|
||||
case denied
|
||||
case unknown
|
||||
}
|
||||
|
||||
public enum LicenseResult: Sendable {
|
||||
case success(String)
|
||||
case error(String)
|
||||
}
|
||||
|
||||
struct UserInfoPayload: Encodable {
|
||||
let userId: String?
|
||||
let name: String?
|
||||
let email: String?
|
||||
let phone: String?
|
||||
}
|
||||
|
||||
struct RegisterRequest: Encodable {
|
||||
let appKey: String
|
||||
let deviceId: String
|
||||
let deviceName: String?
|
||||
let deviceModel: String
|
||||
let deviceVendor: String
|
||||
let osVersion: String
|
||||
let userInfo: UserInfoPayload?
|
||||
}
|
||||
|
||||
struct VerifyRequest: Encodable {
|
||||
let appKey: String
|
||||
let deviceId: String
|
||||
let token: String
|
||||
let userInfo: UserInfoPayload?
|
||||
}
|
||||
|
||||
struct RegisterResponse: Decodable {
|
||||
let success: Bool
|
||||
let token: String?
|
||||
let message: String?
|
||||
}
|
||||
|
||||
struct VerifyResponse: Decodable {
|
||||
let valid: Bool
|
||||
let error: String?
|
||||
}
|
||||
@ -1,148 +0,0 @@
|
||||
import Foundation
|
||||
import XuqmCoreSDK
|
||||
|
||||
public final class LicenseSDK: @unchecked Sendable {
|
||||
|
||||
public static let shared = LicenseSDK()
|
||||
|
||||
private static let defaultBaseUrl = "https://auth.dev.xuqinmin.com/"
|
||||
private static let statusOk = "ok"
|
||||
private static let statusDenied = "denied"
|
||||
private static let cacheWindowMs: Double = 10 * 60 * 1000
|
||||
|
||||
private var appKey: String?
|
||||
private var baseUrl: String = defaultBaseUrl
|
||||
private var deviceName: String?
|
||||
private let store = LicenseStore()
|
||||
private var initialized = false
|
||||
|
||||
private init() {}
|
||||
|
||||
/// Manual initialization. Typically not needed — call checkLicense() directly
|
||||
/// after XuqmSDK is initialized (via config file or manual init).
|
||||
public func initialize(appKey: String, baseUrl: String? = nil, deviceName: String? = nil) {
|
||||
let url = normalize(baseUrl ?? Self.defaultBaseUrl)
|
||||
if store.appKey != nil && store.appKey != appKey { store.clear() }
|
||||
self.appKey = appKey
|
||||
self.baseUrl = url
|
||||
self.deviceName = deviceName ?? DeviceInfoProvider.getDeviceName()
|
||||
store.appKey = appKey
|
||||
initialized = true
|
||||
}
|
||||
|
||||
/// Check license validity. Returns cached result within 10-minute window.
|
||||
/// On network error, falls back to last cached OK status.
|
||||
/// Automatically uses appKey from XuqmSDK if not manually initialized.
|
||||
public func checkLicense(userInfo: LicenseUserInfo? = nil) async -> LicenseResult {
|
||||
if !initialized {
|
||||
await XuqmSDK.shared.awaitInitialization()
|
||||
ensureInitializedFromSDK()
|
||||
}
|
||||
guard initialized, let appKey else {
|
||||
return .error("LicenseSDK not initialized. Ensure XuqmSDK is initialized first.")
|
||||
}
|
||||
|
||||
let cachedStatus = store.status
|
||||
let cachedTime = store.statusTime
|
||||
if cachedStatus == Self.statusOk && (Date().timeIntervalSince1970 * 1000 - cachedTime) < Self.cacheWindowMs {
|
||||
return .success("Cached")
|
||||
}
|
||||
|
||||
let deviceId = DeviceInfoProvider.getDeviceId(store: store)
|
||||
let payload = userInfo.map { UserInfoPayload(userId: $0.userId, name: $0.name, email: $0.email, phone: $0.phone) }
|
||||
|
||||
do {
|
||||
if let token = store.token {
|
||||
let req = VerifyRequest(appKey: appKey, deviceId: deviceId, token: token, userInfo: payload)
|
||||
if let resp = try? await post(VerifyResponse.self, path: "api/license/verify", body: req), resp.valid {
|
||||
persistStatus(Self.statusOk)
|
||||
return .success("Verified")
|
||||
}
|
||||
store.token = nil
|
||||
}
|
||||
|
||||
let req = RegisterRequest(
|
||||
appKey: appKey,
|
||||
deviceId: deviceId,
|
||||
deviceName: deviceName ?? DeviceInfoProvider.getDeviceName(),
|
||||
deviceModel: DeviceInfoProvider.getDeviceModel(),
|
||||
deviceVendor: DeviceInfoProvider.getDeviceVendor(),
|
||||
osVersion: DeviceInfoProvider.getOsVersion(),
|
||||
userInfo: payload
|
||||
)
|
||||
let resp = try await post(RegisterResponse.self, path: "api/license/register", body: req)
|
||||
if resp.success, let token = resp.token {
|
||||
store.token = token
|
||||
persistStatus(Self.statusOk)
|
||||
return .success("Registered")
|
||||
}
|
||||
persistStatus(Self.statusDenied)
|
||||
return .error(resp.message ?? "Registration denied")
|
||||
} catch {
|
||||
if cachedStatus == Self.statusOk { return .success("Offline - cached ok") }
|
||||
return .error(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
public func getStatus() async -> LicenseStatus {
|
||||
if !initialized {
|
||||
await XuqmSDK.shared.awaitInitialization()
|
||||
ensureInitializedFromSDK()
|
||||
}
|
||||
switch store.status {
|
||||
case Self.statusOk: return .ok
|
||||
case Self.statusDenied: return .denied
|
||||
default: return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
public func getDeviceId() async -> String? {
|
||||
if !initialized {
|
||||
await XuqmSDK.shared.awaitInitialization()
|
||||
ensureInitializedFromSDK()
|
||||
}
|
||||
if !initialized { return nil }
|
||||
return store.deviceId
|
||||
}
|
||||
|
||||
public func clear() { store.clear() }
|
||||
|
||||
/// Initialize from XuqmSDK's config. Called when LicenseSDK is used before manual init.
|
||||
@discardableResult
|
||||
private func ensureInitializedFromSDK() -> Bool {
|
||||
let isInit = DispatchQueue.main.sync { XuqmSDK.shared.initialized }
|
||||
guard isInit, let config = DispatchQueue.main.sync(execute: { XuqmSDK.shared.config }) else { return false }
|
||||
initialize(appKey: config.appKey)
|
||||
return true
|
||||
}
|
||||
|
||||
private func persistStatus(_ status: String) {
|
||||
store.status = status
|
||||
store.statusTime = Date().timeIntervalSince1970 * 1000
|
||||
}
|
||||
|
||||
private func normalize(_ url: String) -> String {
|
||||
let s = url.trimmingCharacters(in: .whitespaces)
|
||||
return s.hasSuffix("/") ? s : s + "/"
|
||||
}
|
||||
|
||||
private func post<T: Decodable, B: Encodable>(_ type: T.Type, path: String, body: B) async throws -> T {
|
||||
guard let url = URL(string: baseUrl + path) else { throw URLError(.badURL) }
|
||||
var req = URLRequest(url: url)
|
||||
req.httpMethod = "POST"
|
||||
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
req.httpBody = try JSONEncoder().encode(body)
|
||||
let (data, response) = try await URLSession.shared.data(for: req)
|
||||
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
guard let wrapper = try? JSONDecoder().decode(LicenseResponseWrapper<T>.self, from: data), let result = wrapper.data else {
|
||||
throw URLError(.cannotDecodeContentData)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private struct LicenseResponseWrapper<U: Decodable>: Decodable {
|
||||
let data: U?
|
||||
}
|
||||
@ -1,86 +0,0 @@
|
||||
import Foundation
|
||||
import XuqmCoreSDK
|
||||
import Security
|
||||
|
||||
final class LicenseStore {
|
||||
private let keychainService = "com.xuqm.license"
|
||||
private let defaults: UserDefaults
|
||||
|
||||
init() {
|
||||
defaults = UserDefaults(suiteName: "xuqm_license") ?? .standard
|
||||
}
|
||||
|
||||
var token: String? {
|
||||
get { keychainGet("token") }
|
||||
set { keychainSet("token", value: newValue) }
|
||||
}
|
||||
|
||||
var deviceId: String? {
|
||||
get { keychainGet("deviceId") }
|
||||
set { keychainSet("deviceId", value: newValue) }
|
||||
}
|
||||
|
||||
var status: String? {
|
||||
get { defaults.string(forKey: "status") }
|
||||
set { defaults.set(newValue, forKey: "status") }
|
||||
}
|
||||
|
||||
// Stored as Double (milliseconds since epoch) to match Android's currentTimeMillis
|
||||
var statusTime: Double {
|
||||
get { defaults.double(forKey: "statusTime") }
|
||||
set { defaults.set(newValue, forKey: "statusTime") }
|
||||
}
|
||||
|
||||
var appKey: String? {
|
||||
get { defaults.string(forKey: "appKey") }
|
||||
set { defaults.set(newValue, forKey: "appKey") }
|
||||
}
|
||||
|
||||
func clear() {
|
||||
token = nil
|
||||
deviceId = nil
|
||||
status = nil
|
||||
statusTime = 0
|
||||
appKey = nil
|
||||
}
|
||||
|
||||
private func keychainGet(_ key: String) -> String? {
|
||||
let query: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrService: keychainService,
|
||||
kSecAttrAccount: key,
|
||||
kSecReturnData: true,
|
||||
kSecMatchLimit: kSecMatchLimitOne,
|
||||
]
|
||||
var result: AnyObject?
|
||||
guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
|
||||
let data = result as? Data else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
private func keychainSet(_ key: String, value: String?) {
|
||||
guard let value else { keychainDelete(key); return }
|
||||
let data = Data(value.utf8)
|
||||
let query: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrService: keychainService,
|
||||
kSecAttrAccount: key,
|
||||
]
|
||||
if SecItemCopyMatching(query as CFDictionary, nil) == errSecSuccess {
|
||||
SecItemUpdate(query as CFDictionary, [kSecValueData: data] as CFDictionary)
|
||||
} else {
|
||||
var item = query
|
||||
item[kSecValueData] = data
|
||||
SecItemAdd(item as CFDictionary, nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func keychainDelete(_ key: String) {
|
||||
let query: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrService: keychainService,
|
||||
kSecAttrAccount: key,
|
||||
]
|
||||
SecItemDelete(query as CFDictionary)
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,6 @@
|
||||
@_exported import XuqmImSDK
|
||||
@_exported import XuqmPushSDK
|
||||
@_exported import XuqmUpdateSDK
|
||||
@_exported import XuqmLicenseSDK
|
||||
@_exported import XuqmFileSDK
|
||||
@_exported import XuqmBugCollectSDK
|
||||
|
||||
@ -14,8 +13,6 @@ extension XuqmSDK {
|
||||
|
||||
/// Register all module hooks. Called automatically when any module is first used.
|
||||
public func registerModuleHooks() {
|
||||
// Note: licenseReader hook removed — autoInitialize now reads config.xuqm directly via ConfigFileReader.
|
||||
|
||||
// Initialize hook — auto-start push and bugcollect if configured
|
||||
onInitialize = { @MainActor config in
|
||||
if config.autoRegisterPush {
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'XuqmLicenseSDK'
|
||||
s.version = '0.1.0'
|
||||
s.summary = 'XuqmGroup iOS SDK — License module'
|
||||
s.homepage = 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK'
|
||||
s.license = { :type => 'MIT' }
|
||||
s.author = { 'XuqmGroup' => 'dev@xuqm.com' }
|
||||
s.source = { :git => 'https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK.git', :tag => "license/#{s.version}" }
|
||||
s.ios.deployment_target = '16.0'
|
||||
s.swift_version = '5.9'
|
||||
s.source_files = 'Sources/XuqmLicenseSDK/**/*.swift'
|
||||
s.dependency 'XuqmCoreSDK'
|
||||
end
|
||||
@ -13,6 +13,5 @@ Pod::Spec.new do |s|
|
||||
s.dependency 'XuqmImSDK'
|
||||
s.dependency 'XuqmPushSDK'
|
||||
s.dependency 'XuqmUpdateSDK'
|
||||
s.dependency 'XuqmLicenseSDK'
|
||||
s.dependency 'XuqmFileSDK'
|
||||
end
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户