krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

v5.0.0: DomainDig/FeatureAccessService.swift · raw

  1import Foundation
  2
  3enum FeatureTier: String, Codable, CaseIterable, Identifiable {
  4    case free
  5    case pro
  6    case proPlus = "dataPlus"
  7
  8    var id: String { rawValue }
  9
 10    var title: String {
 11        switch self {
 12        case .free:
 13            return "Free"
 14        case .pro:
 15            return "Pro"
 16        case .proPlus:
 17            return "Pro+"
 18        }
 19    }
 20}
 21
 22enum FeatureCapability: String, CaseIterable, Identifiable {
 23    case singleLookup
 24    case basicHistory
 25    case limitedTracking
 26    case workflows
 27    case batchOperations
 28    case automatedMonitoring
 29    case localAlerts
 30    case advancedExports
 31    case ownershipHistory
 32    case dnsHistory
 33    case extendedSubdomains
 34    case domainPricing
 35    case reputation
 36
 37    var id: String { rawValue }
 38
 39    var title: String {
 40        switch self {
 41        case .singleLookup:
 42            return "Single lookup"
 43        case .basicHistory:
 44            return "Basic history"
 45        case .limitedTracking:
 46            return "Limited tracking"
 47        case .workflows:
 48            return "Workflows"
 49        case .batchOperations:
 50            return "Batch operations"
 51        case .automatedMonitoring:
 52            return "Background monitoring"
 53        case .localAlerts:
 54            return "Local alerts"
 55        case .advancedExports:
 56            return "Advanced exports"
 57        case .ownershipHistory:
 58            return "Ownership history"
 59        case .dnsHistory:
 60            return "DNS history"
 61        case .extendedSubdomains:
 62            return "Extended subdomains"
 63        case .domainPricing:
 64            return "Domain pricing"
 65        case .reputation:
 66            return "Domain reputation"
 67        }
 68    }
 69}
 70
 71struct FeatureEntitlements: Equatable {
 72    let tier: FeatureTier
 73    let capabilities: Set<FeatureCapability>
 74    let trackedDomainLimit: Int
 75    let workflowLimit: Int?
 76    let batchSizeLimit: Int?
 77}
 78
 79struct UpgradePromptContext: Identifiable, Equatable {
 80    let id = UUID()
 81    let title: String
 82    let message: String
 83    let capability: FeatureCapability
 84}
 85
 86enum FeatureAccessService {
 87    private static let effectivelyUnlimitedTrackedDomains = 5_000
 88
 89    static var currentTier: FeatureTier {
 90        PurchaseService.cachedTier
 91    }
 92
 93    static var entitlements: FeatureEntitlements {
 94        switch currentTier {
 95        case .free:
 96            return FeatureEntitlements(
 97                tier: .free,
 98                capabilities: [.singleLookup, .basicHistory, .limitedTracking, .workflows, .batchOperations],
 99                trackedDomainLimit: 5,
100                workflowLimit: 1,
101                batchSizeLimit: 10
102            )
103        case .pro:
104            return FeatureEntitlements(
105                tier: .pro,
106                capabilities: [
107                    .singleLookup,
108                    .basicHistory,
109                    .limitedTracking,
110                    .workflows,
111                    .batchOperations,
112                    .automatedMonitoring,
113                    .localAlerts,
114                    .advancedExports
115                ],
116                trackedDomainLimit: effectivelyUnlimitedTrackedDomains,
117                workflowLimit: nil,
118                batchSizeLimit: nil
119            )
120        case .proPlus:
121            return FeatureEntitlements(
122                tier: .proPlus,
123                capabilities: Set(FeatureCapability.allCases),
124                trackedDomainLimit: effectivelyUnlimitedTrackedDomains,
125                workflowLimit: nil,
126                batchSizeLimit: nil
127            )
128        }
129    }
130
131    static func hasAccess(to capability: FeatureCapability) -> Bool {
132        entitlements.capabilities.contains(capability)
133    }
134
135    static func canAddTrackedDomain(currentCount: Int) -> Bool {
136        currentCount < entitlements.trackedDomainLimit
137    }
138
139    static func trackedDomainLimitMessage(currentCount: Int) -> String? {
140        guard currentTier == .free else { return nil }
141        if currentCount >= entitlements.trackedDomainLimit {
142            return "Free includes up to \(entitlements.trackedDomainLimit) tracked domains. Available in Pro."
143        }
144        return "Free includes up to \(entitlements.trackedDomainLimit) tracked domains."
145    }
146
147    static func canCreateWorkflow(currentCount: Int) -> Bool {
148        guard hasAccess(to: .workflows) else { return false }
149        guard let limit = entitlements.workflowLimit else { return true }
150        return currentCount < limit
151    }
152
153    static func canRunBatch(domainCount: Int) -> Bool {
154        guard hasAccess(to: .batchOperations) else { return false }
155        guard let limit = entitlements.batchSizeLimit else { return true }
156        return domainCount <= limit
157    }
158
159    static func upgradeMessage(for capability: FeatureCapability) -> String {
160        switch capability {
161        case .workflows, .batchOperations, .automatedMonitoring, .localAlerts, .advancedExports:
162            return "Available in Pro"
163        case .ownershipHistory, .dnsHistory, .extendedSubdomains, .domainPricing, .reputation:
164            return "Available in Pro+"
165        case .limitedTracking:
166            return "Tracking is limited on Free"
167        case .singleLookup, .basicHistory:
168            return "Included in Free"
169        }
170    }
171
172    static func workflowLimitMessage(currentCount: Int) -> String? {
173        guard let limit = entitlements.workflowLimit, currentCount >= limit else { return nil }
174        return "Free includes up to \(limit) workflow."
175    }
176
177    static func batchLimitMessage(domainCount: Int) -> String? {
178        guard let limit = entitlements.batchSizeLimit, domainCount > limit else { return nil }
179        return "Free runs batches up to \(limit) domains."
180    }
181
182    static func workflowAllowanceSummary(currentCount: Int) -> String? {
183        guard currentTier == .free, let limit = entitlements.workflowLimit else { return nil }
184        if currentCount >= limit {
185            return "Free includes up to \(limit) workflow. Available in Pro."
186        }
187        return "Free includes up to \(limit) workflow."
188    }
189
190    static func batchAllowanceSummary() -> String? {
191        guard currentTier == .free, let limit = entitlements.batchSizeLimit else { return nil }
192        return "Free runs batches up to \(limit) domains."
193    }
194
195    static func upgradePromptForTrackedDomains(currentCount: Int) -> UpgradePromptContext? {
196        guard currentCount >= entitlements.trackedDomainLimit else { return nil }
197        return UpgradePromptContext(
198            title: "Available in Pro",
199            message: "Free includes up to \(entitlements.trackedDomainLimit) tracked domains. You can keep your current watchlist, but adding more requires Pro.",
200            capability: .limitedTracking
201        )
202    }
203
204    static func upgradePromptForWorkflows(currentCount: Int) -> UpgradePromptContext? {
205        guard let limit = entitlements.workflowLimit, currentCount >= limit else { return nil }
206        return UpgradePromptContext(
207            title: "Available in Pro",
208            message: "Free includes up to \(limit) workflow. You can keep your current workflows, but creating more requires Pro.",
209            capability: .workflows
210        )
211    }
212
213    static func upgradePromptForBatch(domainCount: Int) -> UpgradePromptContext? {
214        guard let limit = entitlements.batchSizeLimit, domainCount > limit else { return nil }
215        return UpgradePromptContext(
216            title: "Available in Pro",
217            message: "Free runs batches up to \(limit) domains at a time. You can continue with smaller batches or upgrade to Pro.",
218            capability: .batchOperations
219        )
220    }
221
222    static func upgradePrompt(for capability: FeatureCapability) -> UpgradePromptContext {
223        let title: String
224        switch capability {
225        case .ownershipHistory, .dnsHistory, .extendedSubdomains, .domainPricing, .reputation:
226            title = "Available in Pro+"
227        default:
228            title = "Available in Pro"
229        }
230        return UpgradePromptContext(
231            title: title,
232            message: upgradeMessage(for: capability),
233            capability: capability
234        )
235    }
236
237    static func enabledFeatureLabels() -> [String] {
238        FeatureCapability.allCases
239            .filter { hasAccess(to: $0) }
240            .map(\.title)
241    }
242}