From 0d7efd0bc33e0dea8d415eb313c5e679861373ee Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:18:40 +0400 Subject: [PATCH] ios, android: tell the buyer of a refusal without relying on the store echoing the invoice --- apps/ios/Shared/Views/Badges/BadgeStore.swift | 10 ++++++---- .../chat/simplex/common/views/badges/BadgeStore.kt | 9 ++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/ios/Shared/Views/Badges/BadgeStore.swift b/apps/ios/Shared/Views/Badges/BadgeStore.swift index d2a1a302fe..696ed7c29c 100644 --- a/apps/ios/Shared/Views/Badges/BadgeStore.swift +++ b/apps/ios/Shared/Views/Badges/BadgeStore.swift @@ -66,6 +66,7 @@ struct BadgeStoreReceipt { // the signed token the badge service verifies - never transaction.jsonRepresentation let jws: String let productId: String + let transactionId: UInt64 let invoiceId: UUID? let signatureVerified: Bool let transaction: Transaction @@ -111,8 +112,8 @@ final class BadgeStore: ObservableObject { // set when the first sweep has returned, which is after every transaction the store holds was handed // to core - until then a purchase made while the app was not running is unknown @Published private var reconciledOnce = false - // invoices of the purchases the user started this run, whose refusal is theirs to be told of - private var awaitedInvoices: Set = [] + // purchases the user started this run, by transaction: the store is not relied on to echo the invoice back + private var awaitedTransactions: Set = [] private var transactionUpdates: Task? = nil private init() {} @@ -208,7 +209,7 @@ final class BadgeStore: ObservableObject { do { let outcome = try await storePurchase(product, invoiceId) if case let .purchased(receipt) = outcome, receipt.signatureVerified { - await MainActor.run { _ = awaitedInvoices.insert(invoice) } + await MainActor.run { _ = awaitedTransactions.insert(receipt.transactionId) } await handOver(receipt) } await MainActor.run { buying = false } @@ -263,7 +264,7 @@ final class BadgeStore: ObservableObject { private func settle(_ receipt: BadgeStoreReceipt, refusal: Error?) async { await receipt.transaction.finish() - let awaited = await MainActor.run { receipt.echoedInvoiceId.map { awaitedInvoices.remove($0) != nil } ?? false } + let awaited = await MainActor.run { awaitedTransactions.remove(receipt.transactionId) != nil } if awaited, let refusal { await MainActor.run { showAlert(NSLocalizedString("Purchase error", comment: "alert title"), message: redeemErrorText(refusal, purchase: true)) } } @@ -332,6 +333,7 @@ private func storeReceipt(_ verification: VerificationResult) -> Ba return BadgeStoreReceipt( jws: verification.jwsRepresentation, productId: t.productID, + transactionId: t.id, invoiceId: t.appAccountToken, signatureVerified: signatureVerified, transaction: t diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgeStore.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgeStore.kt index 0ee09547bd..f292da605c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgeStore.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgeStore.kt @@ -159,9 +159,8 @@ object BadgeStore { // set when the first sweep has returned or failed, which is after every purchase the store holds was handed // to core - until then a purchase made while the app was not running is unknown private val reconciledOnce = mutableStateOf(false) - // invoices of the purchases the user started this run, whose refusal is theirs to be told of; - // read and written on the main thread only - private val awaitedInvoices = mutableSetOf() + // purchases the user started this run, by token: the store is not relied on to echo the invoice back + private val awaitedTransactions = mutableSetOf() fun purchaseState(userId: Long?): BadgePurchaseState? { if (!badgeStoreAvailable) return null @@ -247,7 +246,7 @@ object BadgeStore { try { val outcome = storePurchase(id, invoiceId) if (outcome is BadgePurchaseOutcome.Purchased) { - withContext(Dispatchers.Main) { awaitedInvoices += invoiceId } + withContext(Dispatchers.Main) { awaitedTransactions += outcome.receipt.token } handOver(outcome.receipt) } return outcome @@ -313,7 +312,7 @@ object BadgeStore { private suspend fun settle(receipt: BadgeStoreReceipt, refusal: ChatError?) { finish(receipt) - val awaited = withContext(Dispatchers.Main) { receipt.invoiceId?.let { awaitedInvoices.remove(it) } == true } + val awaited = withContext(Dispatchers.Main) { awaitedTransactions.remove(receipt.token) } if (awaited && refusal != null) { AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.badges_purchase_error), text = chatModel.controller.redeemErrorText(refusal, purchase = true)) }