93 lines
2.9 KiB
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()
|
|
}
|
|
} |