80 lines
3.2 KiB
Swift
80 lines
3.2 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
import RevenueCat
|
|
import TermindApplication
|
|
import TermindDomain
|
|
|
|
@MainActor
|
|
final class RevenueCatProPurchaseService: ProPurchaseService {
|
|
private let purchases: Purchases
|
|
private let configuration: RevenueCatConfiguration
|
|
private let defaults: UserDefaults
|
|
private let now: () -> Date
|
|
private var updateTask: Task<Void, Never>?
|
|
private var attemptedMigration = false
|
|
private let continuation: AsyncStream<EntitlementStatus>.Continuation
|
|
let updates: AsyncStream<EntitlementStatus>
|
|
|
|
init(
|
|
configuration: RevenueCatConfiguration, defaults: UserDefaults = .standard, now: @escaping () -> Date = { .now }
|
|
) {
|
|
self.configuration = configuration
|
|
self.defaults = defaults
|
|
self.now = now
|
|
// Both platforms use anonymous IDs. Restoring a universal Apple purchase aliases these IDs.
|
|
// Never use the device ID or iCloud account as a custom RevenueCat user ID.
|
|
if !Purchases.isConfigured {
|
|
Purchases.configure(
|
|
with: Configuration.builder(withAPIKey: configuration.apiKey)
|
|
.with(storeKitVersion: .storeKit2)
|
|
.with(entitlementVerificationMode: .informational)
|
|
.with(automaticDeviceIdentifierCollectionEnabled: false)
|
|
.with(preferredUILocaleOverride: AppLanguage.stored().locale.identifier)
|
|
)
|
|
}
|
|
purchases = Purchases.shared
|
|
(updates, continuation) = AsyncStream.makeStream(bufferingPolicy: .bufferingNewest(1))
|
|
let stream = purchases.customerInfoStream
|
|
updateTask = Task { [weak self] in
|
|
for await info in stream {
|
|
guard !Task.isCancelled, let self else { return }
|
|
self.continuation.yield(self.resolve(info))
|
|
}
|
|
}
|
|
}
|
|
|
|
isolated deinit {
|
|
updateTask?.cancel()
|
|
continuation.finish()
|
|
}
|
|
|
|
func currentStatus() async throws -> EntitlementStatus {
|
|
do {
|
|
// Revalidate deadlines against RevenueCat instead of accepting a still-fresh SDK cache.
|
|
let info = try await purchases.customerInfo(fetchPolicy: .fetchCurrent)
|
|
let status = resolve(info)
|
|
let migrationKey = "commerce.revenuecat.migrated.\(purchases.appUserID)"
|
|
if !configuration.isTestStore, !status.hasProAccess,
|
|
status != .verificationFailed, !attemptedMigration, !defaults.bool(forKey: migrationKey)
|
|
{
|
|
attemptedMigration = true
|
|
// RevenueCat imports pre-integration Apple purchases without a second transaction observer.
|
|
let migrated = resolve(try await purchases.syncPurchases())
|
|
if migrated != .verificationFailed { defaults.set(true, forKey: migrationKey) }
|
|
return migrated
|
|
}
|
|
return status
|
|
} catch {
|
|
if let cached = purchases.cachedCustomerInfo { return resolve(cached) }
|
|
throw error
|
|
}
|
|
}
|
|
|
|
private func resolve(_ info: CustomerInfo) -> EntitlementStatus {
|
|
RevenueCatEntitlementResolver.resolve(info, entitlementID: configuration.entitlementID, now: now())
|
|
}
|
|
|
|
}
|