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,6 +100,7 @@
struct ProThemePreviewState: Equatable {
    let theme: AppGridTheme
    let themeTuning: Double?
    let startedAt: Date
    let endsAt: Date
@@ -124,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
@@ -163,6 +169,7 @@
private struct ProEntitlementSnapshot {
    var accessState: ProAccessState = .free
    var previewThemeID: String? = nil
    var previewThemeTuning: Double? = nil
}
private enum ProEntitlementSnapshotStore {
@@ -200,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
        }
    }
@@ -218,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 {
@@ -401,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 }
@@ -409,6 +429,7 @@
        let preview = ProThemePreviewState(
            theme: theme,
            themeTuning: theme.supportsColorTuning ? theme.normalizedTuning(tuning) : nil,
            startedAt: Date(),
            endsAt: Date().addingTimeInterval(ProEntitlementConfig.themePreviewDuration)
        )
@@ -420,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() {
@@ -538,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 {
@@ -559,6 +600,7 @@
        themePreviewState = preview
        ProEntitlementSnapshotStore.update { snapshot in
            snapshot.previewThemeID = preview?.theme.rawValue
            snapshot.previewThemeTuning = preview?.themeTuning
        }
    }