From cf3d6c45d0a964198cc334e99a20e7e5d02cba5b Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:59:39 +0000 Subject: [PATCH] ios, android: tell the buyer once a run when a swept purchase cannot be handed over (#7674) --- apps/ios/Shared/Views/Badges/BadgeStore.swift | 12 +++++++++++- .../chat/simplex/common/views/badges/BadgeStore.kt | 14 +++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/apps/ios/Shared/Views/Badges/BadgeStore.swift b/apps/ios/Shared/Views/Badges/BadgeStore.swift index 8194abcc46..2b265b01ab 100644 --- a/apps/ios/Shared/Views/Badges/BadgeStore.swift +++ b/apps/ios/Shared/Views/Badges/BadgeStore.swift @@ -111,6 +111,8 @@ final class BadgeStore: ObservableObject { // set once presentUnfinished has read the store: until then, an Ask to Buy approved while the app was // closed, or a purchase it died before handing over, are both unknown, so canBuy refuses to buy again @Published private var reconciledOnce = false + // the sweep has no buyer waiting and runs at four triggers, so a failure that cannot clear is told once a run + private var sweepFailureShown = false private var transactionUpdates: Task? = nil private init() {} @@ -321,9 +323,17 @@ final class BadgeStore: ObservableObject { if !badgeOneTimeProductIds.contains(receipt.productId) { await receipt.transaction.finish() } else if receipt.signatureVerified { - // no buyer to tell: the receipt stays unfinished and the next sweep hands it over again + // the receipt stays unfinished, so the next sweep hands it over again do { try await handOver(receipt) } catch let error { logger.error("BadgeStore.reconcile: \(responseError(error))") + await MainActor.run { + // showAlert presents on the top view controller, which the launch sweep can precede; + // an alert with nowhere to go must not spend the one this run gets + if !sweepFailureShown, getTopViewController() != nil { + sweepFailureShown = true + showAlert(NSLocalizedString("Purchase error", comment: "alert title"), message: redeemErrorText(error, purchase: true)) + } + } } } } 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 3b82ac1371..c9b413221d 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 @@ -161,6 +161,8 @@ object BadgeStore { // set once presentUnfinished has read the store, or failed to: until then, a slow payment completed while // the app was closed, or a purchase it died before handing over, are both unknown, so canBuy refuses private val reconciledOnce = mutableStateOf(false) + // the sweep has no buyer waiting and runs at four triggers, so a failure that cannot clear is told once a run + private var sweepFailureShown = false fun purchaseState(userId: Long?): BadgePurchaseState? { if (!badgeStoreAvailable) return null @@ -372,12 +374,22 @@ object BadgeStore { when (outcome) { is BadgePurchaseOutcome.Purchased -> if (outcome.receipt.productId !in badgeOneTimeProductIds) finish(outcome.receipt) - // no buyer to tell: the purchase stays unfinished and the next sweep hands it over again + // the purchase stays unfinished, so the next sweep hands it over again else try { handOver(outcome.receipt) } catch (e: Exception) { if (e is CancellationException) throw e Log.e(TAG, "BadgeStore.reconcile: ${e.stackTraceToString()}") + withContext(Dispatchers.Main) { + if (!sweepFailureShown) { + sweepFailureShown = true + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.badges_purchase_error), + text = if (e is BadgeStoreError.ApiError) chatModel.controller.redeemErrorText(e.err, purchase = true) + else "${generalGetString(MR.strings.error_prefix)}: ${e.message ?: e}" + ) + } + } } is BadgePurchaseOutcome.Pending -> if (outcome.invoiceId != null) withContext(Dispatchers.Main) { waitingForApproval.value += outcome.invoiceId }