Ariver
2026-06-30 bcceca83ce966598e00e0319d85ec11d6df9d272
src/Apptag/ProEntitlement.swift
@@ -6,6 +6,7 @@
    case premiumThemes
    case layoutImport
    case layoutExport
    case customTagColors
    case unlimitedNotes
    case persistentAppSorting
    case customHotkeys
@@ -18,6 +19,8 @@
            return "pro.feature.import"
        case .layoutExport:
            return "pro.feature.export"
        case .customTagColors:
            return "pro.feature.customTagColors"
        case .unlimitedNotes:
            return "pro.feature.notes"
        case .persistentAppSorting:
@@ -35,6 +38,8 @@
            return "pro.feature.import.benefit"
        case .layoutExport:
            return "pro.feature.export.benefit"
        case .customTagColors:
            return "pro.feature.customTagColors.benefit"
        case .unlimitedNotes:
            return "pro.feature.notes.benefit"
        case .persistentAppSorting:
@@ -109,6 +114,16 @@
    }
}
struct ProDisplayModePreviewState: Equatable {
    let displayMode: String
    let startedAt: Date
    let endsAt: Date
    func remainingSeconds(at date: Date = Date()) -> Int {
        max(0, Int(ceil(endsAt.timeIntervalSince(date))))
    }
}
struct ProNoteQuotaStatus: Equatable {
    let isUnlimited: Bool
    let used: Int
@@ -131,7 +146,7 @@
    static let legacyFallbackOriginalPurchaseDateISO8601: String? = nil
    static let freeThemes: Set<AppGridTheme> = [.defaultLight, .black]
    static let freeNoteLimit = 5
    static let freeNoteLimit = 3
    static let defaultThemePreviewDuration: TimeInterval = 5 * 60
    static let qaStateEnvKey = "TAGLAUNCHER_QA_PRO_STATE"
@@ -170,6 +185,7 @@
    var accessState: ProAccessState = .free
    var previewThemeID: String? = nil
    var previewThemeTuning: Double? = nil
    var previewDisplayMode: String? = nil
}
private enum ProEntitlementSnapshotStore {
@@ -191,6 +207,8 @@
}
enum ProEntitlementPolicy {
    static let premiumDisplayModes: Set<String> = ["coloredContainer", "coloredGridContainer"]
    static func accessState() -> ProAccessState {
        ProEntitlementSnapshotStore.read().accessState
    }
@@ -207,13 +225,44 @@
    static func isUnlocked(_ feature: ProFeature) -> Bool {
        switch feature {
        case .premiumThemes, .layoutImport, .layoutExport, .unlimitedNotes, .persistentAppSorting, .customHotkeys:
        case .premiumThemes, .layoutImport, .layoutExport, .customTagColors, .unlimitedNotes, .persistentAppSorting, .customHotkeys:
            return accessState().isUnlocked
        }
    }
    static func canUseTheme(_ theme: AppGridTheme) -> Bool {
        ProEntitlementConfig.freeThemes.contains(theme) || isUnlocked(.premiumThemes)
    }
    static func isPremiumDisplayMode(_ displayMode: String) -> Bool {
        premiumDisplayModes.contains(displayMode)
    }
    static func canUseDisplayMode(_ displayMode: String) -> Bool {
        !isPremiumDisplayMode(displayMode) || isUnlocked(.premiumThemes)
    }
    static func fallbackDisplayMode(for displayMode: String) -> String {
        switch displayMode {
        case "coloredContainer":
            return "container"
        case "coloredGridContainer":
            return "gridContainer"
        default:
            return displayMode
        }
    }
    static func effectiveDisplayMode(for storedDisplayMode: String) -> String {
        let snapshot = ProEntitlementSnapshotStore.read()
        if let previewDisplayMode = snapshot.previewDisplayMode,
           isPremiumDisplayMode(previewDisplayMode) {
            return previewDisplayMode
        }
        guard canUseDisplayMode(storedDisplayMode) else {
            return fallbackDisplayMode(for: storedDisplayMode)
        }
        return storedDisplayMode
    }
    static func effectiveTheme(for storedTheme: AppGridTheme) -> AppGridTheme {
@@ -305,17 +354,20 @@
    @Published private(set) var operationState: ProOperationState = .idle
    @Published private(set) var priceDisplayText: String? = nil
    @Published private(set) var themePreviewState: ProThemePreviewState? = nil
    @Published private(set) var displayModePreviewState: ProDisplayModePreviewState? = nil
    private var hasStarted = false
    private var cachedProduct: Product?
    private var updateTask: Task<Void, Never>?
    private var themePreviewTask: Task<Void, Never>?
    private var displayModePreviewTask: Task<Void, Never>?
    private init() {}
    deinit {
        updateTask?.cancel()
        themePreviewTask?.cancel()
        displayModePreviewTask?.cancel()
    }
    var isUnlocked: Bool {
@@ -326,8 +378,12 @@
        themePreviewState != nil
    }
    var isPreviewingProAppearance: Bool {
        themePreviewState != nil || displayModePreviewState != nil
    }
    var compactStatusTextKey: String {
        if themePreviewState != nil {
        if isPreviewingProAppearance {
            return "pro.theme.preview.status"
        }
        switch accessState {
@@ -425,6 +481,7 @@
        guard !ProEntitlementConfig.freeThemes.contains(theme) else { return }
        guard !accessState.isUnlocked else { return }
        stopDisplayModePreview()
        themePreviewTask?.cancel()
        let preview = ProThemePreviewState(
@@ -462,6 +519,35 @@
        themePreviewTask?.cancel()
        themePreviewTask = nil
        applyThemePreview(nil)
    }
    func startDisplayModePreview(for displayMode: String) {
        guard ProEntitlementPolicy.isPremiumDisplayMode(displayMode) else { return }
        guard !accessState.isUnlocked else { return }
        stopThemePreview()
        displayModePreviewTask?.cancel()
        let now = Date()
        let preview = ProDisplayModePreviewState(
            displayMode: displayMode,
            startedAt: now,
            endsAt: now.addingTimeInterval(ProEntitlementConfig.themePreviewDuration)
        )
        applyDisplayModePreview(preview)
        displayModePreviewTask = Task { [weak self] in
            let duration = max(1, ProEntitlementConfig.themePreviewDuration)
            try? await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000))
            guard !Task.isCancelled else { return }
            self?.stopDisplayModePreview()
        }
    }
    func stopDisplayModePreview() {
        displayModePreviewTask?.cancel()
        displayModePreviewTask = nil
        applyDisplayModePreview(nil)
    }
    private func startTransactionUpdates() {
@@ -604,6 +690,13 @@
        }
    }
    private func applyDisplayModePreview(_ preview: ProDisplayModePreviewState?) {
        displayModePreviewState = preview
        ProEntitlementSnapshotStore.update { snapshot in
            snapshot.previewDisplayMode = preview?.displayMode
        }
    }
    private var cacheURL: URL {
        AppIdentity.applicationSupportDirectory
            .appendingPathComponent("pro-entitlement-cache.json")