Skip to content

Instantly share code, notes, and snippets.

@dqhieu
Created March 14, 2024 03:59
Show Gist options
  • Save dqhieu/992696b62871dee7a16e7927f73cce09 to your computer and use it in GitHub Desktop.
Save dqhieu/992696b62871dee7a16e7927f73cce09 to your computer and use it in GitHub Desktop.
Lemon Squeezy License Manager
//
// 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