126 lines
4.5 KiB
Swift
126 lines
4.5 KiB
Swift
import Foundation
|
|
import os
|
|
|
|
/// Manages license data using secure Keychain storage (non-syncable, device-local).
|
|
final class LicenseManager {
|
|
static let shared = LicenseManager()
|
|
|
|
private let keychain = KeychainService.shared
|
|
private let userDefaults = UserDefaults.standard
|
|
private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "LicenseManager")
|
|
|
|
private let licenseKeyIdentifier = "voiceink.license.key"
|
|
private let trialStartDateIdentifier = "voiceink.license.trialStartDate"
|
|
private let activationIdIdentifier = "voiceink.license.activationId"
|
|
private let migrationCompletedKey = "LicenseKeychainMigrationCompleted"
|
|
|
|
private init() {
|
|
migrateFromUserDefaultsIfNeeded()
|
|
}
|
|
|
|
// MARK: - License Key
|
|
|
|
var licenseKey: String? {
|
|
get { keychain.getString(forKey: licenseKeyIdentifier, syncable: false) }
|
|
set {
|
|
if let value = newValue {
|
|
keychain.save(value, forKey: licenseKeyIdentifier, syncable: false)
|
|
} else {
|
|
keychain.delete(forKey: licenseKeyIdentifier, syncable: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Trial Start Date
|
|
|
|
var trialStartDate: Date? {
|
|
get {
|
|
guard let data = keychain.getData(forKey: trialStartDateIdentifier, syncable: false),
|
|
let timestamp = String(data: data, encoding: .utf8),
|
|
let timeInterval = Double(timestamp) else {
|
|
return nil
|
|
}
|
|
return Date(timeIntervalSince1970: timeInterval)
|
|
}
|
|
set {
|
|
if let date = newValue {
|
|
let timestamp = String(date.timeIntervalSince1970)
|
|
keychain.save(timestamp, forKey: trialStartDateIdentifier, syncable: false)
|
|
} else {
|
|
keychain.delete(forKey: trialStartDateIdentifier, syncable: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Activation ID
|
|
|
|
var activationId: String? {
|
|
get { keychain.getString(forKey: activationIdIdentifier, syncable: false) }
|
|
set {
|
|
if let value = newValue {
|
|
keychain.save(value, forKey: activationIdIdentifier, syncable: false)
|
|
} else {
|
|
keychain.delete(forKey: activationIdIdentifier, syncable: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Migration
|
|
|
|
private func migrateFromUserDefaultsIfNeeded() {
|
|
guard !userDefaults.bool(forKey: migrationCompletedKey) else { return }
|
|
|
|
// Migrate license key
|
|
if let oldLicenseKey = userDefaults.string(forKey: "VoiceInkLicense"), !oldLicenseKey.isEmpty {
|
|
licenseKey = oldLicenseKey
|
|
userDefaults.removeObject(forKey: "VoiceInkLicense")
|
|
logger.info("Migrated license key to Keychain")
|
|
}
|
|
|
|
// Migrate trial start date (from obfuscated storage)
|
|
if let oldTrialDate = getObfuscatedTrialStartDate() {
|
|
trialStartDate = oldTrialDate
|
|
clearObfuscatedTrialStartDate()
|
|
logger.info("Migrated trial start date to Keychain")
|
|
}
|
|
|
|
// Migrate activation ID
|
|
if let oldActivationId = userDefaults.string(forKey: "VoiceInkActivationId"), !oldActivationId.isEmpty {
|
|
activationId = oldActivationId
|
|
userDefaults.removeObject(forKey: "VoiceInkActivationId")
|
|
logger.info("Migrated activation ID to Keychain")
|
|
}
|
|
|
|
userDefaults.set(true, forKey: migrationCompletedKey)
|
|
logger.info("License migration completed")
|
|
}
|
|
|
|
/// Reads the old obfuscated trial start date from UserDefaults.
|
|
private func getObfuscatedTrialStartDate() -> Date? {
|
|
let salt = Obfuscator.getDeviceIdentifier()
|
|
let obfuscatedKey = Obfuscator.encode("VoiceInkTrialStartDate", salt: salt)
|
|
|
|
guard let obfuscatedValue = userDefaults.string(forKey: obfuscatedKey),
|
|
let decodedValue = Obfuscator.decode(obfuscatedValue, salt: salt),
|
|
let timestamp = Double(decodedValue) else {
|
|
return nil
|
|
}
|
|
|
|
return Date(timeIntervalSince1970: timestamp)
|
|
}
|
|
|
|
/// Clears the old obfuscated trial start date from UserDefaults.
|
|
private func clearObfuscatedTrialStartDate() {
|
|
let salt = Obfuscator.getDeviceIdentifier()
|
|
let obfuscatedKey = Obfuscator.encode("VoiceInkTrialStartDate", salt: salt)
|
|
userDefaults.removeObject(forKey: obfuscatedKey)
|
|
}
|
|
|
|
/// Removes all license data (for license removal/reset).
|
|
func removeAll() {
|
|
licenseKey = nil
|
|
trialStartDate = nil
|
|
activationId = nil
|
|
}
|
|
}
|