Ariver
2026-07-01 0ae3a9359e4cdf6ae5b669c4affd91662463f370
src/Apptag/ProEntitlement.swift
@@ -7,6 +7,7 @@
    case layoutImport
    case layoutExport
    case customTagColors
    case customContainerQuota
    case unlimitedNotes
    case persistentAppSorting
    case customHotkeys
@@ -21,6 +22,8 @@
            return "pro.feature.export"
        case .customTagColors:
            return "pro.feature.customTagColors"
        case .customContainerQuota:
            return "pro.feature.customContainerQuota"
        case .unlimitedNotes:
            return "pro.feature.notes"
        case .persistentAppSorting:
@@ -40,6 +43,8 @@
            return "pro.feature.export.benefit"
        case .customTagColors:
            return "pro.feature.customTagColors.benefit"
        case .customContainerQuota:
            return "pro.feature.customContainerQuota.benefit"
        case .unlimitedNotes:
            return "pro.feature.notes.benefit"
        case .persistentAppSorting:
@@ -139,6 +144,21 @@
    case blocked(ProNoteQuotaStatus)
}
struct ProCustomContainerQuotaStatus: Equatable {
    let isUnlimited: Bool
    let used: Int
    let limit: Int
    var remaining: Int {
        max(0, limit - used)
    }
}
enum ProCustomContainerMaintenanceDecision: Equatable {
    case allow(ProCustomContainerQuotaStatus)
    case blocked(ProCustomContainerQuotaStatus)
}
enum ProEntitlementConfig {
    // 发布前只改这里,不允许把商品信息和边界版本散落到业务模块。
    static let lifetimeProductID = "com.taglauncher.pro.lifetime"
@@ -147,6 +167,7 @@
    static let freeThemes: Set<AppGridTheme> = [.defaultLight, .black]
    static let freeNoteLimit = 3
    static let freeCustomContainerLimit = 6
    static let defaultThemePreviewDuration: TimeInterval = 5 * 60
    static let qaStateEnvKey = "TAGLAUNCHER_QA_PRO_STATE"
@@ -225,7 +246,7 @@
    static func isUnlocked(_ feature: ProFeature) -> Bool {
        switch feature {
        case .premiumThemes, .layoutImport, .layoutExport, .customTagColors, .unlimitedNotes, .persistentAppSorting, .customHotkeys:
        case .premiumThemes, .layoutImport, .layoutExport, .customTagColors, .customContainerQuota, .unlimitedNotes, .persistentAppSorting, .customHotkeys:
            return accessState().isUnlocked
        }
    }
@@ -344,6 +365,37 @@
            throw ProEntitlementError.noteQuotaExceeded(status)
        }
    }
    static func customContainerQuotaStatus(in store: TagDatabase.Store? = nil) -> ProCustomContainerQuotaStatus {
        guard !isUnlocked(.customContainerQuota) else {
            return ProCustomContainerQuotaStatus(isUnlimited: true, used: 0, limit: Int.max)
        }
        let used = TagDatabase.customMaintainedContainerCount(in: store)
        return ProCustomContainerQuotaStatus(
            isUnlimited: false,
            used: used,
            limit: ProEntitlementConfig.freeCustomContainerLimit
        )
    }
    static func customContainerMaintenanceDecision(
        addingCustomContainerCount addedCount: Int,
        in store: TagDatabase.Store? = nil
    ) -> ProCustomContainerMaintenanceDecision {
        let status = customContainerQuotaStatus(in: store)
        guard !status.isUnlimited else { return .allow(status) }
        guard addedCount > 0 else { return .allow(status) }
        guard status.used + addedCount <= status.limit else {
            return .blocked(status)
        }
        return .allow(
            ProCustomContainerQuotaStatus(
                isUnlimited: false,
                used: status.used + addedCount,
                limit: status.limit
            )
        )
    }
}
@MainActor