197 lines
7.8 KiB
Swift
197 lines
7.8 KiB
Swift
import Foundation
|
|
import IOKit
|
|
import os
|
|
|
|
class PolarService {
|
|
private let organizationId = "Org"
|
|
private let apiToken = "Token"
|
|
private let baseURL = "https://api.polar.sh"
|
|
private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "PolarService")
|
|
|
|
struct LicenseValidationResponse: Codable {
|
|
let status: String
|
|
let limit_activations: Int?
|
|
let id: String?
|
|
let activation: ActivationResponse?
|
|
}
|
|
|
|
struct ActivationResponse: Codable {
|
|
let id: String
|
|
}
|
|
|
|
struct ActivationRequest: Codable {
|
|
let key: String
|
|
let organization_id: String
|
|
let label: String
|
|
let meta: [String: String]
|
|
}
|
|
|
|
struct ActivationResult: Codable {
|
|
let id: String
|
|
let license_key: LicenseKeyInfo
|
|
}
|
|
|
|
struct LicenseKeyInfo: Codable {
|
|
let limit_activations: Int
|
|
let status: String
|
|
}
|
|
|
|
// Generate a unique device identifier
|
|
private func getDeviceIdentifier() -> String {
|
|
// Use the macOS serial number or a generated UUID that persists
|
|
if let serialNumber = getMacSerialNumber() {
|
|
return serialNumber
|
|
}
|
|
|
|
// Fallback to a stored UUID if we can't get the serial number
|
|
let defaults = UserDefaults.standard
|
|
if let storedId = defaults.string(forKey: "VoiceInkDeviceIdentifier") {
|
|
return storedId
|
|
}
|
|
|
|
// Create and store a new UUID if none exists
|
|
let newId = UUID().uuidString
|
|
defaults.set(newId, forKey: "VoiceInkDeviceIdentifier")
|
|
return newId
|
|
}
|
|
|
|
// Try to get the Mac serial number
|
|
private func getMacSerialNumber() -> String? {
|
|
let platformExpert = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("IOPlatformExpertDevice"))
|
|
if platformExpert == 0 { return nil }
|
|
|
|
defer { IOObjectRelease(platformExpert) }
|
|
|
|
if let serialNumber = IORegistryEntryCreateCFProperty(platformExpert, "IOPlatformSerialNumber" as CFString, kCFAllocatorDefault, 0) {
|
|
return (serialNumber.takeRetainedValue() as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Check if a license key requires activation
|
|
func checkLicenseRequiresActivation(_ key: String) async throws -> (isValid: Bool, requiresActivation: Bool, activationsLimit: Int?) {
|
|
let url = URL(string: "\(baseURL)/v1/customer-portal/license-keys/validate")!
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "POST"
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
|
|
let body: [String: Any] = [
|
|
"key": key,
|
|
"organization_id": organizationId
|
|
]
|
|
|
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
|
|
let (data, httpResponse) = try await URLSession.shared.data(for: request)
|
|
|
|
if let httpResponse = httpResponse as? HTTPURLResponse {
|
|
if !(200...299).contains(httpResponse.statusCode) {
|
|
let errorMsg = String(data: data, encoding: .utf8) ?? "Unknown error"
|
|
logger.notice("🔑 License validation failed [HTTP \(httpResponse.statusCode)]: \(errorMsg, privacy: .public)")
|
|
throw LicenseError.validationFailed(errorMsg)
|
|
}
|
|
}
|
|
|
|
let validationResponse = try JSONDecoder().decode(LicenseValidationResponse.self, from: data)
|
|
let isValid = validationResponse.status == "granted"
|
|
|
|
// If limit_activations is nil or 0, the license doesn't require activation
|
|
let requiresActivation = (validationResponse.limit_activations ?? 0) > 0
|
|
|
|
return (isValid: isValid, requiresActivation: requiresActivation, activationsLimit: validationResponse.limit_activations)
|
|
}
|
|
|
|
// Activate a license key on this device
|
|
func activateLicenseKey(_ key: String) async throws -> (activationId: String, activationsLimit: Int) {
|
|
let url = URL(string: "\(baseURL)/v1/customer-portal/license-keys/activate")!
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "POST"
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
|
|
let deviceId = getDeviceIdentifier()
|
|
let hostname = Host.current().localizedName ?? "Unknown Mac"
|
|
|
|
let activationRequest = ActivationRequest(
|
|
key: key,
|
|
organization_id: organizationId,
|
|
label: hostname,
|
|
meta: ["device_id": deviceId]
|
|
)
|
|
|
|
request.httpBody = try JSONEncoder().encode(activationRequest)
|
|
|
|
let (data, httpResponse) = try await URLSession.shared.data(for: request)
|
|
|
|
if let httpResponse = httpResponse as? HTTPURLResponse {
|
|
if !(200...299).contains(httpResponse.statusCode) {
|
|
let errorMsg = String(data: data, encoding: .utf8) ?? "Unknown error"
|
|
logger.notice("🔑 License activation failed [HTTP \(httpResponse.statusCode)]: \(errorMsg, privacy: .public)")
|
|
|
|
// Check for specific error messages
|
|
if errorMsg.contains("activation limit") || errorMsg.contains("maximum activations") {
|
|
throw LicenseError.activationLimitReached(errorMsg)
|
|
}
|
|
if errorMsg.contains("License key does not require activation") {
|
|
throw LicenseError.activationNotRequired
|
|
}
|
|
throw LicenseError.activationFailed(errorMsg)
|
|
}
|
|
}
|
|
|
|
let activationResult = try JSONDecoder().decode(ActivationResult.self, from: data)
|
|
|
|
return (activationId: activationResult.id, activationsLimit: activationResult.license_key.limit_activations)
|
|
}
|
|
|
|
// Validate a license key with an activation ID
|
|
func validateLicenseKeyWithActivation(_ key: String, activationId: String) async throws -> Bool {
|
|
let url = URL(string: "\(baseURL)/v1/customer-portal/license-keys/validate")!
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "POST"
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
|
|
let body: [String: Any] = [
|
|
"key": key,
|
|
"organization_id": organizationId,
|
|
"activation_id": activationId
|
|
]
|
|
|
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
|
|
let (data, httpResponse) = try await URLSession.shared.data(for: request)
|
|
|
|
if let httpResponse = httpResponse as? HTTPURLResponse {
|
|
if !(200...299).contains(httpResponse.statusCode) {
|
|
let errorMsg = String(data: data, encoding: .utf8) ?? "Unknown error"
|
|
logger.notice("🔑 License validation with activation failed [HTTP \(httpResponse.statusCode)]: \(errorMsg, privacy: .public)")
|
|
throw LicenseError.validationFailed(errorMsg)
|
|
}
|
|
}
|
|
|
|
let validationResponse = try JSONDecoder().decode(LicenseValidationResponse.self, from: data)
|
|
|
|
return validationResponse.status == "granted"
|
|
}
|
|
}
|
|
|
|
enum LicenseError: Error, LocalizedError {
|
|
case activationFailed(String)
|
|
case validationFailed(String)
|
|
case activationLimitReached(String)
|
|
case activationNotRequired
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .activationFailed(let details):
|
|
return "Failed to activate license: \(details)"
|
|
case .validationFailed(let details):
|
|
return "License validation failed: \(details)"
|
|
case .activationLimitReached(let details):
|
|
return "Activation limit reached: \(details)"
|
|
case .activationNotRequired:
|
|
return "This license does not require activation."
|
|
}
|
|
}
|
|
}
|