mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-06 20:48:27 +00:00
ios, android: tell whoever is looking when a purchase is refused
This commit is contained in:
@@ -66,7 +66,6 @@ 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
|
||||
@@ -112,8 +111,7 @@ 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
|
||||
// purchases the user started this run, by transaction: the store is not relied on to echo the invoice back
|
||||
private var awaitedTransactions: Set<UInt64> = []
|
||||
let refusals = PassthroughSubject<Error, Never>()
|
||||
private var transactionUpdates: Task<Void, Never>? = nil
|
||||
|
||||
private init() {}
|
||||
@@ -209,7 +207,6 @@ final class BadgeStore: ObservableObject {
|
||||
do {
|
||||
let outcome = try await storePurchase(product, invoiceId)
|
||||
if case let .purchased(receipt) = outcome, receipt.signatureVerified {
|
||||
await MainActor.run { _ = awaitedTransactions.insert(receipt.transactionId) }
|
||||
await handOver(receipt)
|
||||
}
|
||||
await MainActor.run { buying = false }
|
||||
@@ -264,9 +261,8 @@ final class BadgeStore: ObservableObject {
|
||||
|
||||
private func settle(_ receipt: BadgeStoreReceipt, refusal: Error?) async {
|
||||
await receipt.transaction.finish()
|
||||
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)) }
|
||||
if let refusal {
|
||||
await MainActor.run { refusals.send(refusal) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +329,6 @@ private func storeReceipt(_ verification: VerificationResult<Transaction>) -> Ba
|
||||
return BadgeStoreReceipt(
|
||||
jws: verification.jwsRepresentation,
|
||||
productId: t.productID,
|
||||
transactionId: t.id,
|
||||
invoiceId: t.appAccountToken,
|
||||
signatureVerified: signatureVerified,
|
||||
transaction: t
|
||||
|
||||
@@ -30,6 +30,12 @@ struct BadgesView: View {
|
||||
// holds the purchase screens' slot, so a consumable cannot be bought twice
|
||||
BadgesPurchaseStateView(title: purchaseState.title, message: purchaseState.message, failure: store.creditError(chatModel.currentUser?.userId), showsAsSheet: showsAsSheet)
|
||||
.transition(.opacity)
|
||||
.onReceive(store.refusals) { refusal in
|
||||
// only the issuing screen belongs to the active profile's held purchase, so a refusal shown there reads as its own
|
||||
if purchaseState == .issuing {
|
||||
showAlert(NSLocalizedString("Purchase error", comment: "alert title"), message: redeemErrorText(refusal, purchase: true))
|
||||
}
|
||||
}
|
||||
} else if store.checkingPurchases {
|
||||
BadgesPurchaseStateView(title: "Checking your purchases", showsAsSheet: showsAsSheet)
|
||||
.transition(.opacity)
|
||||
|
||||
+4
-9
@@ -6,12 +6,10 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.AlertManager
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.NumberFormat
|
||||
import java.util.Currency
|
||||
@@ -159,8 +157,7 @@ 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)
|
||||
// purchases the user started this run, by token: the store is not relied on to echo the invoice back
|
||||
private val awaitedTransactions = mutableSetOf<String>()
|
||||
val refusals = MutableSharedFlow<ChatError>()
|
||||
|
||||
fun purchaseState(userId: Long?): BadgePurchaseState? {
|
||||
if (!badgeStoreAvailable) return null
|
||||
@@ -246,7 +243,6 @@ object BadgeStore {
|
||||
try {
|
||||
val outcome = storePurchase(id, invoiceId)
|
||||
if (outcome is BadgePurchaseOutcome.Purchased) {
|
||||
withContext(Dispatchers.Main) { awaitedTransactions += outcome.receipt.token }
|
||||
handOver(outcome.receipt)
|
||||
}
|
||||
return outcome
|
||||
@@ -312,9 +308,8 @@ object BadgeStore {
|
||||
|
||||
private suspend fun settle(receipt: BadgeStoreReceipt, refusal: ChatError?) {
|
||||
finish(receipt)
|
||||
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))
|
||||
if (refusal != null) {
|
||||
withContext(Dispatchers.Main) { refusals.emit(refusal) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -7,8 +7,10 @@ import androidx.compose.runtime.remember
|
||||
import chat.simplex.common.model.BadgeModel
|
||||
import chat.simplex.common.model.BadgeState
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.views.helpers.AlertManager
|
||||
import chat.simplex.common.views.helpers.ModalManager
|
||||
import chat.simplex.common.views.helpers.ModalView
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@@ -29,6 +31,14 @@ fun BadgesView(modalManager: ModalManager, close: () -> Unit) {
|
||||
} else if (purchaseState != null) {
|
||||
// holds the purchase screens' slot, so a consumable cannot be bought twice
|
||||
BadgesPurchaseStateView(purchaseState.title, purchaseState.message, BadgeStore.creditError(chatModel.currentUser.value?.userId), onDismiss = close)
|
||||
LaunchedEffect(purchaseState) {
|
||||
BadgeStore.refusals.collect { refusal ->
|
||||
// only the issuing screen belongs to the active profile's held purchase, so a refusal shown there reads as its own
|
||||
if (purchaseState == BadgePurchaseState.Issuing) {
|
||||
AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.badges_purchase_error), text = chatModel.controller.redeemErrorText(refusal, purchase = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (checkingPurchases) {
|
||||
BadgesPurchaseStateView(MR.strings.badges_checking_purchases_title, null, onDismiss = close)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user