Closer/iphone/Closer/Core/Billing/BillingService.swift

93 lines
2.9 KiB
Swift

import Foundation
import RevenueCat
// MARK: - Billing Service
final class BillingService: @unchecked Sendable {
static let shared = BillingService()
private var isConfigured = false
private var customerInfoListener: Task<Void, Never>?
private init() {}
func configure(with apiKey: String) {
guard !isConfigured else { return }
Purchases.logLevel = .debug
Purchases.configure(withAPIKey: apiKey)
isConfigured = true
}
/// Fetch available offerings for the paywall
func getOfferings() async throws -> Offerings {
try await Purchases.shared.offerings()
}
/// Purchase a package
func purchase(_ package: Package) async throws {
let result = try await Purchases.shared.purchase(package: package)
// After successful purchase, sync entitlement with server
try await FirestoreService.shared.syncEntitlementCallable()
}
/// Restore previous purchases
func restorePurchases() async throws -> CustomerInfo {
try await Purchases.shared.restorePurchases()
}
/// Reactive stream of customer info
var customerInfoStream: AsyncStream<CustomerInfo> {
AsyncStream { continuation in
let task = Task {
for await info in Purchases.shared.customerInfoStream {
continuation.yield(info)
}
}
continuation.onTermination = { _ in task.cancel() }
}
}
/// Check if current user has premium entitlement
func checkPremiumStatus() async -> Bool {
do {
let customerInfo = try await Purchases.shared.customerInfo()
return customerInfo.entitlements["closer_premium"]?.isActive == true
} catch {
return false
}
}
}
// MARK: - Entitlement Checker
protocol EntitlementChecker: Actor {
var isPremium: AsyncStream<Bool> { get }
func hasPremium() async -> Bool
}
final actor DefaultEntitlementChecker: EntitlementChecker {
nonisolated let isPremium: AsyncStream<Bool>
private let billing: BillingService
private let firestore: FirestoreService
init(billing: BillingService = .shared, firestore: FirestoreService = .shared) {
self.billing = billing
self.firestore = firestore
// Create async stream combining RevenueCat + Firestore
self.isPremium = AsyncStream { continuation in
let task = Task {
// Listen to RevenueCat changes
for await _ in billing.customerInfoStream {
let premium = await billing.checkPremiumStatus()
continuation.yield(premium)
}
}
continuation.onTermination = { _ in task.cancel() }
}
}
func hasPremium() async -> Bool {
await billing.checkPremiumStatus()
}
}