SelfieRingLight/SelfieRingLight/Shared/Premium/PremiumManager.swift
Matt Bruce 74e65829de Initial commit: SelfieRingLight app
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
2026-01-02 13:01:24 -06:00

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")
)
}
}