Features: - Camera preview with ring light effect - Adjustable ring size with slider - Light color presets (white, warm cream, ice blue, soft pink, warm amber, cool lavender) - Light intensity control (opacity) - Front flash (hides preview during capture) - True mirror mode - Skin smoothing toggle - Grid overlay (rule of thirds) - Self-timer options - Photo and video capture modes - iCloud sync for settings across devices Architecture: - SwiftUI with @Observable view models - Protocol-oriented design (RingLightConfigurable, CaptureControlling) - Bedrock design system integration - CloudSyncManager for iCloud settings sync - RevenueCat for premium features
148 lines
4.5 KiB
Swift
148 lines
4.5 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
|
|
|
|
/// Check if debug premium is enabled via environment variable.
|
|
/// Set "ENABLE_DEBUG_PREMIUM" = "1" in your scheme's environment variables to unlock all premium features during debugging.
|
|
private var isDebugPremiumEnabled: Bool {
|
|
#if DEBUG
|
|
return ProcessInfo.processInfo.environment["ENABLE_DEBUG_PREMIUM"] == "1"
|
|
#else
|
|
return false
|
|
#endif
|
|
}
|
|
|
|
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")
|
|
)
|
|
}
|
|
}
|