From b2a06ec5ebc86c0241e635782ae5a032ab5aad0a Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 02 Jul 2026 02:43:51 +0800
Subject: [PATCH] Prepare TagLauncher 8.3.2 App Store candidate
---
src/Apptag/ProEntitlement.swift | 163 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 159 insertions(+), 4 deletions(-)
diff --git a/src/Apptag/ProEntitlement.swift b/src/Apptag/ProEntitlement.swift
index 7aa5e4a..4d5fc2b 100644
--- a/src/Apptag/ProEntitlement.swift
+++ b/src/Apptag/ProEntitlement.swift
@@ -6,8 +6,11 @@
case premiumThemes
case layoutImport
case layoutExport
+ case customTagColors
+ case customContainerQuota
case unlimitedNotes
case persistentAppSorting
+ case customHotkeys
var titleKey: String {
switch self {
@@ -17,10 +20,16 @@
return "pro.feature.import"
case .layoutExport:
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.sorting"
+ case .customHotkeys:
+ return "pro.feature.customHotkeys"
}
}
@@ -32,10 +41,16 @@
return "pro.feature.import.benefit"
case .layoutExport:
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:
return "pro.feature.sorting.benefit"
+ case .customHotkeys:
+ return "pro.feature.customHotkeys.benefit"
}
}
}
@@ -104,6 +119,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
@@ -119,14 +144,30 @@
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 firstFreeProVersion = "8.1.0"
+ static let firstFreeProVersion = "8.3.2"
static let legacyFallbackOriginalPurchaseDateISO8601: String? = nil
static let freeThemes: Set<AppGridTheme> = [.defaultLight, .black]
- static let freeNoteLimit = 5
+ static let freeNoteLimit = 3
+ static let freeCustomContainerLimit = 6
static let defaultThemePreviewDuration: TimeInterval = 5 * 60
static let qaStateEnvKey = "TAGLAUNCHER_QA_PRO_STATE"
@@ -165,6 +206,7 @@
var accessState: ProAccessState = .free
var previewThemeID: String? = nil
var previewThemeTuning: Double? = nil
+ var previewDisplayMode: String? = nil
}
private enum ProEntitlementSnapshotStore {
@@ -186,6 +228,8 @@
}
enum ProEntitlementPolicy {
+ static let premiumDisplayModes: Set<String> = ["coloredContainer", "coloredGridContainer"]
+
static func accessState() -> ProAccessState {
ProEntitlementSnapshotStore.read().accessState
}
@@ -202,13 +246,44 @@
static func isUnlocked(_ feature: ProFeature) -> Bool {
switch feature {
- case .premiumThemes, .layoutImport, .layoutExport, .unlimitedNotes, .persistentAppSorting:
+ case .premiumThemes, .layoutImport, .layoutExport, .customTagColors, .customContainerQuota, .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 {
@@ -290,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
@@ -300,17 +406,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 {
@@ -321,8 +430,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 {
@@ -420,6 +533,7 @@
guard !ProEntitlementConfig.freeThemes.contains(theme) else { return }
guard !accessState.isUnlocked else { return }
+ stopDisplayModePreview()
themePreviewTask?.cancel()
let preview = ProThemePreviewState(
@@ -457,6 +571,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() {
@@ -569,9 +712,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 {
@@ -594,6 +742,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")
--
Gitblit v1.9.3