SelfieCam/SelfieCam/Shared/Premium/PremiumManager.swift

157 lines
4.9 KiB
Swift

import RevenueCat
import SwiftUI
@MainActor
@Observable
final class PremiumManager: PremiumManaging {
var availablePackages: [Package] = []
// MARK: - Configuration
/// RevenueCat entitlement identifier - must match your RevenueCat dashboard
private let entitlementIdentifier = "pro"
/// Reads the RevenueCat API key from Info.plist (injected at build time from Secrets.xcconfig)
private static var apiKey: String {
guard let key = Bundle.main.object(forInfoDictionaryKey: "RevenueCatAPIKey") as? String,
!key.isEmpty,
key != "your_revenuecat_public_api_key_here" else {
#if DEBUG
print("⚠️ [PremiumManager] RevenueCat API key not configured. See Configuration/Secrets.xcconfig.template")
#endif
return ""
}
return key
}
// MARK: - Debug Override
/// Debug premium toggle stored in UserDefaults (only available in DEBUG builds)
@AppStorage("debugPremiumEnabled") @ObservationIgnored private var debugPremiumEnabled = false
/// Check if debug premium is enabled via UserDefaults toggle or environment variable.
/// The toggle in Settings > Debug takes precedence over environment variables.
private var isDebugPremiumEnabled: Bool {
#if DEBUG
return debugPremiumEnabled || ProcessInfo.processInfo.environment["ENABLE_DEBUG_PREMIUM"] == "1"
#else
return false
#endif
}
/// Public getter/setter for debug premium toggle (DEBUG builds only)
var isDebugPremiumToggleEnabled: Bool {
get { debugPremiumEnabled }
set { debugPremiumEnabled = newValue }
}
var isPremium: Bool {
// Debug override takes precedence
if isDebugPremiumEnabled {
return true
}
// If API key isn't configured, return false
guard !Self.apiKey.isEmpty else {
return false
}
return Purchases.shared.cachedCustomerInfo?.entitlements[entitlementIdentifier]?.isActive == true
}
var isPremiumUnlocked: Bool { isPremium }
init() {
#if DEBUG
if isDebugPremiumEnabled {
print("🔓 [PremiumManager] Debug premium enabled via environment variable")
}
#endif
// Only configure RevenueCat if we have a valid API key
guard !Self.apiKey.isEmpty else {
#if DEBUG
print("⚠️ [PremiumManager] Skipping RevenueCat configuration - no API key")
#endif
return
}
#if DEBUG
Purchases.logLevel = .debug
#endif
Purchases.configure(withAPIKey: Self.apiKey)
Task {
try? await loadProducts()
}
}
func loadProducts() async throws {
guard !Self.apiKey.isEmpty else { return }
let offerings = try await Purchases.shared.offerings()
if let current = offerings.current {
availablePackages = current.availablePackages
}
}
func purchase(_ package: Package) async throws -> Bool {
#if DEBUG
if isDebugPremiumEnabled {
// Simulate successful purchase in debug mode
UIAccessibility.post(
notification: .announcement,
argument: String(localized: "Debug mode: Purchase simulated!")
)
return true
}
#endif
let result = try await Purchases.shared.purchase(package: package)
if result.customerInfo.entitlements[entitlementIdentifier]?.isActive == true {
UIAccessibility.post(
notification: .announcement,
argument: String(localized: "Purchase successful! Pro features unlocked.")
)
return true
}
return false
}
func purchase(productId: String) async throws {
#if DEBUG
if isDebugPremiumEnabled {
return // Already "premium" in debug mode
}
#endif
guard let package = availablePackages.first(where: { $0.storeProduct.productIdentifier == productId }) else {
throw NSError(
domain: "PremiumManager",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "Product not found"]
)
}
_ = try await purchase(package)
}
func restorePurchases() async throws {
#if DEBUG
if isDebugPremiumEnabled {
UIAccessibility.post(
notification: .announcement,
argument: String(localized: "Debug mode: Restore simulated!")
)
return
}
#endif
_ = try await Purchases.shared.restorePurchases()
UIAccessibility.post(
notification: .announcement,
argument: String(localized: "Purchases restored")
)
}
}