diff --git a/sxitch/ContentView.swift b/sxitch/ContentView.swift index 761a3f8..b3352fd 100644 --- a/sxitch/ContentView.swift +++ b/sxitch/ContentView.swift @@ -288,7 +288,7 @@ struct ContentView: View { let windowPickerEnabled = UserDefaults.standard.bool(forKey: "windowPickerEnabled") let currentMode = appState.mode - if windows.count > 1, userState.shared.isPro, windowPickerEnabled { + if windows.count > 1, userState.shared.hasFullAccess, windowPickerEnabled { appState.drillDownApp = runningApp } else if windows.count == 1 { let theme = ModeTheme.theme(for: currentMode) diff --git a/sxitch/Core/AppDelegate.swift b/sxitch/Core/AppDelegate.swift index d4e0693..6ce4ae0 100644 --- a/sxitch/Core/AppDelegate.swift +++ b/sxitch/Core/AppDelegate.swift @@ -173,7 +173,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } private func toggleMode(_ mode: AppMode) { - guard window.isVisible, proState.isPro else { + guard window.isVisible, proState.hasFullAccess else { closeWindow() let alert = NSAlert() alert.messageText = "Hide and Quit modes are disabled" @@ -215,7 +215,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { // App-launch hotkeys are Pro-only. The handler is registered regardless so the // shortcut stays claimed and survives the async licence check, but it only acts // once a licence is present. - guard let self, self.proState.isPro else { return } + guard let self, self.proState.hasFullAccess else { return } guard let url = URL(string: bundleURL) else { return } let config = NSWorkspace.OpenConfiguration() config.activates = true @@ -225,7 +225,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { func registerModeHotkeys() { let modes = CustomModeStore.load() - let eligible = proState.isPro + let eligible = proState.hasFullAccess ? modes : Array(modes.prefix(CustomModeStore.freeModeLimit)) let eligibleIDs = Set(eligible.map { $0.id.uuidString }) @@ -354,7 +354,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { if appState.mode == .normal { closeWindow() } - } else if windows.count > 1, proState.isPro, windowPickerEnabled { + } else if windows.count > 1, proState.hasFullAccess, windowPickerEnabled { appState.drillDownApp = running } else { theme.appAction(entry) @@ -442,6 +442,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func applicationDidFinishLaunching(_: Notification) { + proState.beginTrialIfNeeded() + window = NSPanel( contentRect: NSRect(x: 0, y: 0, width: 0, height: 0), styleMask: [.borderless, .fullSizeContentView, .nonactivatingPanel], @@ -834,7 +836,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { if let letter = typedCharacter(from: event) { let raw = String(letter) let pickerChar: String - if proState.isPro { + if proState.hasFullAccess { let overrides = UserDefaults.standard.keyOverrides pickerChar = overrides[raw] ?? raw } else { diff --git a/sxitch/Core/RunningApp.swift b/sxitch/Core/RunningApp.swift index 834eda6..b5011fc 100644 --- a/sxitch/Core/RunningApp.swift +++ b/sxitch/Core/RunningApp.swift @@ -73,7 +73,7 @@ struct RunningApp: SwitchableApp, Equatable { return deduped .filter { app in app.app.activationPolicy == .regular - && (!blacklist.contains(app.appName.lowercased()) || !usState.isPro) + && (!blacklist.contains(app.appName.lowercased()) || !usState.hasFullAccess) } .sorted { $0.appName < $1.appName } } diff --git a/sxitch/OnboardingView.swift b/sxitch/OnboardingView.swift index 9e9b45a..73573d9 100644 --- a/sxitch/OnboardingView.swift +++ b/sxitch/OnboardingView.swift @@ -722,34 +722,103 @@ struct LicensePage: View { @State private var isActivating = false @State private var errorMessage: String? @State private var justActivated = false - @State private var skipVisible = false + @State private var justStartedTrial = false + @State private var showingKeyEntry = false + + private var showsConfirmation: Bool { + justActivated || justStartedTrial || appState.hasFullAccess + } var body: some View { VStack(spacing: 18) { - if justActivated { - activatedContent + if showsConfirmation { + confirmationContent + } else if showingKeyEntry { + keyEntryContent } else { - offerContent - skipContent + choiceContent } } .padding(32) .frame(maxWidth: .infinity, maxHeight: .infinity) - .task { - // Skip appears after a short beat so the offer gets a fair look first. - try? await Task.sleep(for: .seconds(1.2)) - withAnimation(.easeOut(duration: 0.3)) { - skipVisible = true + .animation(.easeOut(duration: 0.3), value: showingKeyEntry) + .animation(.spring(response: 0.4, dampingFraction: 0.75), value: showsConfirmation) + } + + // MARK: Choice state + + private var choiceContent: some View { + VStack(spacing: 16) { + VStack(spacing: 8) { + Text("Choose Your Sxitch") + .font(.largeTitle) + .fontWeight(.bold) + + Text("Try everything Pro has to offer, unlock it with a key, or keep the free version. You can always change this later in Settings.") + .font(.body) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .frame(maxWidth: 440) + } + + VStack(spacing: 10) { + Button { + appState.startTrial() + withAnimation(.spring(response: 0.4, dampingFraction: 0.75)) { + justStartedTrial = true + } + } label: { + VStack(spacing: 3) { + Text("Start 14-Day Pro Trial") + .font(.headline) + Text("All Pro features free for 14 days. No card required.") + .font(.caption) + .opacity(0.7) + } + .frame(maxWidth: 380) + } + .buttonStyle(WhitishProminentStyle()) + + Button { + withAnimation(.easeOut(duration: 0.3)) { + showingKeyEntry = true + } + } label: { + VStack(spacing: 3) { + Text("Enter Activation Key") + .font(.headline) + Text("Already purchased? Unlock Pro with your license key.") + .font(.caption) + .opacity(0.7) + } + .frame(maxWidth: 380) + } + .buttonStyle(WhitishOutlineStyle()) + + Button { + onSkip() + } label: { + VStack(spacing: 3) { + Text("Use in Free Mode") + .font(.headline) + Text("Core switching features stay free, forever.") + .font(.caption) + .opacity(0.7) + } + .frame(maxWidth: 380) + } + .buttonStyle(WhitishGhostStyle()) } } + .transition(.opacity) } - // MARK: Offer state + // MARK: Key entry state - private var offerContent: some View { + private var keyEntryContent: some View { VStack(spacing: 16) { VStack(spacing: 8) { - Text("Activate License Key") + Text("Enter Activation Key") .font(.largeTitle) .fontWeight(.bold) @@ -771,7 +840,7 @@ struct LicensePage: View { HStack(spacing: 8) { ProgressView() .controlSize(.small) - Text("Activating…") + Text("Activating") } } else { Text("Activate") @@ -781,7 +850,15 @@ struct LicensePage: View { .disabled( licenseKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isActivating ) + } + if let errorMessage { + Text(errorMessage) + .font(.caption) + .foregroundStyle(.red) + } + + HStack(spacing: 12) { Button { if let url = URL(string: "https://sxitch.app/#pricing") { NSWorkspace.shared.open(url) @@ -790,20 +867,30 @@ struct LicensePage: View { Text("Get Pro") } .buttonStyle(WhitishOutlineStyle()) - } - if let errorMessage { - Text(errorMessage) - .font(.caption) - .foregroundStyle(.red) + Button { + withAnimation(.easeOut(duration: 0.3)) { + showingKeyEntry = false + } + } label: { + Text("Back to Options") + } + .buttonStyle(WhitishGhostStyle()) + + Button { + onSkip() + } label: { + Text("Skip") + } + .buttonStyle(WhitishGhostStyle()) } } .transition(.opacity) } - // MARK: Activated state + // MARK: Confirmation state - private var activatedContent: some View { + private var confirmationContent: some View { VStack(spacing: 18) { ZStack { ConfettiBurst(tint: accent) @@ -812,13 +899,23 @@ struct LicensePage: View { .frame(height: 100) VStack(spacing: 8) { - Text("You're Pro! 🎉") - .font(.largeTitle) - .fontWeight(.bold) + if justActivated || appState.isPro { + Text("You're Pro! 🎉") + .font(.largeTitle) + .fontWeight(.bold) + + Text("Thank you for supporting Sxitch's development.") + .font(.body) + .foregroundStyle(.secondary) + } else { + Text("Trial Started! 🎉") + .font(.largeTitle) + .fontWeight(.bold) - Text("Thank you for supporting Sxitch's development.") - .font(.body) - .foregroundStyle(.secondary) + Text("All Pro features are unlocked for the next \(userState.trialLengthDays) days. Enjoy!") + .font(.body) + .foregroundStyle(.secondary) + } } Button("Continue") { @@ -829,19 +926,6 @@ struct LicensePage: View { .transition(.scale(scale: 0.9).combined(with: .opacity)) } - // MARK: Skip - - @ViewBuilder - private var skipContent: some View { - if skipVisible { - Button("Skip") { - onSkip() - } - .buttonStyle(WhitishGhostStyle()) - .animation(.easeOut(duration: 0.4), value: skipVisible) - } - } - // MARK: Actions private func performActivation() async { @@ -856,7 +940,6 @@ struct LicensePage: View { licenseKey = "" withAnimation(.spring(response: 0.4, dampingFraction: 0.75)) { justActivated = true - skipVisible = true } } else { errorMessage = "Invalid license key or activation limit reached." @@ -900,7 +983,13 @@ struct FinishPage: View { } HStack(spacing: 12) { - if !userState.shared.isPro { + if userState.shared.isPro { + // Nothing extra to upsell for licensed users. + } else if userState.shared.isTrialActive { + Text("\(userState.shared.trialDaysRemaining) days of Pro left") + .font(.subheadline) + .foregroundStyle(.secondary) + } else { Link(destination: URL(string: "https://sxitch.app/#pricing")!) { Label("Get Sxitch Pro", systemImage: "star.fill") } diff --git a/sxitch/Settings/ActivateSettingsView.swift b/sxitch/Settings/ActivateSettingsView.swift index 15fc213..5e36990 100644 --- a/sxitch/Settings/ActivateSettingsView.swift +++ b/sxitch/Settings/ActivateSettingsView.swift @@ -39,12 +39,42 @@ struct ActivateSettingsView: View, SettingsTab { Button("Deactivate Device", role: .destructive) { deactivateLicense() } + } else if appState.isTrialActive { + HStack { + Image(systemName: "hourglass.bottom.filled.tentpath") + .foregroundColor(.blue) + .imageScale(.large) + VStack(alignment: .leading, spacing: 2) { + Text("Pro Trial Active") + .font(.headline) + Text( + "\(appState.trialDaysRemaining) of \(userState.trialLengthDays) days remaining. All Pro features are unlocked." + ) + .font(.subheadline) + .foregroundColor(.secondary) + } + } + .padding(.vertical, 4) } else { HStack { - Image(systemName: "xmark.seal.fill") + Image(systemName: appState.isTrialExpired ? "clock.badge.xmark" : "xmark.seal.fill") .foregroundColor(.secondary) - Text("Free Version") - .font(.headline) + VStack(alignment: .leading, spacing: 2) { + Text(appState.isTrialExpired ? "Free Version (Trial Ended)" : "Free Version") + .font(.headline) + if appState.isTrialExpired { + Text("Your 14-day Pro trial has ended.") + .font(.subheadline) + .foregroundColor(.secondary) + } + } + } + if !appState.hasTrialEverStarted { + Button { + appState.startTrial() + } label: { + Label("Start 14-Day Free Trial", systemImage: "clock.badge.checkmark") + } } } } diff --git a/sxitch/Settings/CustomModesSettingsView.swift b/sxitch/Settings/CustomModesSettingsView.swift index 99c1640..d4f7861 100644 --- a/sxitch/Settings/CustomModesSettingsView.swift +++ b/sxitch/Settings/CustomModesSettingsView.swift @@ -9,11 +9,11 @@ struct CustomModesSettingsView: View { private var usState = userState.shared private var atLimit: Bool { - !usState.isPro && modes.count >= CustomModeStore.freeModeLimit + !usState.hasFullAccess && modes.count >= CustomModeStore.freeModeLimit } private var canAdd: Bool { - usState.isPro || modes.count < CustomModeStore.freeModeLimit + usState.hasFullAccess || modes.count < CustomModeStore.freeModeLimit } var body: some View { diff --git a/sxitch/Settings/FilterSettingsView.swift b/sxitch/Settings/FilterSettingsView.swift index b5a7ab3..f71d4cf 100644 --- a/sxitch/Settings/FilterSettingsView.swift +++ b/sxitch/Settings/FilterSettingsView.swift @@ -17,7 +17,7 @@ struct FilterSettingsView: View, SettingsTab { var body: some View { Form { - if appState.isPro { + if appState.hasFullAccess { ManagedListSection( addHeader: "Blacklist Apps", listHeader: "Blacklisted Apps", @@ -34,7 +34,7 @@ struct FilterSettingsView: View, SettingsTab { items: $prefixStrip ) Section("App name override") { - if !appState.isPro { + if !appState.hasFullAccess { HStack { Label("Pro", systemImage: "lock.fill") .font(.caption) @@ -49,12 +49,12 @@ struct FilterSettingsView: View, SettingsTab { HStack { TextField("Old name", text: $newAppRename) .textFieldStyle(.roundedBorder) - .disabled(!appState.isPro) + .disabled(!appState.hasFullAccess) TextField("New name", text: $newAppRenameTo) .textFieldStyle(.roundedBorder) - .disabled(!appState.isPro) + .disabled(!appState.hasFullAccess) Button("Add", systemImage: "plus") { addAppRename() } - .disabled(!appState.isPro || newAppRename.trimmingCharacters(in: .whitespaces).isEmpty || newAppRenameTo.trimmingCharacters(in: .whitespaces).isEmpty) + .disabled(!appState.hasFullAccess || newAppRename.trimmingCharacters(in: .whitespaces).isEmpty || newAppRenameTo.trimmingCharacters(in: .whitespaces).isEmpty) } if appRenames.isEmpty { @@ -84,7 +84,7 @@ struct FilterSettingsView: View, SettingsTab { Image(systemName: "trash") } .buttonStyle(.borderless) - .disabled(!appState.isPro) + .disabled(!appState.hasFullAccess) } .padding(.vertical, 2) } diff --git a/sxitch/Settings/KeyboardSettingsView.swift b/sxitch/Settings/KeyboardSettingsView.swift index 3853a74..946c702 100644 --- a/sxitch/Settings/KeyboardSettingsView.swift +++ b/sxitch/Settings/KeyboardSettingsView.swift @@ -22,21 +22,21 @@ struct KeyboardSettingsView: View, SettingsTab { Text("Hide mode") Spacer() KeyboardShortcuts.Recorder(for: .hideMode) - .disabled(!usState.isPro) + .disabled(!usState.hasFullAccess) } HStack { Text("Quit mode") Spacer() KeyboardShortcuts.Recorder(for: .quitMode) - .disabled(!usState.isPro) + .disabled(!usState.hasFullAccess) } HStack { Text("Normal mode") Spacer() KeyboardShortcuts.Recorder(for: .normalMode) - .disabled(!usState.isPro) + .disabled(!usState.hasFullAccess) } - if !usState.isPro { + if !usState.hasFullAccess { HStack { Label("Pro", systemImage: "lock.fill") .font(.caption) @@ -50,7 +50,7 @@ struct KeyboardSettingsView: View, SettingsTab { } AppHotkeySettingsView() Section("Key Overrides") { - if !usState.isPro { + if !usState.hasFullAccess { HStack { Label("Pro", systemImage: "lock.fill") .font(.caption) @@ -65,12 +65,12 @@ struct KeyboardSettingsView: View, SettingsTab { HStack { TextField("Original", text: $newOverrideOriginal) .textFieldStyle(.roundedBorder) - .disabled(!usState.isPro) + .disabled(!usState.hasFullAccess) TextField("Override", text: $newOverrideTo) .textFieldStyle(.roundedBorder) - .disabled(!usState.isPro) + .disabled(!usState.hasFullAccess) Button("Add", systemImage: "plus") { addOverride() } - .disabled(!usState.isPro || newOverrideOriginal.trimmingCharacters(in: .whitespaces).count != 1 || newOverrideTo.trimmingCharacters(in: .whitespaces).isEmpty) + .disabled(!usState.hasFullAccess || newOverrideOriginal.trimmingCharacters(in: .whitespaces).count != 1 || newOverrideTo.trimmingCharacters(in: .whitespaces).isEmpty) } if overrides.isEmpty { @@ -108,7 +108,7 @@ struct KeyboardSettingsView: View, SettingsTab { Image(systemName: "trash") } .buttonStyle(.borderless) - .disabled(!usState.isPro) + .disabled(!usState.hasFullAccess) } .padding(.vertical, 2) } diff --git a/sxitch/Settings/SettingsView.swift b/sxitch/Settings/SettingsView.swift index 463fca0..7e19e83 100644 --- a/sxitch/Settings/SettingsView.swift +++ b/sxitch/Settings/SettingsView.swift @@ -18,7 +18,7 @@ struct SettingsView: View { if tab.id == "activate" { AnySettingsTab( id: tab.id, title: tab.title, - icon: usState.isPro ? "lock.open" : "lock", + icon: usState.hasFullAccess ? "lock.open" : "lock", content: tab.content ) } else { diff --git a/sxitch/Settings/ThemeSettingsView.swift b/sxitch/Settings/ThemeSettingsView.swift index 875e32e..5b99dd6 100644 --- a/sxitch/Settings/ThemeSettingsView.swift +++ b/sxitch/Settings/ThemeSettingsView.swift @@ -91,15 +91,15 @@ struct ThemeSettingsView: View, SettingsTab { Toggle("Show picker UI", isOn: $showUi) HStack { Toggle("Window Picker", isOn: $windowPickerEnabled) - .disabled(!usState.isPro) - if !usState.isPro { + .disabled(!usState.hasFullAccess) + if !usState.hasFullAccess { Spacer() Label("Pro", systemImage: "lock.fill") .font(.caption) .foregroundStyle(.secondary) } } - if !usState.isPro { + if !usState.hasFullAccess { Text("Upgrade to Pro to pick individual windows when an app has multiple open.") .font(.caption) .foregroundStyle(.secondary) diff --git a/sxitch/Views/AppContextMenu.swift b/sxitch/Views/AppContextMenu.swift index b3182c6..b1d7ed7 100644 --- a/sxitch/Views/AppContextMenu.swift +++ b/sxitch/Views/AppContextMenu.swift @@ -35,7 +35,7 @@ extension SwitchableApp { extension View { @ViewBuilder func appContextMenu(for app: any SwitchableApp) -> some View { - if userState.shared.isPro, app.runningApplication != nil || app.isPinned { + if userState.shared.hasFullAccess, app.runningApplication != nil || app.isPinned { modifier(AppContextMenuModifier(app: app)) } else { self diff --git a/sxitch/license.swift b/sxitch/license.swift index 6c74360..3c97e20 100644 --- a/sxitch/license.swift +++ b/sxitch/license.swift @@ -181,7 +181,7 @@ func deleteCredentials() throws { // MARK: - Low-Level Keychain Operations -private func saveKeychainSecret(account: String, secret: String) throws { +func saveKeychainSecret(account: String, secret: String) throws { guard let data = secret.data(withAllowedCharacters: .utf8) else { throw KeychainError.conversionError } @@ -202,7 +202,7 @@ private func saveKeychainSecret(account: String, secret: String) throws { } } -private func readKeychainSecret(account: String) throws -> String { +func readKeychainSecret(account: String) throws -> String { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: APP_NAME, diff --git a/sxitch/sxitchApp.swift b/sxitch/sxitchApp.swift index 36c7ee4..deaf560 100644 --- a/sxitch/sxitchApp.swift +++ b/sxitch/sxitchApp.swift @@ -18,6 +18,12 @@ struct sxitchApp: App { Label { if appState.isPro { Text("Sxitch Pro") + } else if appState.isTrialActive { + Button("Sxitch Trial · \(appState.trialDaysRemaining) days left") { + if let url = URL(string: "https://sxitch.app") { + NSWorkspace.shared.open(url) + } + } } else { Button("Sxitch Free") { if let url = URL(string: "https://sxitch.app") { diff --git a/sxitch/userState.swift b/sxitch/userState.swift index 396f64c..4d9d13b 100644 --- a/sxitch/userState.swift +++ b/sxitch/userState.swift @@ -11,9 +11,14 @@ import SwiftUI final class userState { static let shared = userState() + static let trialLengthDays: Int = 14 + var isPro: Bool = false var isCheckingLicense: Bool = false + /// Date the free trial started; nil means the user has never started one. + var trialStartDate: Date? + /// Set by the onboarding layout page: while true, the summon hotkey /// toggles the real switcher (demo) instead of being suppressed. var layoutDemoActive = false @@ -22,6 +27,117 @@ final class userState { private init() {} + // MARK: - Trial + + /// The full Pro experience is unlocked by a valid license OR an active trial. + var hasFullAccess: Bool { + isPro || isTrialActive + } + + var isTrialActive: Bool { + guard let start = trialStartDate else { return false } + return Date().timeIntervalSince(start) < Self.trialLength + } + + /// Whole days left in the trial, clamped to 0. + var trialDaysRemaining: Int { + guard let start = trialStartDate else { return 0 } + let remaining = (start.addingTimeInterval(Self.trialLength).timeIntervalSinceNow) / 86400 + return max(0, Int(remaining.rounded(.up))) + } + + var hasTrialEverStarted: Bool { + trialStartDate != nil + } + + var isTrialExpired: Bool { + hasTrialEverStarted && !isTrialActive + } + + private static var trialLength: TimeInterval { + Double(trialLengthDays) * 86400 + } + + /// Starts the 14-day trial if it has not been started before. + func startTrial() { + guard trialStartDate == nil else { return } + let now = Date() + trialStartDate = now + persistTrialStart(now) + } + + /// Called on launch: restores the persisted trial date and shows the + /// expiry alert once if the trial has ended. + func beginTrialIfNeeded() { + trialStartDate = loadTrialStartDate() + showTrialExpiryAlertIfNeeded() + } + + // MARK: Trial persistence + + // The trial start date is written to the Keychain (primary, survives + // app deletion) and mirrored in UserDefaults as a fallback. If both + // exist and disagree, the earliest date wins so deleting either copy + // or rolling the clock cannot extend the trial. + + private static let trialKeychainAccount = "trial_start" + private static let trialDefaultsKey = "trial_start_mirror" + + private func persistTrialStart(_ date: Date) { + let iso = ISO8601DateFormatter().string(from: date) + UserDefaults.standard.set(iso, forKey: Self.trialDefaultsKey) + try? saveKeychainSecret(account: Self.trialKeychainAccount, secret: iso) + } + + private func loadTrialStartDate() -> Date? { + var dates: [Date] = [] + + if let iso = UserDefaults.standard.string(forKey: Self.trialDefaultsKey) { + if let date = ISO8601DateFormatter().date(from: iso) { + dates.append(date) + } + } + + if let iso = try? readKeychainSecret(account: Self.trialKeychainAccount) { + if let date = ISO8601DateFormatter().date(from: iso) { + dates.append(date) + } + } + + guard let earliest = dates.min() else { return nil } + + // Heal whichever copy is missing or drifted. + if dates.count < 2 { + persistTrialStart(earliest) + } + + return earliest + } + + // MARK: Trial expiry alert + + private static let trialExpiryAlertShownKey = "trial_expiry_alert_shown" + + func showTrialExpiryAlertIfNeeded() { + guard isTrialExpired else { return } + let defaults = UserDefaults.standard + guard !defaults.bool(forKey: Self.trialExpiryAlertShownKey) else { return } + defaults.set(true, forKey: Self.trialExpiryAlertShownKey) + + let alert = NSAlert() + alert.messageText = "Your Sxitch Pro trial has ended" + alert.informativeText = "The 14-day trial is over and Pro features are now locked. Activate a license key to keep them." + alert.addButton(withTitle: "Enter License Key") + alert.addButton(withTitle: "Not Now") + + if alert.runModal() == .alertFirstButtonReturn { + UserDefaults.standard.set("activate", forKey: "selectedSettingsTab") + NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) + } + } + + // MARK: - License + /// Call this on app launch (e.g., in your App struct's init or .task) func checkCurrentActivationStatus() async { guard !isCheckingLicense else { return }