Tip jar: finish transactions + Transaction.updates listener

The £4.99 consumable was never finished — StoreKit kept re-delivering
it through Transaction.updates on every launch. Verified purchases now
call transaction.finish() (direct path), and a Transaction.updates
listener finishes + acknowledges purchases that complete outside the
direct call (Ask to Buy approvals, other-device purchases on the same
Apple ID). Untrusted (unverified) transactions are never finished or
delivered; only our own product ID is handled.
This commit is contained in:
FuelBoard Contributor
2026-08-16 08:23:18 +01:00
parent a490bcf7b5
commit 2ffa90d879
+31 -1
View File
@@ -564,6 +564,33 @@ final class TipStore: ObservableObject {
@Published private(set) var purchaseInProgress = false
@Published private(set) var message: String?
/// Listens for transactions that complete OUTSIDE the direct purchase()
/// call Ask to Buy approvals, payments finished on another device
/// signed into the same Apple ID. Without this, those purchases land in
/// the queue but are never finished or acknowledged.
private var updatesTask: Task<Void, Never>?
init() {
updatesTask = Task { [weak self] in
for await update in Transaction.updates {
await self?.handle(update)
}
}
}
deinit {
updatesTask?.cancel()
}
private func handle(_ update: VerificationResult<StoreKit.Transaction>) async {
// Never deliver or finish an untrusted purchase.
guard case .verified(let transaction) = update else { return }
// Only acknowledge our own consumable (future products get their own).
guard transaction.productID == Self.productID else { return }
await transaction.finish()
message = NSLocalizedString("Thank you! Your tip has been received. ⛽", comment: "")
}
var displayPrice: String {
product?.displayPrice ?? "£4.99"
}
@@ -599,7 +626,10 @@ final class TipStore: ObservableObject {
switch result {
case .success(let verification):
switch verification {
case .verified:
case .verified(let transaction):
// Consume the consumable otherwise StoreKit re-delivers
// it through Transaction.updates on every launch.
await transaction.finish()
message = NSLocalizedString("Thank you! Your tip has been received. ⛽", comment: "")
case .unverified:
message = NSLocalizedString("The purchase couldn't be verified. Please try again.", comment: "")