| | |
| | | case layoutImport |
| | | case layoutExport |
| | | case customTagColors |
| | | case customContainerQuota |
| | | case unlimitedNotes |
| | | case persistentAppSorting |
| | | case customHotkeys |
| | |
| | | return "pro.feature.export" |
| | | case .customTagColors: |
| | | return "pro.feature.customTagColors" |
| | | case .customContainerQuota: |
| | | return "pro.feature.customContainerQuota" |
| | | case .unlimitedNotes: |
| | | return "pro.feature.notes" |
| | | case .persistentAppSorting: |
| | |
| | | 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: |
| | |
| | | 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" |
| | |
| | | |
| | | 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" |
| | |
| | | |
| | | 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 |
| | | } |
| | | } |
| | |
| | | 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 |