import Foundation
|
import AlignerCore
|
|
struct Round015EntitlementLaunchOptions {
|
let fixtureState: EntitlementFixtureState?
|
let now: Date?
|
let trialStartedAt: Date?
|
let lastSuccessfulVerifyAt: Date?
|
let verifyResult: VerifyResult?
|
let usesEphemeralLicenseStore: Bool
|
let debugLicenseKey: DebugLicenseKey?
|
|
enum VerifyResult: String {
|
case success
|
case networkFailure
|
case invalid
|
case revoked
|
case serverError
|
case timeout
|
}
|
|
enum DebugLicenseKey: String {
|
case valid
|
case invalid
|
case revoked
|
}
|
|
static func parse(arguments: [String]) -> Round015EntitlementLaunchOptions {
|
Round015EntitlementLaunchOptions(
|
fixtureState: stringValue(for: "--round015-license-state", in: arguments)
|
.flatMap(EntitlementFixtureState.init(rawValue:)),
|
now: dateValue(for: "--round015-now", in: arguments),
|
trialStartedAt: dateValue(for: "--round015-trial-started-at", in: arguments),
|
lastSuccessfulVerifyAt: dateValue(for: "--round015-last-successful-verify-at", in: arguments),
|
verifyResult: stringValue(for: "--round015-verify-result", in: arguments)
|
.flatMap(VerifyResult.init(rawValue:)),
|
usesEphemeralLicenseStore: arguments.contains("--round015-ephemeral-license-store"),
|
debugLicenseKey: stringValue(for: "--round015-debug-license-key", in: arguments)
|
.flatMap(DebugLicenseKey.init(rawValue:))
|
)
|
}
|
|
func authorizationReportDictionary() -> [String: Any] {
|
guard let snapshot else {
|
return [
|
"state": "notConfigured",
|
"proFeature": ProFeature.windowShortcutActivation.rawValue,
|
"proFeatureUnlocked": false,
|
"fixtureEnabled": false,
|
"ephemeralLicenseStore": usesEphemeralLicenseStore,
|
"licenseKeyPresent": debugLicenseKey != nil,
|
"licenseKeyFingerprint": debugLicenseKey.map { DevelopmentDiagnostics.stableFingerprint($0.rawValue) } ?? NSNull()
|
]
|
}
|
|
let evaluationNow = effectiveNow
|
let entitlement = EntitlementPolicy.evaluate(
|
snapshot: snapshot,
|
feature: .windowShortcutActivation,
|
now: evaluationNow
|
)
|
|
var report: [String: Any] = [
|
"state": stateLabel(entitlement),
|
"proFeature": ProFeature.windowShortcutActivation.rawValue,
|
"proFeatureUnlocked": entitlement.isProAllowed,
|
"decision": decisionLabel(entitlement),
|
"decisionReason": decisionReasonLabel(entitlement),
|
"fixtureEnabled": fixtureState != nil,
|
"fixtureState": fixtureState?.rawValue ?? NSNull(),
|
"ephemeralLicenseStore": usesEphemeralLicenseStore,
|
"now": Self.string(from: evaluationNow),
|
"trialStartedAt": trialStartedAtString(from: snapshot.trialState) ?? NSNull(),
|
"trialEndsAt": trialEndsAtString(from: snapshot.trialState) ?? NSNull(),
|
"trialDaysRemaining": trialDaysRemaining(from: snapshot.trialState, now: evaluationNow) ?? NSNull(),
|
"lastVerifyResult": lastVerifyResultLabel(from: snapshot.validationFreshness),
|
"lastSuccessfulVerifyAt": lastSuccessfulVerifyAtString(from: snapshot.validationFreshness) ?? NSNull(),
|
"offlineGraceEndsAt": offlineGraceEndsAtString(from: snapshot.validationFreshness) ?? NSNull(),
|
"offlineGraceRemainingDays": offlineGraceRemainingDays(from: snapshot.validationFreshness, now: evaluationNow) ?? NSNull(),
|
"licenseKeyPresent": debugLicenseKey != nil || licenseKeyPresent(in: snapshot.licenseState),
|
"licenseKeyFingerprint": licenseFingerprint(from: snapshot.licenseState)
|
?? debugLicenseKey.map { DevelopmentDiagnostics.stableFingerprint($0.rawValue) }
|
?? NSNull()
|
]
|
|
if let verifyResult {
|
report["fixtureVerifyResult"] = verifyResult.rawValue
|
}
|
if let debugLicenseKey {
|
report["debugLicenseKeyKind"] = debugLicenseKey.rawValue
|
}
|
|
return report
|
}
|
|
private var effectiveNow: Date {
|
now ?? Date()
|
}
|
|
private var snapshot: EntitlementSnapshot? {
|
guard fixtureState != nil
|
|| trialStartedAt != nil
|
|| lastSuccessfulVerifyAt != nil
|
|| verifyResult != nil
|
|| debugLicenseKey != nil
|
else {
|
return nil
|
}
|
|
if fixtureState == nil {
|
return snapshotFromOverrides(base: EntitlementFixtureState.trialExpired)
|
}
|
|
return snapshotFromOverrides(base: fixtureState ?? .trialExpired)
|
}
|
|
private func snapshotFromOverrides(base fixtureState: EntitlementFixtureState) -> EntitlementSnapshot {
|
var snapshot = FixtureEntitlementProvider(
|
fixtureState: fixtureState,
|
now: effectiveNow
|
).snapshot
|
|
if let trialStartedAt {
|
snapshot.trialState = trialState(
|
from: snapshot.trialState,
|
startedAt: trialStartedAt,
|
now: effectiveNow
|
)
|
}
|
|
if let debugLicenseKey {
|
snapshot.licenseState = licenseState(for: debugLicenseKey)
|
}
|
|
if let verifyResult {
|
snapshot.validationFreshness = validationFreshness(
|
for: verifyResult,
|
now: effectiveNow,
|
lastSuccessfulVerifyAt: lastSuccessfulVerifyAt
|
)
|
} else if let lastSuccessfulVerifyAt {
|
snapshot.validationFreshness = .valid(
|
lastValidAt: lastSuccessfulVerifyAt,
|
nextCheckAt: lastSuccessfulVerifyAt.addingTimeInterval(24 * 60 * 60)
|
)
|
}
|
|
return snapshot
|
}
|
|
private func trialState(from base: TrialState, startedAt: Date, now: Date) -> TrialState {
|
let expiresAt = startedAt.addingTimeInterval(EntitlementPolicy.defaultTrialDuration)
|
switch base {
|
case .expired:
|
return now < expiresAt
|
? .active(startedAt: startedAt, expiresAt: expiresAt)
|
: .expired(startedAt: startedAt, expiredAt: expiresAt)
|
default:
|
return .active(startedAt: startedAt, expiresAt: expiresAt)
|
}
|
}
|
|
private func licenseState(for debugLicenseKey: DebugLicenseKey) -> LicenseState {
|
switch debugLicenseKey {
|
case .valid:
|
return .activated(
|
fingerprint: DevelopmentDiagnostics.stableFingerprint("round015-valid"),
|
instanceID: "round015-fixture-instance",
|
status: .active
|
)
|
case .invalid:
|
return .revokedOrExpiredRemote(reason: .invalid)
|
case .revoked:
|
return .revokedOrExpiredRemote(reason: .revoked)
|
}
|
}
|
|
private func validationFreshness(
|
for verifyResult: VerifyResult,
|
now: Date,
|
lastSuccessfulVerifyAt: Date?
|
) -> ValidationFreshness {
|
switch verifyResult {
|
case .success:
|
let lastValidAt = lastSuccessfulVerifyAt ?? now
|
return .valid(
|
lastValidAt: lastValidAt,
|
nextCheckAt: lastValidAt.addingTimeInterval(24 * 60 * 60)
|
)
|
case .networkFailure, .serverError, .timeout:
|
return EntitlementPolicy.offlineGrace(lastAttemptAt: now)
|
case .invalid:
|
return .hardFailure(reason: .invalid)
|
case .revoked:
|
return .hardFailure(reason: .revoked)
|
}
|
}
|
|
private func stateLabel(_ entitlement: EffectiveEntitlement) -> String {
|
switch entitlement {
|
case let .proAllowed(reason):
|
return reason.rawValue
|
case let .proDenied(reason):
|
return reason.rawValue
|
}
|
}
|
|
private func decisionLabel(_ entitlement: EffectiveEntitlement) -> String {
|
entitlement.isProAllowed ? "allowed" : "blocked"
|
}
|
|
private func decisionReasonLabel(_ entitlement: EffectiveEntitlement) -> String {
|
switch entitlement {
|
case let .proAllowed(reason):
|
return reason.rawValue
|
case let .proDenied(reason):
|
return reason.rawValue
|
}
|
}
|
|
private func trialStartedAtString(from state: TrialState) -> String? {
|
switch state {
|
case let .active(startedAt, _), let .expired(startedAt, _):
|
return Self.string(from: startedAt)
|
case .notInitialized, .invalidLocalRecord:
|
return nil
|
}
|
}
|
|
private func trialEndsAtString(from state: TrialState) -> String? {
|
switch state {
|
case let .active(_, expiresAt), let .expired(_, expiresAt):
|
return Self.string(from: expiresAt)
|
case .notInitialized, .invalidLocalRecord:
|
return nil
|
}
|
}
|
|
private func trialDaysRemaining(from state: TrialState, now: Date) -> Int? {
|
guard case let .active(_, expiresAt) = state else { return nil }
|
return max(0, Int(ceil(expiresAt.timeIntervalSince(now) / (24 * 60 * 60))))
|
}
|
|
private func lastVerifyResultLabel(from freshness: ValidationFreshness) -> String {
|
switch freshness {
|
case .neverChecked:
|
return "neverChecked"
|
case .valid:
|
return "success"
|
case .temporaryFailure:
|
return "temporaryFailure"
|
case let .hardFailure(reason):
|
return reason.rawValue
|
}
|
}
|
|
private func lastSuccessfulVerifyAtString(from freshness: ValidationFreshness) -> String? {
|
switch freshness {
|
case let .valid(lastValidAt, _):
|
return Self.string(from: lastValidAt)
|
case .neverChecked, .temporaryFailure, .hardFailure:
|
return nil
|
}
|
}
|
|
private func offlineGraceEndsAtString(from freshness: ValidationFreshness) -> String? {
|
switch freshness {
|
case let .temporaryFailure(_, graceUntil):
|
return Self.string(from: graceUntil)
|
case .neverChecked, .valid, .hardFailure:
|
return nil
|
}
|
}
|
|
private func offlineGraceRemainingDays(from freshness: ValidationFreshness, now: Date) -> Int? {
|
guard case let .temporaryFailure(_, graceUntil) = freshness else { return nil }
|
return max(0, Int(ceil(graceUntil.timeIntervalSince(now) / (24 * 60 * 60))))
|
}
|
|
private func licenseKeyPresent(in state: LicenseState) -> Bool {
|
switch state {
|
case .activated:
|
return true
|
case .none, .deactivatedLocal, .revokedOrExpiredRemote, .mismatch:
|
return false
|
}
|
}
|
|
private func licenseFingerprint(from state: LicenseState) -> String? {
|
switch state {
|
case let .activated(fingerprint, _, _):
|
return fingerprint
|
case .none, .deactivatedLocal, .revokedOrExpiredRemote, .mismatch:
|
return nil
|
}
|
}
|
|
private static func dateValue(for key: String, in arguments: [String]) -> Date? {
|
guard let value = stringValue(for: key, in: arguments) else { return nil }
|
|
return iso8601Date(from: value, fractionalSeconds: true)
|
?? iso8601Date(from: value, fractionalSeconds: false)
|
}
|
|
private static func stringValue(for key: String, in arguments: [String]) -> String? {
|
let prefix = "\(key)="
|
guard let argument = arguments.first(where: { $0.hasPrefix(prefix) }) else {
|
return nil
|
}
|
|
return String(argument.dropFirst(prefix.count))
|
}
|
|
private static func string(from date: Date) -> String {
|
let formatter = ISO8601DateFormatter()
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
return formatter.string(from: date)
|
}
|
|
private static func iso8601Date(from value: String, fractionalSeconds: Bool) -> Date? {
|
let formatter = ISO8601DateFormatter()
|
formatter.formatOptions = fractionalSeconds
|
? [.withInternetDateTime, .withFractionalSeconds]
|
: [.withInternetDateTime]
|
return formatter.date(from: value)
|
}
|
}
|