Ariver
2026-06-28 71a520840eb3debe46173c1f73bc85bbea0b1aa6
src/Apptag/ProEntitlement.swift
@@ -8,6 +8,7 @@
    case layoutExport
    case unlimitedNotes
    case persistentAppSorting
    case customHotkeys
    var titleKey: String {
        switch self {
@@ -21,6 +22,8 @@
            return "pro.feature.notes"
        case .persistentAppSorting:
            return "pro.feature.sorting"
        case .customHotkeys:
            return "pro.feature.customHotkeys"
        }
    }
@@ -36,6 +39,8 @@
            return "pro.feature.notes.benefit"
        case .persistentAppSorting:
            return "pro.feature.sorting.benefit"
        case .customHotkeys:
            return "pro.feature.customHotkeys.benefit"
        }
    }
}
@@ -95,8 +100,13 @@
struct ProThemePreviewState: Equatable {
    let theme: AppGridTheme
    let themeTuning: Double?
    let startedAt: Date
    let endsAt: Date
    func remainingSeconds(at date: Date = Date()) -> Int {
        max(0, Int(ceil(endsAt.timeIntervalSince(date))))
    }
}
struct ProNoteQuotaStatus: Equatable {
@@ -120,7 +130,7 @@
    static let firstFreeProVersion = "8.1.0"
    static let legacyFallbackOriginalPurchaseDateISO8601: String? = nil
    static let freeThemes: Set<AppGridTheme> = [.defaultLight, .deepBlue, .black]
    static let freeThemes: Set<AppGridTheme> = [.defaultLight, .black]
    static let freeNoteLimit = 5
    static let defaultThemePreviewDuration: TimeInterval = 5 * 60
@@ -159,6 +169,7 @@
private struct ProEntitlementSnapshot {
    var accessState: ProAccessState = .free
    var previewThemeID: String? = nil
    var previewThemeTuning: Double? = nil
}
private enum ProEntitlementSnapshotStore {
@@ -196,7 +207,7 @@
    static func isUnlocked(_ feature: ProFeature) -> Bool {
        switch feature {
        case .premiumThemes, .layoutImport, .layoutExport, .unlimitedNotes, .persistentAppSorting:
        case .premiumThemes, .layoutImport, .layoutExport, .unlimitedNotes, .persistentAppSorting, .customHotkeys:
            return accessState().isUnlocked
        }
    }
@@ -214,6 +225,19 @@
            return .defaultLight
        }
        return storedTheme
    }
    static func effectiveThemeTuning(for storedTheme: AppGridTheme, storedTuning: Double?) -> Double? {
        let snapshot = ProEntitlementSnapshotStore.read()
        if let previewID = snapshot.previewThemeID,
           let previewTheme = AppGridTheme(rawValue: previewID),
           previewTheme.supportsColorTuning {
            return previewTheme.normalizedTuning(snapshot.previewThemeTuning)
        }
        guard canUseTheme(storedTheme), storedTheme.supportsColorTuning else {
            return nil
        }
        return storedTheme.normalizedTuning(storedTuning)
    }
    static func noteQuotaStatus(in store: TagDatabase.Store? = nil) -> ProNoteQuotaStatus {
@@ -397,7 +421,7 @@
        }
    }
    func startThemePreview(for theme: AppGridTheme) {
    func startThemePreview(for theme: AppGridTheme, tuning: Double? = nil) {
        guard !ProEntitlementConfig.freeThemes.contains(theme) else { return }
        guard !accessState.isUnlocked else { return }
@@ -405,6 +429,7 @@
        let preview = ProThemePreviewState(
            theme: theme,
            themeTuning: theme.supportsColorTuning ? theme.normalizedTuning(tuning) : nil,
            startedAt: Date(),
            endsAt: Date().addingTimeInterval(ProEntitlementConfig.themePreviewDuration)
        )
@@ -416,6 +441,21 @@
            guard !Task.isCancelled else { return }
            self?.stopThemePreview()
        }
    }
    func updateThemePreviewTuning(_ tuning: Double, for theme: AppGridTheme) {
        guard let preview = themePreviewState,
              preview.theme == theme,
              theme.supportsColorTuning,
              !accessState.isUnlocked
        else { return }
        let updatedPreview = ProThemePreviewState(
            theme: preview.theme,
            themeTuning: theme.normalizedTuning(tuning),
            startedAt: preview.startedAt,
            endsAt: preview.endsAt
        )
        applyThemePreview(updatedPreview)
    }
    func stopThemePreview() {
@@ -534,9 +574,14 @@
        originalAppVersion: String? = nil,
        originalPurchaseDate: Date? = nil
    ) {
        let previousState = accessState
        accessState = state
        ProEntitlementSnapshotStore.update { snapshot in
            snapshot.accessState = state
        }
        if previousState != state {
            NotificationCenter.default.post(name: .tagLauncherProEntitlementChanged, object: nil)
        }
        if persistCache {
@@ -555,6 +600,7 @@
        themePreviewState = preview
        ProEntitlementSnapshotStore.update { snapshot in
            snapshot.previewThemeID = preview?.theme.rawValue
            snapshot.previewThemeTuning = preview?.themeTuning
        }
    }