Read receipts in a serial fashion (#824)

Deal with reentrancy issues by ensuring serial execution.
This commit is contained in:
Davide 2024-11-07 11:25:40 +01:00 committed by GitHub
parent 63a0a661c9
commit 5949ff1508
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 124 additions and 79 deletions

View File

@ -33,6 +33,8 @@ public actor FallbackReceiptReader: AppReceiptReader {
private let localReader: (URL) -> InAppReceiptReader? private let localReader: (URL) -> InAppReceiptReader?
private var pendingTask: Task<InAppReceipt?, Never>?
public init( public init(
reader: (InAppReceiptReader & Sendable)?, reader: (InAppReceiptReader & Sendable)?,
localReader: @escaping @Sendable (URL) -> InAppReceiptReader & Sendable localReader: @escaping @Sendable (URL) -> InAppReceiptReader & Sendable
@ -42,16 +44,30 @@ public actor FallbackReceiptReader: AppReceiptReader {
} }
public func receipt(at userLevel: AppUserLevel) async -> InAppReceipt? { public func receipt(at userLevel: AppUserLevel) async -> InAppReceipt? {
if let pendingTask {
_ = await pendingTask.value
}
pendingTask = Task {
await asyncReceipt(at: userLevel)
}
let receipt = await pendingTask?.value
pendingTask = nil
return receipt
}
}
private extension FallbackReceiptReader {
func asyncReceipt(at userLevel: AppUserLevel) async -> InAppReceipt? {
let localURL = Bundle.main.appStoreReceiptURL let localURL = Bundle.main.appStoreReceiptURL
pp_log(.app, .debug, "Parse receipt for user level \(userLevel)") pp_log(.iap, .debug, "\tParse receipt for user level \(userLevel)")
// 1. TestFlight, look for release receipt // 1. TestFlight, look for release receipt
let releaseReceipt: InAppReceipt? = await { let releaseReceipt: InAppReceipt? = await {
guard userLevel == .beta, let localURL else { guard userLevel == .beta, let localURL else {
return nil return nil
} }
pp_log(.app, .debug, "\tTestFlight, look for release receipt") pp_log(.iap, .debug, "\tTestFlight, look for release receipt")
let releaseURL = localURL let releaseURL = localURL
.deletingLastPathComponent() .deletingLastPathComponent()
.appendingPathComponent("receipt") .appendingPathComponent("receipt")
@ -67,7 +83,7 @@ public actor FallbackReceiptReader: AppReceiptReader {
}() }()
if let releaseReceipt { if let releaseReceipt {
pp_log(.app, .debug, "\tTestFlight, return release receipt") pp_log(.iap, .debug, "\tTestFlight, return release receipt")
return releaseReceipt return releaseReceipt
} }
@ -79,18 +95,18 @@ public actor FallbackReceiptReader: AppReceiptReader {
} }
// 2. primary receipt + build from local receipt // 2. primary receipt + build from local receipt
pp_log(.app, .debug, "\tNo release receipt, read primary receipt") pp_log(.iap, .debug, "\tNo release receipt, read primary receipt")
if let receipt = await reader?.receipt() { if let receipt = await reader?.receipt() {
if let build = await localReceiptBlock()?.originalBuildNumber { if let build = await localReceiptBlock()?.originalBuildNumber {
pp_log(.app, .debug, "\tReturn primary receipt with local build: \(build)") pp_log(.iap, .debug, "\tReturn primary receipt with local build: \(build)")
return receipt.withBuildNumber(build) return receipt.withBuildNumber(build)
} }
pp_log(.app, .debug, "\tReturn primary receipt without local build") pp_log(.iap, .debug, "\tReturn primary receipt without local build")
return receipt return receipt
} }
// 3. fall back to local receipt // 3. fall back to local receipt
pp_log(.app, .debug, "\tReturn local receipt") pp_log(.iap, .debug, "\tReturn local receipt")
return await localReceiptBlock() return await localReceiptBlock()
} }
} }

View File

@ -48,6 +48,8 @@ public final class IAPManager: ObservableObject {
private var eligibleFeatures: Set<AppFeature> private var eligibleFeatures: Set<AppFeature>
private var pendingReceiptTask: Task<Void, Never>?
private var subscriptions: Set<AnyCancellable> private var subscriptions: Set<AnyCancellable>
public init( public init(
@ -81,7 +83,7 @@ extension IAPManager {
inAppProducts[$0] inAppProducts[$0]
} }
} catch { } catch {
pp_log(.app, .error, "Unable to fetch in-app products: \(error)") pp_log(.iap, .error, "Unable to fetch in-app products: \(error)")
return [] return []
} }
} }
@ -100,75 +102,14 @@ extension IAPManager {
} }
public func reloadReceipt() async { public func reloadReceipt() async {
purchasedAppBuild = nil if let pendingReceiptTask {
purchasedProducts.removeAll() await pendingReceiptTask.value
eligibleFeatures.removeAll()
pp_log(.app, .notice, "Reload IAP receipt...")
if let receipt = await receiptReader.receipt(at: userLevel) {
if let originalBuildNumber = receipt.originalBuildNumber {
purchasedAppBuild = originalBuildNumber
} }
pendingReceiptTask = Task {
if let purchasedAppBuild { await asyncReloadReceipt()
pp_log(.app, .info, "Original purchased build: \(purchasedAppBuild)")
// assume some purchases by build number
let entitled = productsAtBuild?(purchasedAppBuild) ?? []
pp_log(.app, .notice, "Entitled features: \(entitled.map(\.rawValue))")
entitled.forEach {
purchasedProducts.insert($0)
} }
} await pendingReceiptTask?.value
if let iapReceipts = receipt.purchaseReceipts { pendingReceiptTask = nil
pp_log(.app, .info, "In-app receipts:")
iapReceipts.forEach {
guard let pid = $0.productIdentifier, let product = AppProduct(rawValue: pid) else {
return
}
if let expirationDate = $0.expirationDate {
let now = Date()
pp_log(.app, .debug, "\t\(pid) [expiration date: \(expirationDate), now: \(now)]")
if now >= expirationDate {
pp_log(.app, .info, "\t\(pid) [expired on: \(expirationDate)]")
return
}
}
if let cancellationDate = $0.cancellationDate {
pp_log(.app, .info, "\t\(pid) [cancelled on: \(cancellationDate)]")
return
}
if let purchaseDate = $0.originalPurchaseDate {
pp_log(.app, .info, "\t\(pid) [purchased on: \(purchaseDate)]")
}
purchasedProducts.insert(product)
}
}
eligibleFeatures = purchasedProducts.reduce(into: []) { eligible, product in
product.features.forEach {
eligible.insert($0)
}
}
} else {
pp_log(.app, .error, "Could not parse App Store receipt!")
}
userLevel.features.forEach {
eligibleFeatures.insert($0)
}
unrestrictedFeatures.forEach {
eligibleFeatures.insert($0)
}
pp_log(.app, .notice, "Reloaded IAP receipt:")
pp_log(.app, .notice, "\tPurchased build number: \(purchasedAppBuild?.description ?? "unknown")")
pp_log(.app, .notice, "\tPurchased products: \(purchasedProducts.map(\.rawValue))")
pp_log(.app, .notice, "\tEligible features: \(eligibleFeatures)")
objectWillChange.send()
} }
} }
@ -214,15 +155,101 @@ extension IAPManager {
} }
} }
// MARK: - Receipt
private extension IAPManager {
func asyncReloadReceipt() async {
pp_log(.iap, .notice, "Start reloading in-app receipt...")
purchasedAppBuild = nil
purchasedProducts.removeAll()
eligibleFeatures.removeAll()
if let receipt = await receiptReader.receipt(at: userLevel) {
if let originalBuildNumber = receipt.originalBuildNumber {
purchasedAppBuild = originalBuildNumber
}
if let purchasedAppBuild {
pp_log(.iap, .info, "Original purchased build: \(purchasedAppBuild)")
// assume some purchases by build number
let entitled = productsAtBuild?(purchasedAppBuild) ?? []
pp_log(.iap, .notice, "Entitled features: \(entitled.map(\.rawValue))")
entitled.forEach {
purchasedProducts.insert($0)
}
}
if let iapReceipts = receipt.purchaseReceipts {
pp_log(.iap, .info, "Process in-app purchase receipts...")
let products: [AppProduct] = iapReceipts.compactMap {
guard let pid = $0.productIdentifier else {
return nil
}
guard let product = AppProduct(rawValue: pid) else {
pp_log(.iap, .debug, "\tDiscard unknown product identifier: \(pid)")
return nil
}
if let expirationDate = $0.expirationDate {
let now = Date()
pp_log(.iap, .debug, "\t\(pid) [expiration date: \(expirationDate), now: \(now)]")
if now >= expirationDate {
pp_log(.iap, .info, "\t\(pid) [expired on: \(expirationDate)]")
return nil
}
}
if let cancellationDate = $0.cancellationDate {
pp_log(.iap, .info, "\t\(pid) [cancelled on: \(cancellationDate)]")
return nil
}
if let purchaseDate = $0.originalPurchaseDate {
pp_log(.iap, .info, "\t\(pid) [purchased on: \(purchaseDate)]")
}
return product
}
products.forEach {
purchasedProducts.insert($0)
}
}
eligibleFeatures = purchasedProducts.reduce(into: []) { eligible, product in
product.features.forEach {
eligible.insert($0)
}
}
} else {
pp_log(.iap, .error, "Could not parse App Store receipt!")
}
userLevel.features.forEach {
eligibleFeatures.insert($0)
}
unrestrictedFeatures.forEach {
eligibleFeatures.insert($0)
}
pp_log(.iap, .notice, "Finished reloading in-app receipt for user level \(userLevel)")
pp_log(.iap, .notice, "\tPurchased build number: \(purchasedAppBuild?.description ?? "unknown")")
pp_log(.iap, .notice, "\tPurchased products: \(purchasedProducts.map(\.rawValue))")
pp_log(.iap, .notice, "\tEligible features: \(eligibleFeatures)")
objectWillChange.send()
}
}
// MARK: - Observation // MARK: - Observation
private extension IAPManager { private extension IAPManager {
func observeObjects() { func observeObjects() {
Task { Task {
await fetchLevelIfNeeded() await fetchLevelIfNeeded()
await reloadReceipt()
do { do {
let products = try await inAppHelper.fetchProducts() let products = try await inAppHelper.fetchProducts()
pp_log(.app, .info, "Available in-app products: \(products.map(\.key))") pp_log(.iap, .info, "Available in-app products: \(products.map(\.key))")
inAppHelper inAppHelper
.didUpdate .didUpdate
@ -235,7 +262,7 @@ private extension IAPManager {
.store(in: &subscriptions) .store(in: &subscriptions)
} catch { } catch {
pp_log(.app, .error, "Unable to fetch in-app products: \(error)") pp_log(.iap, .error, "Unable to fetch in-app products: \(error)")
} }
} }
} }
@ -246,11 +273,11 @@ private extension IAPManager {
} }
if let customUserLevel { if let customUserLevel {
userLevel = customUserLevel userLevel = customUserLevel
pp_log(.app, .info, "App level (custom): \(userLevel)") pp_log(.iap, .info, "App level (custom): \(userLevel)")
} else { } else {
let isBeta = await SandboxChecker().isBeta let isBeta = await SandboxChecker().isBeta
userLevel = isBeta ? .beta : .freemium userLevel = isBeta ? .beta : .freemium
pp_log(.app, .info, "App level: \(userLevel)") pp_log(.iap, .info, "App level: \(userLevel)")
} }
} }
} }

View File

@ -29,6 +29,8 @@ import PassepartoutWireGuardGo
extension LoggerDestination { extension LoggerDestination {
public static let app = Self(category: "app") public static let app = Self(category: "app")
public static let iap = Self(category: "iap")
} }
extension WireGuard.Configuration.Builder { extension WireGuard.Configuration.Builder {