From 2ffa90d8795ff55a7a67d999c504350c2a1bbf08 Mon Sep 17 00:00:00 2001 From: FuelBoard Contributor Date: Sun, 16 Aug 2026 08:23:18 +0100 Subject: [PATCH] Tip jar: finish transactions + Transaction.updates listener MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- FuelBoard/SettingsView.swift | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/FuelBoard/SettingsView.swift b/FuelBoard/SettingsView.swift index ee316c4..c02245f 100644 --- a/FuelBoard/SettingsView.swift +++ b/FuelBoard/SettingsView.swift @@ -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? + + init() { + updatesTask = Task { [weak self] in + for await update in Transaction.updates { + await self?.handle(update) + } + } + } + + deinit { + updatesTask?.cancel() + } + + private func handle(_ update: VerificationResult) 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: "")