| | |
| | | case layoutExport |
| | | case unlimitedNotes |
| | | case persistentAppSorting |
| | | case customHotkeys |
| | | |
| | | var titleKey: String { |
| | | switch self { |
| | |
| | | return "pro.feature.notes" |
| | | case .persistentAppSorting: |
| | | return "pro.feature.sorting" |
| | | case .customHotkeys: |
| | | return "pro.feature.customHotkeys" |
| | | } |
| | | } |
| | | |
| | |
| | | return "pro.feature.notes.benefit" |
| | | case .persistentAppSorting: |
| | | return "pro.feature.sorting.benefit" |
| | | case .customHotkeys: |
| | | return "pro.feature.customHotkeys.benefit" |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | struct ProThemePreviewState: Equatable { |
| | | let theme: AppGridTheme |
| | | let themeTuning: Double? |
| | | let startedAt: Date |
| | | let endsAt: Date |
| | | |
| | | func remainingSeconds(at date: Date = Date()) -> Int { |
| | | max(0, Int(ceil(endsAt.timeIntervalSince(date)))) |
| | | } |
| | | } |
| | | |
| | | struct ProNoteQuotaStatus: Equatable { |
| | |
| | | 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 |
| | | |
| | |
| | | private struct ProEntitlementSnapshot { |
| | | var accessState: ProAccessState = .free |
| | | var previewThemeID: String? = nil |
| | | var previewThemeTuning: Double? = nil |
| | | } |
| | | |
| | | private enum ProEntitlementSnapshotStore { |
| | |
| | | |
| | | static func isUnlocked(_ feature: ProFeature) -> Bool { |
| | | switch feature { |
| | | case .premiumThemes, .layoutImport, .layoutExport, .unlimitedNotes, .persistentAppSorting: |
| | | case .premiumThemes, .layoutImport, .layoutExport, .unlimitedNotes, .persistentAppSorting, .customHotkeys: |
| | | return accessState().isUnlocked |
| | | } |
| | | } |
| | |
| | | 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 { |
| | |
| | | } |
| | | } |
| | | |
| | | 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 } |
| | | |
| | |
| | | |
| | | let preview = ProThemePreviewState( |
| | | theme: theme, |
| | | themeTuning: theme.supportsColorTuning ? theme.normalizedTuning(tuning) : nil, |
| | | startedAt: Date(), |
| | | endsAt: Date().addingTimeInterval(ProEntitlementConfig.themePreviewDuration) |
| | | ) |
| | |
| | | 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() { |
| | |
| | | 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 { |
| | |
| | | themePreviewState = preview |
| | | ProEntitlementSnapshotStore.update { snapshot in |
| | | snapshot.previewThemeID = preview?.theme.rawValue |
| | | snapshot.previewThemeTuning = preview?.themeTuning |
| | | } |
| | | } |
| | | |