ios, android: show progress while a badge is bought, and say the payment is complete while it is issued

This commit is contained in:
spaced4ndy
2026-10-02 13:47:39 +04:00
parent a70ab3bac5
commit e33a06fc32
9 changed files with 57 additions and 95 deletions
@@ -88,10 +88,7 @@ data class BadgeStoreReceipt(
// the token the badge service verifies with the Publisher API
val token: String,
val productId: String,
val orderId: String?,
val invoiceId: String?,
// only set for test products - a real Play purchase has no environment to report
val environment: String? = null
val invoiceId: String?
)
// TODO [badges] Play Billing has no offline product configuration. Set to true to price the screens
@@ -224,15 +221,14 @@ object BadgeStore {
}
// A one-time purchase has its core record before the store charges, and every store outcome reaches it.
suspend fun purchase(level: BadgeLevel, period: BadgePeriod): Pair<BadgePurchaseOutcome, String> {
suspend fun purchase(level: BadgeLevel, period: BadgePeriod): BadgePurchaseOutcome {
val id = badgeStoreProductId(level, period)
if (!products.value.containsKey(id)) throw BadgeStoreError.ProductUnavailable(id.productId)
// a subscription is never sent to core, so nothing would ever finish it later
if (id.productId !in badgeOneTimeProductIds) {
val invoiceId = newBadgeInvoiceId()
val outcome = storePurchase(id, invoiceId)
val outcome = storePurchase(id, newBadgeInvoiceId())
if (outcome is BadgePurchaseOutcome.Purchased) finish(outcome.receipt)
return outcome to invoiceId
return outcome
}
val rhId = chatModel.remoteHostId()
val userId = chatModel.currentUser.value?.userId ?: throw BadgeStoreError.NoActiveProfile
@@ -247,7 +243,7 @@ object BadgeStore {
is BadgePurchaseOutcome.Cancelled -> closeInvoice(userId, invoiceId)
is BadgePurchaseOutcome.Pending -> {}
}
return outcome to invoiceId
return outcome
} catch (e: Exception) {
closeInvoice(userId, invoiceId)
throw e
@@ -262,9 +258,7 @@ object BadgeStore {
BadgeStoreReceipt(
token = "test-${UUID.randomUUID()}",
productId = id.productId,
orderId = null,
invoiceId = invoiceId,
environment = "test products"
invoiceId = invoiceId
)
)
} else {
@@ -1,6 +1,7 @@
package chat.simplex.common.views.badges
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
@@ -48,6 +49,7 @@ fun BadgesCheckOrderView(level: BadgeLevel, period: BadgePeriod, modalManager: M
.padding(top = 20.dp)
.clip(RoundedCornerShape(16.dp))
.background(sectionCardColor())
.border(1.dp, MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.92f), RoundedCornerShape(16.dp))
.padding(horizontal = 24.dp, vertical = 12.dp)
) {
OrderRow(MR.strings.badges_your_badge, stringResource(level.title))
@@ -63,6 +65,22 @@ fun BadgesCheckOrderView(level: BadgeLevel, period: BadgePeriod, modalManager: M
BadgeBillingFooter(period)
}
}
if (purchasing.value) {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Surface(Modifier.size(50.dp), color = MaterialTheme.colors.background.copy(0.9f), contentColor = LocalContentColor.current, shape = RoundedCornerShape(50)){}
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(30.dp),
color = MaterialTheme.colors.secondary,
strokeWidth = 3.dp
)
}
}
}
@Composable
@@ -99,10 +117,7 @@ private fun purchase(level: BadgeLevel, period: BadgePeriod, purchasing: Mutable
// not withBGApi: the purchase waits for the user in the Play sheet and would block chat API calls
withLongRunningApi {
try {
val (outcome, invoiceId) = BadgeStore.purchase(level, period)
if (outcome is BadgePurchaseOutcome.Purchased && outcome.receipt.productId !in badgeOneTimeProductIds) {
showPurchasedAlert(outcome.receipt, invoiceId)
}
BadgeStore.purchase(level, period)
purchasing.value = false
} catch (e: Exception) {
Log.e(TAG, "BadgesCheckOrderView.purchase: ${e.stackTraceToString()}")
@@ -115,25 +130,3 @@ private fun purchase(level: BadgeLevel, period: BadgePeriod, purchasing: Mutable
}
}
}
// TODO [badges] store integration diagnostics - replaced by the issued badge once subscriptions are delivered.
private fun showPurchasedAlert(receipt: BadgeStoreReceipt, invoiceId: String) {
val returnedInvoice = when (receipt.invoiceId) {
null -> "none"
invoiceId -> "yes"
else -> "mismatch: ${receipt.invoiceId}"
}
val lines = mutableListOf(
"Product: ${receipt.productId}",
"Invoice: $invoiceId",
"Invoice returned by Google: $returnedInvoice",
"Order: ${receipt.orderId ?: "none"}"
)
if (receipt.environment != null) lines.add("Environment: ${receipt.environment}")
val summary = lines.joinToString("\n")
Log.d(TAG, "badge purchase succeeded\n$summary")
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.badges_purchase_successful),
text = summary
)
}
@@ -171,7 +171,7 @@ private fun PeriodCard(level: BadgeLevel, period: BadgePeriod, selectedPeriod: B
Text(stringResource(period.label), style = MaterialTheme.typography.body1, color = textColor, textAlign = TextAlign.Center)
Text(period.priceText(BadgeStore.price(level, period)), style = MaterialTheme.typography.h3, fontWeight = FontWeight.SemiBold, color = textColor, textAlign = TextAlign.Center)
val percent = savingsPercent(level, period)
if (percent != null) {
if (forSale && percent != null) {
Text(
stringResource(MR.strings.badges_savings).format("${percent}%"),
style = MaterialTheme.typography.body2,
@@ -3180,14 +3180,13 @@
<string name="badges_banner_subtitle">Get badge + better files</string>
<string name="badges_banner_dismiss_message">You can support SimpleX later in Settings.</string>
<string name="badges_larger_files_longer">Support SimpleX to send larger files that stay available longer</string>
<string name="badges_purchase_successful">Purchase successful</string>
<string name="badges_purchase_error">Purchase error</string>
<string name="badges_purchase_will_retry">The purchase will be retried, and the badge will arrive.</string>
<string name="badges_check_your_order_title">Check your order</string>
<string name="badges_order_duration">Duration</string>
<string name="badges_order_total">Total</string>
<string name="badges_issuing_title">Issuing your badge</string>
<string name="badges_issuing_body">The store has taken the payment. Keeps trying if you close the app.</string>
<string name="badges_issuing_body">Your payment is complete. The badge will be added to this profile.</string>
<string name="badges_waiting_for_approval_title">Waiting for approval</string>
<string name="badges_waiting_for_approval_body">Nothing has been charged.</string>
<string name="badges_checking_title">Checking your purchase</string>