diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 49a0b5c0a6..b5767f2348 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -2425,6 +2425,7 @@ private func changeActiveUser_(_ userId: Int64, viewPwd: String?) throws { m.currentUser = try apiSetActiveUser(userId, viewPwd: viewPwd) m.users = try listUsers() try getUserChatData() + Task { await BadgeStore.shared.presentUnfinished() } } func changeActiveUserAsync_(_ userId: Int64?, viewPwd: String?, keepingChatId: String? = nil) async throws { @@ -2440,6 +2441,7 @@ func changeActiveUserAsync_(_ userId: Int64?, viewPwd: String?, keepingChatId: S m.users = users } try await getUserChatDataAsync(keepingChatId: keepingChatId) + Task { await BadgeStore.shared.presentUnfinished() } await MainActor.run { if let currentUser = currentUser, var (_, invitation) = ChatModel.shared.callInvitations.first(where: { _, inv in inv.user.userId == userId }) { invitation.user = currentUser diff --git a/apps/ios/Shared/Views/Badges/BadgeStore.swift b/apps/ios/Shared/Views/Badges/BadgeStore.swift index b2566b2c7e..5346f8e914 100644 --- a/apps/ios/Shared/Views/Badges/BadgeStore.swift +++ b/apps/ios/Shared/Views/Badges/BadgeStore.swift @@ -259,7 +259,7 @@ final class BadgeStore: ObservableObject { let refused = badgeReceiptRefused(error) if refused { await finish(receipt) } let text = redeemErrorText(error, purchase: true) - alertText = refused ? text : text + "\n\n" + NSLocalizedString("The purchase will be retried, and the badge will arrive.", comment: "alert message") + alertText = refused || retryCannotCredit(error) ? text : text + "\n\n" + NSLocalizedString("The purchase will be retried, and the badge will arrive.", comment: "alert message") } await loadCurrentBadgeState() let userWaiting = await MainActor.run { presenting.removeValue(forKey: receipt.transactionId) == true } @@ -393,3 +393,12 @@ private func badgeReceiptRefused(_ error: Error) -> Bool { } return false } + +private func retryCannotCredit(_ error: Error) -> Bool { + guard case let .error(.badgeRedeemError(e)) = error as? ChatError else { return false } + switch e { + case .badgeActive, .serviceNotConfigured: return true + case let .serviceError(code): return code == .providerNotConfigured || code == .productUnavailable + default: return false + } +} diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index acc4939cf8..1815a53f4f 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -917,8 +917,11 @@ public func badgeServiceErrorText(_ code: BadgeServiceErrorCode) -> String? { case .internalError: NSLocalizedString("The badge service reported an internal error.", comment: "alert message") case .receiptInvalid: NSLocalizedString("The store did not confirm this purchase.", comment: "alert message") case .receiptUsed: NSLocalizedString("This purchase was already credited to a badge on another device or profile.", comment: "alert message") - case .badRequest, .unknownOfferId, .offerDisabled, .offerMismatch, .productUnavailable, - .paymentNotEntitled, .paymentPending, .providerUnavailable, .providerNotConfigured, .unknown: nil + case .paymentPending: NSLocalizedString("The payment has not been confirmed yet.", comment: "alert message") + case .providerUnavailable: NSLocalizedString("The store could not be reached.", comment: "alert message") + case .providerNotConfigured: NSLocalizedString("Badges cannot be bought in the app yet.", comment: "alert message") + case .productUnavailable: NSLocalizedString("This badge is not on sale.", comment: "alert message") + case .badRequest, .unknownOfferId, .offerDisabled, .offerMismatch, .paymentNotEntitled, .unknown: nil } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 7594547128..3a224f8d0c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -789,6 +789,7 @@ object ChatController { chatModel.users.clear() chatModel.users.addAll(users) getUserChatData(rhId, keepingChatId = keepingChatId) + withLongRunningApi { BadgeStore.presentUnfinished() } val invitation = chatModel.callInvitations.values.firstOrNull { inv -> inv.user.userId == toUserId } if (invitation != null && currentUser != null) { chatModel.callManager.reportNewIncomingCall(invitation.copy(user = currentUser)) @@ -7468,6 +7469,10 @@ fun badgeServiceErrorText(code: BadgeServiceErrorCode): String? = when (code) { is BadgeServiceErrorCode.Internal -> generalGetString(MR.strings.badges_error_service_internal) is BadgeServiceErrorCode.ReceiptInvalid -> generalGetString(MR.strings.badges_error_receipt_invalid) is BadgeServiceErrorCode.ReceiptUsed -> generalGetString(MR.strings.badges_error_receipt_used) + is BadgeServiceErrorCode.PaymentPending -> generalGetString(MR.strings.badges_error_payment_pending) + is BadgeServiceErrorCode.ProviderUnavailable -> generalGetString(MR.strings.badges_error_provider_unavailable) + is BadgeServiceErrorCode.ProviderNotConfigured -> generalGetString(MR.strings.badges_error_provider_not_configured) + is BadgeServiceErrorCode.ProductUnavailable -> generalGetString(MR.strings.badges_error_product_unavailable) else -> null } 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 dc6ad6f55d..2458d119b2 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 @@ -9,6 +9,7 @@ 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.withContext @@ -299,10 +300,14 @@ object BadgeStore { val refused = badgeReceiptRefused(r.err) if (refused) finish(receipt) val text = chatModel.controller.redeemErrorText(r.err, purchase = true) - alertText = if (refused) text else text + "\n\n" + generalGetString(MR.strings.badges_purchase_will_retry) + alertText = if (refused || retryCannotCredit(r.err)) text else text + "\n\n" + generalGetString(MR.strings.badges_purchase_will_retry) } null -> {} } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e(TAG, "BadgeStore.presentPurchase: ${e.stackTraceToString()}") + alertText = "${generalGetString(MR.strings.error_prefix)}: ${e.message ?: e}" + "\n\n" + generalGetString(MR.strings.badges_purchase_will_retry) } finally { withContext(NonCancellable) { chatModel.controller.loadBadgeState(chatModel.remoteHostId()) } userWaiting = withContext(Dispatchers.Main + NonCancellable) { presenting.remove(receipt.token) == true } @@ -419,3 +424,10 @@ private fun badgeReceiptRefused(err: ChatError?): Boolean { val code = (redeemError as? BadgeRedeemError.ServiceError)?.serviceError return code is BadgeServiceErrorCode.ReceiptInvalid || code is BadgeServiceErrorCode.ReceiptUsed } + +private fun retryCannotCredit(err: ChatError?): Boolean = + when (val e = ((err as? ChatError.ChatErrorChat)?.errorType as? ChatErrorType.CEBadgeRedeemError)?.badgeRedeemError) { + is BadgeRedeemError.BadgeActive, is BadgeRedeemError.ServiceNotConfigured -> true + is BadgeRedeemError.ServiceError -> e.serviceError is BadgeServiceErrorCode.ProviderNotConfigured || e.serviceError is BadgeServiceErrorCode.ProductUnavailable + else -> false + } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index ed4bf596d2..636fa4845c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -3225,6 +3225,10 @@ The badge service reported an internal error. The store did not confirm this purchase. This purchase was already credited to a badge on another device or profile. + The payment has not been confirmed yet. + The store could not be reached. + Badges cannot be bought in the app yet. + This badge is not on sale. The code was accepted, but the badge it grants has already ended. The code could not be redeemed. The code you scanned is not a badge code.