-
-
Notifications
You must be signed in to change notification settings - Fork 286
Expand file tree
/
Copy pathLicense.swift
More file actions
235 lines (207 loc) · 6.98 KB
/
License.swift
File metadata and controls
235 lines (207 loc) · 6.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
//
// License.swift
// TablePro
//
// License model, signed payload types, and error definitions
//
import Foundation
// MARK: - License Status
/// Represents the current license state in the app
enum LicenseStatus: String, Codable {
case unlicensed
case active
case expired
case suspended
case deactivated
case validationFailed
var displayName: String {
switch self {
case .unlicensed: return String(localized: "Unlicensed")
case .active: return String(localized: "Active")
case .expired: return String(localized: "Expired")
case .suspended: return String(localized: "Suspended")
case .deactivated: return String(localized: "Deactivated")
case .validationFailed: return String(localized: "Validation Failed")
}
}
var isValid: Bool {
self == .active
}
}
// MARK: - Server Response Types
/// The `data` portion of the signed license payload from the server
struct LicensePayloadData: Codable, Equatable {
let billingCycle: String?
let licenseKey: String
let email: String
let status: String
let expiresAt: String?
let issuedAt: String
let tier: String
private enum CodingKeys: String, CodingKey {
case billingCycle = "billing_cycle"
case licenseKey = "license_key"
case email
case status
case expiresAt = "expires_at"
case issuedAt = "issued_at"
case tier
}
/// Custom encode to explicitly write null for nil optionals.
/// The auto-synthesized Codable uses encodeIfPresent which omits nil keys,
/// but PHP's json_encode includes null values — the signed JSON must match exactly.
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if let billingCycle {
try container.encode(billingCycle, forKey: .billingCycle)
} else {
try container.encodeNil(forKey: .billingCycle)
}
try container.encode(licenseKey, forKey: .licenseKey)
try container.encode(email, forKey: .email)
try container.encode(status, forKey: .status)
if let expiresAt {
try container.encode(expiresAt, forKey: .expiresAt)
} else {
try container.encodeNil(forKey: .expiresAt)
}
try container.encode(issuedAt, forKey: .issuedAt)
try container.encode(tier, forKey: .tier)
}
}
/// Signed license payload returned by the server (data + RSA signature)
struct SignedLicensePayload: Codable, Equatable {
let data: LicensePayloadData
let signature: String
}
// MARK: - API Request/Response Types
/// Request body for license activation
struct LicenseActivationRequest: Codable {
let licenseKey: String
let machineId: String
let machineName: String
let appVersion: String
let osVersion: String
private enum CodingKeys: String, CodingKey {
case licenseKey = "license_key"
case machineId = "machine_id"
case machineName = "machine_name"
case appVersion = "app_version"
case osVersion = "os_version"
}
}
/// Request body for license validation
struct LicenseValidationRequest: Codable {
let licenseKey: String
let machineId: String
private enum CodingKeys: String, CodingKey {
case licenseKey = "license_key"
case machineId = "machine_id"
}
}
/// Request body for license deactivation
struct LicenseDeactivationRequest: Codable {
let licenseKey: String
let machineId: String
private enum CodingKeys: String, CodingKey {
case licenseKey = "license_key"
case machineId = "machine_id"
}
}
/// Wrapper for API error responses
struct LicenseAPIErrorResponse: Codable {
let message: String
}
// MARK: - Cached License
/// Local cached license with metadata for offline use
struct License: Codable, Equatable {
var key: String
var email: String
var status: LicenseStatus
var expiresAt: Date?
var lastValidatedAt: Date
var machineId: String
var signedPayload: SignedLicensePayload
var tier: String
var billingCycle: String?
/// Whether the license has expired based on expiration date
var isExpired: Bool {
guard let expiresAt else { return false }
return expiresAt < Date()
}
/// Days since last successful server validation
var daysSinceLastValidation: Int {
Calendar.current.dateComponents([.day], from: lastValidatedAt, to: Date()).day ?? 0
}
private static let iso8601Formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime]
return formatter
}()
/// Create a License from a verified server payload
static func from(
payload: LicensePayloadData,
signedPayload: SignedLicensePayload,
machineId: String
) -> License {
let expiresAt = payload.expiresAt.flatMap { iso8601Formatter.date(from: $0) }
let status: LicenseStatus = switch payload.status {
case "active": .active
case "expired": .expired
case "suspended": .suspended
default: .validationFailed
}
return License(
key: payload.licenseKey,
email: payload.email,
status: status,
expiresAt: expiresAt,
lastValidatedAt: Date(),
machineId: machineId,
signedPayload: signedPayload,
tier: payload.tier,
billingCycle: payload.billingCycle
)
}
}
// MARK: - License Error
/// Errors that can occur during license operations
enum LicenseError: LocalizedError {
case invalidKey
case signatureInvalid
case publicKeyNotFound
case publicKeyInvalid
case activationLimitReached
case licenseExpired
case licenseSuspended
case notActivated
case networkError(Error)
case serverError(Int, String)
case decodingError(Error)
var errorDescription: String? {
switch self {
case .invalidKey:
return "The license key is invalid."
case .signatureInvalid:
return "License signature verification failed."
case .publicKeyNotFound:
return "License public key not found in app bundle."
case .publicKeyInvalid:
return "License public key is invalid."
case .activationLimitReached:
return "Maximum number of activations reached."
case .licenseExpired:
return "The license has expired."
case .licenseSuspended:
return "The license has been suspended."
case .notActivated:
return "This machine is not activated."
case .networkError(let error):
return "Network error: \(error.localizedDescription)"
case .serverError(let code, let message):
return "Server error (\(code)): \(message)"
case .decodingError(let error):
return "Failed to parse server response: \(error.localizedDescription)"
}
}
}