Skip to content

Commit 7751d37

Browse files
authored
feat: license UX improvements (#390)
* feat: license UX improvements — expiry warning, activation management, validation retry * fix: address PR review — extract pricing URL constant, add access control, log errors
1 parent 1aa2ebf commit 7751d37

8 files changed

Lines changed: 281 additions & 4 deletions

File tree

TablePro/Core/Services/Licensing/LicenseAPIClient.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ final class LicenseAPIClient {
5252
return try await post(url: url, body: request)
5353
}
5454

55+
/// List all activations for a license key
56+
func listActivations(licenseKey: String, machineId: String) async throws -> ListActivationsResponse {
57+
let url = baseURL.appendingPathComponent("activations")
58+
let body = LicenseValidationRequest(licenseKey: licenseKey, machineId: machineId)
59+
return try await post(url: url, body: body)
60+
}
61+
5562
/// Deactivate a license key from this machine
5663
func deactivate(request: LicenseDeactivationRequest) async throws {
5764
let url = baseURL.appendingPathComponent("deactivate")

TablePro/Core/Services/Licensing/LicenseManager+Pro.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ extension LicenseManager {
2222
switch status {
2323
case .expired:
2424
return .expired
25+
case .validationFailed:
26+
return .validationFailed
2527
default:
2628
return .unlicensed
2729
}

TablePro/Core/Services/Licensing/LicenseManager.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,17 @@ final class LicenseManager {
191191

192192
// MARK: - Re-validation
193193

194+
var isExpiringSoon: Bool {
195+
guard let days = license?.daysUntilExpiry else { return false }
196+
return days >= 0 && days <= 7
197+
}
198+
199+
var daysUntilExpiry: Int? {
200+
license?.daysUntilExpiry
201+
}
202+
194203
/// Periodic re-validation: refresh license from server, fall back to offline grace period
195-
private func revalidate() async {
204+
func revalidate() async {
196205
guard let license else { return }
197206

198207
isValidating = true

TablePro/Models/Settings/License.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,37 @@ struct LicenseAPIErrorResponse: Codable {
131131
let message: String
132132
}
133133

134+
/// Information about a single license activation (machine)
135+
internal struct LicenseActivationInfo: Codable, Identifiable {
136+
var id: String { machineId }
137+
let machineId: String
138+
let machineName: String
139+
let appVersion: String
140+
let osVersion: String
141+
let lastValidatedAt: String?
142+
let createdAt: String
143+
144+
private enum CodingKeys: String, CodingKey {
145+
case machineId = "machine_id"
146+
case machineName = "machine_name"
147+
case appVersion = "app_version"
148+
case osVersion = "os_version"
149+
case lastValidatedAt = "last_validated_at"
150+
case createdAt = "created_at"
151+
}
152+
}
153+
154+
/// Response from the list activations endpoint
155+
internal struct ListActivationsResponse: Codable {
156+
let activations: [LicenseActivationInfo]
157+
let maxActivations: Int
158+
159+
private enum CodingKeys: String, CodingKey {
160+
case activations
161+
case maxActivations = "max_activations"
162+
}
163+
}
164+
134165
// MARK: - Cached License
135166

136167
/// Local cached license with metadata for offline use
@@ -151,6 +182,12 @@ struct License: Codable, Equatable {
151182
return expiresAt < Date()
152183
}
153184

185+
/// Days until the license expires (nil for lifetime licenses)
186+
var daysUntilExpiry: Int? {
187+
guard let expiresAt else { return nil }
188+
return Calendar.current.dateComponents([.day], from: Date(), to: expiresAt).day
189+
}
190+
154191
/// Days since last successful server validation
155192
var daysSinceLastValidation: Int {
156193
Calendar.current.dateComponents([.day], from: lastValidatedAt, to: Date()).day ?? 0

TablePro/Models/Settings/ProFeature.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ internal enum ProFeatureAccess {
3838
case available
3939
case unlicensed
4040
case expired
41+
case validationFailed
4142
}

0 commit comments

Comments
 (0)