Created
March 14, 2024 03:59
-
-
Save dqhieu/992696b62871dee7a16e7927f73cce09 to your computer and use it in GitHub Desktop.
Lemon Squeezy License Manager
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// LicenseManager.swift | |
// CompressX | |
// | |
// Created by Dinh Quang Hieu on 06/01/2024. | |
// | |
import Foundation | |
import SwiftUI | |
import IOKit | |
enum LicenseStatus: String { | |
case inactive | |
case active | |
case expired | |
case disabled | |
} | |
struct LicenseKey: Decodable { | |
let expires_at: String? | |
let status: String | |
let activation_limit: Int | |
let activation_usage: Int | |
} | |
struct Instance: Decodable { | |
let id: String | |
} | |
struct Meta: Decodable { | |
let product_id: Int | |
} | |
struct LicenseActivateModel: Decodable { | |
let activated: Bool | |
let error: String? | |
let license_key: LicenseKey? | |
let instance: Instance? | |
let meta: Meta? | |
} | |
struct LicenseValidateModel: Decodable { | |
let valid: Bool | |
let error: String? | |
let license_key: LicenseKey? | |
let meta: Meta? | |
let instance: Instance? | |
} | |
struct LicenseDeactivateModel: Decodable { | |
let deactivated: Bool | |
let error: String? | |
} | |
@MainActor | |
class LicenseManager: ObservableObject { | |
@AppStorage("shouldShowActivateLicense") var shouldShowActivateLicense = true | |
@AppStorage("licenseStatus") var licenseStatus: String? | |
@AppStorage("isValid") var isValid = false | |
@AppStorage("licenseKey") var licenseKey = "" | |
@AppStorage("instanceId") var instanceId = "" | |
@AppStorage("expiryDate") var expiryDate = "" | |
@AppStorage("activation_limit") var activation_limit = 0 | |
@AppStorage("activation_usage") var activation_usage = 0 | |
let product_id_1_device = xxx | |
let product_id_2_devices = yyy | |
@Published var isActivating = false | |
@Published var activateError = "" | |
@Published var validationError = "" | |
static let shared = LicenseManager() | |
@MainActor | |
func activate(key: String) async -> Bool { | |
isActivating = true | |
licenseKey = key | |
activateError = "" | |
let baseUrlString = "https://api.lemonsqueezy.com/v1/licenses/activate" | |
let instanceName = getDeviceUUID() ?? UUID().uuidString | |
var urlComponents = URLComponents(string: baseUrlString)! | |
urlComponents.queryItems = [ | |
URLQueryItem(name: "license_key", value: key), | |
URLQueryItem(name: "instance_name", value: instanceName) | |
] | |
var request = URLRequest(url: urlComponents.url!) | |
request.httpMethod = "POST" | |
request.addValue("application/json", forHTTPHeaderField: "Accept") | |
do { | |
let (data, _) = try await URLSession.shared.data(for: request) | |
let model = try JSONDecoder().decode(LicenseActivateModel.self, from: data) | |
if model.meta?.product_id == product_id_1_device || model.meta?.product_id == product_id_2_devices { | |
instanceId = model.instance?.id ?? "" | |
expiryDate = model.license_key?.expires_at ?? "" | |
licenseStatus = model.license_key?.status | |
activation_limit = model.license_key?.activation_limit ?? 0 | |
activation_usage = model.license_key?.activation_usage ?? 0 | |
isValid = model.activated || (licenseStatus != nil && licenseStatus == LicenseStatus.expired.rawValue && activation_usage < activation_limit) | |
} | |
activateError = model.error ?? "" | |
} catch { | |
activateError = error.localizedDescription | |
} | |
isActivating = false | |
if !isValid { | |
licenseKey = "" | |
} else { | |
shouldShowActivateLicense = false | |
} | |
return isValid | |
} | |
@MainActor | |
func validate() async { | |
guard !licenseKey.isEmpty && !instanceId.isEmpty else { return } | |
let baseUrlString = "https://api.lemonsqueezy.com/v1/licenses/validate" | |
var urlComponents = URLComponents(string: baseUrlString)! | |
urlComponents.queryItems = [ | |
URLQueryItem(name: "license_key", value: licenseKey), | |
URLQueryItem(name: "instance_id", value: instanceId) | |
] | |
var request = URLRequest(url: urlComponents.url!) | |
request.httpMethod = "POST" | |
request.addValue("application/json", forHTTPHeaderField: "Accept") | |
do { | |
let (data, _) = try await URLSession.shared.data(for: request) | |
let model = try JSONDecoder().decode(LicenseValidateModel.self, from: data) | |
if let id = model.instance?.id, !id.isEmpty { | |
instanceId = id | |
} | |
if let expiry = model.license_key?.expires_at { | |
expiryDate = expiry | |
} | |
if let status = model.license_key?.status { | |
licenseStatus = status | |
} | |
if let limit = model.license_key?.activation_limit { | |
activation_limit = limit | |
} | |
if let usage = model.license_key?.activation_usage { | |
activation_usage = usage | |
} | |
isValid = model.valid || (licenseStatus != nil && licenseStatus == LicenseStatus.expired.rawValue) | |
validationError = model.error ?? "" | |
} catch { | |
} | |
} | |
@MainActor | |
func deactivate() async { | |
let baseUrlString = "https://api.lemonsqueezy.com/v1/licenses/deactivate" | |
var urlComponents = URLComponents(string: baseUrlString)! | |
urlComponents.queryItems = [ | |
URLQueryItem(name: "license_key", value: licenseKey), | |
URLQueryItem(name: "instance_id", value: instanceId) | |
] | |
var request = URLRequest(url: urlComponents.url!) | |
request.httpMethod = "POST" | |
request.addValue("application/json", forHTTPHeaderField: "Accept") | |
reset() | |
do { | |
let (data, _) = try await URLSession.shared.data(for: request) | |
let _ = try JSONDecoder().decode(LicenseDeactivateModel.self, from: data) | |
} catch { | |
} | |
} | |
func getDeviceUUID() -> String? { | |
let service = IOServiceGetMatchingService(kIOMainPortDefault, | |
IOServiceMatching("IOPlatformExpertDevice")) | |
var modelIdentifier: String? | |
if let modelData = IORegistryEntryCreateCFProperty(service, "model" as CFString, kCFAllocatorDefault, 0).takeRetainedValue() as? Data { | |
if let modelIdentifierCString = String(data: modelData, encoding: .utf8)?.cString(using: .utf8) { | |
modelIdentifier = String(cString: modelIdentifierCString) | |
} | |
} | |
IOObjectRelease(service) | |
return modelIdentifier | |
} | |
func reset() { | |
isValid = false | |
licenseStatus = nil | |
licenseKey = "" | |
instanceId = "" | |
expiryDate = "" | |
activation_limit = 0 | |
activation_usage = 0 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment