android: close the badge purchase screens in one step; ios, android: drop the order screen spinner

This commit is contained in:
spaced4ndy
2026-10-02 17:22:52 +04:00
parent 3edeb3be02
commit c74e313d02
7 changed files with 32 additions and 73 deletions
@@ -16,7 +16,6 @@ struct BadgesCheckOrderView: View {
let level: BadgeLevel
let period: BadgePeriod
@State private var purchasing = false
@State private var progressByTimeout = false
// presented from this view, not AlertManager: its host is behind the sheet these views open in
@State private var alert: SomeAlert?
@@ -60,20 +59,6 @@ struct BadgesCheckOrderView: View {
.frame(minHeight: g.size.height)
}
}
.overlay {
if progressByTimeout {
ProgressView().scaleEffect(2)
}
}
.onChange(of: purchasing) { _ in
if purchasing {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
progressByTimeout = purchasing
}
} else {
progressByTimeout = false
}
}
.frame(maxHeight: .infinity)
.navigationBarTitleDisplayMode(.inline)
.task { await store.load() }
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -19,24 +18,13 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingActionButton
import chat.simplex.res.MR
import kotlinx.coroutines.delay
@Composable
fun BadgesCheckOrderView(level: BadgeLevel, period: BadgePeriod, modalManager: ModalManager) {
fun BadgesCheckOrderView(level: BadgeLevel, period: BadgePeriod, modalManager: ModalManager, unwindToDepth: Int) {
val purchasing = remember { mutableStateOf(false) }
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(purchasing.value) {
progressByTimeout = if (purchasing.value) {
delay(1000)
purchasing.value
} else {
false
}
}
LaunchedEffect(Unit) { BadgeStore.load() }
CloseWhenSupportGivesWay(modalManager)
CloseWhenSupportGivesWay(modalManager, unwindToDepth)
ColumnWithScrollBar(
Modifier.padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp),
@@ -74,21 +62,6 @@ fun BadgesCheckOrderView(level: BadgeLevel, period: BadgePeriod, modalManager: M
BadgeBillingFooter(period)
}
}
if (progressByTimeout) {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(30.dp),
color = MaterialTheme.colors.secondary,
strokeWidth = 3.dp
)
}
}
}
@Composable
@@ -60,11 +60,11 @@ enum class BadgeLevel {
}
@Composable
fun BadgesChooseBadgeView(modalManager: ModalManager) {
fun BadgesChooseBadgeView(modalManager: ModalManager, unwindToDepth: Int) {
var selectedLevel by remember { mutableStateOf(BadgeLevel.Supporter) }
LaunchedEffect(Unit) { BadgeStore.load() }
CloseWhenSupportGivesWay(modalManager)
CloseWhenSupportGivesWay(modalManager, unwindToDepth)
ColumnWithScrollBar(
Modifier.background(MaterialTheme.colors.background).padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp),
@@ -108,10 +108,10 @@ fun BadgesChooseBadgeView(modalManager: ModalManager) {
// Nested Column with no spacing so the TextButtonBelowOnboardingButton sits directly under
// the action button (matches onboarding pattern where its own 7.5dp top padding is the gap).
Column(horizontalAlignment = Alignment.CenterHorizontally) {
ContinueButton(selectedLevel, modalManager)
ContinueButton(selectedLevel, modalManager, unwindToDepth)
// redeeming a code is here only when Support SimpleX offers the browser instead
if (badgeBrowserAllowed()) {
RedeemCodeButton(modalManager)
RedeemCodeButton(modalManager, unwindToDepth)
} else {
TextButtonBelowOnboardingButton("", null)
}
@@ -153,13 +153,13 @@ private fun LevelCard(level: BadgeLevel, selectedLevel: BadgeLevel, modifier: Mo
}
@Composable
private fun ContinueButton(selectedLevel: BadgeLevel, modalManager: ModalManager) {
private fun ContinueButton(selectedLevel: BadgeLevel, modalManager: ModalManager, unwindToDepth: Int) {
OnboardingActionButton(
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
labelId = MR.strings.badges_continue,
onboarding = null,
onclick = {
modalManager.showModal { BadgesHowLongView(selectedLevel, modalManager) }
modalManager.showModal { BadgesHowLongView(selectedLevel, modalManager, unwindToDepth) }
}
)
}
@@ -74,11 +74,11 @@ enum class BadgePeriod {
}
@Composable
fun BadgesHowLongView(level: BadgeLevel, modalManager: ModalManager) {
fun BadgesHowLongView(level: BadgeLevel, modalManager: ModalManager, unwindToDepth: Int) {
var selectedPeriod by remember { mutableStateOf(if (BadgePeriod.Monthly in badgePeriodsForSale) BadgePeriod.Monthly else BadgePeriod.OneMonth) }
LaunchedEffect(Unit) { BadgeStore.load() }
CloseWhenSupportGivesWay(modalManager)
CloseWhenSupportGivesWay(modalManager, unwindToDepth)
ColumnWithScrollBar(
Modifier.background(MaterialTheme.colors.background).padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp),
@@ -121,7 +121,7 @@ fun BadgesHowLongView(level: BadgeLevel, modalManager: ModalManager) {
Spacer(Modifier.weight(1f).heightIn(min = 8.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally) {
ContinueButton(level, selectedPeriod, modalManager)
ContinueButton(level, selectedPeriod, modalManager, unwindToDepth)
BadgeBillingFooter(selectedPeriod)
}
}
@@ -186,13 +186,13 @@ private fun savingsPercent(level: BadgeLevel, period: BadgePeriod): Int? =
if (period == BadgePeriod.Annual) BadgeStore.annualSavings(level) else null
@Composable
private fun ContinueButton(level: BadgeLevel, selectedPeriod: BadgePeriod, modalManager: ModalManager) {
private fun ContinueButton(level: BadgeLevel, selectedPeriod: BadgePeriod, modalManager: ModalManager, unwindToDepth: Int) {
OnboardingActionButton(
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
labelId = MR.strings.badges_continue,
onboarding = null,
onclick = {
modalManager.showModal(cardScreen = true) { BadgesCheckOrderView(level, selectedPeriod, modalManager) }
modalManager.showModal(cardScreen = true) { BadgesCheckOrderView(level, selectedPeriod, modalManager, unwindToDepth) }
}
)
}
@@ -94,12 +94,12 @@ fun showCannotRedeemAlert(message: String) {
}
@Composable
fun BadgesRedeemCodeView(modalManager: ModalManager) {
fun BadgesRedeemCodeView(modalManager: ModalManager, unwindToDepth: Int) {
val rhId = remember { chatModel.remoteHostId() }
val code = remember { mutableStateOf(TextFieldValue("")) }
val canonicalCode = remember { mutableStateOf<String?>(null) }
val submitting = remember { mutableStateOf(false) }
CloseWhenSupportGivesWay(modalManager)
CloseWhenSupportGivesWay(modalManager, unwindToDepth)
// when the text is unchanged, the field's own value is kept: it carries the cursor position and the
// keyboard's composition state, which BasicTextField loses unless they are passed back to it
@@ -32,7 +32,7 @@ import chat.simplex.common.views.onboarding.TextButtonBelowOnboardingButton
import chat.simplex.res.MR
@Composable
fun BadgesSupportSimplexView(modalManager: ModalManager) {
fun BadgesSupportSimplexView(modalManager: ModalManager, unwindToDepth: Int) {
ColumnWithScrollBar(
Modifier.background(MaterialTheme.colors.background).padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
@@ -65,29 +65,29 @@ fun BadgesSupportSimplexView(modalManager: ModalManager) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
if (badgeStoreAvailable) {
ChooseLevelButton(modalManager)
ChooseLevelButton(modalManager, unwindToDepth)
if (badgeBrowserAllowed()) {
BuyInBrowserTextButton()
} else {
RedeemCodeButton(modalManager)
RedeemCodeButton(modalManager, unwindToDepth)
}
} else {
BuyInBrowserButton(modalManager)
RedeemCodeButton(modalManager)
BuyInBrowserButton(modalManager, unwindToDepth)
RedeemCodeButton(modalManager, unwindToDepth)
}
}
}
}
@Composable
private fun ChooseLevelButton(modalManager: ModalManager) {
private fun ChooseLevelButton(modalManager: ModalManager, unwindToDepth: Int) {
OnboardingActionButton(
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
labelId = MR.strings.badges_choose_your_badge_title,
onboarding = null,
enabled = BadgeStore.canBuy(chatModel.currentUser.value?.userId),
onclick = {
modalManager.showModal { BadgesChooseBadgeView(modalManager) }
modalManager.showModal { BadgesChooseBadgeView(modalManager, unwindToDepth) }
}
)
}
@@ -118,10 +118,10 @@ private fun HowItWorksButton(modalManager: ModalManager) {
}
@Composable
fun RedeemCodeButton(modalManager: ModalManager) {
fun RedeemCodeButton(modalManager: ModalManager, unwindToDepth: Int) {
TextButtonBelowOnboardingButton(
text = stringResource(MR.strings.badges_redeem_code_button),
onClick = { modalManager.showModal { BadgesRedeemCodeView(modalManager) } }
onClick = { modalManager.showModal { BadgesRedeemCodeView(modalManager, unwindToDepth) } }
)
}
@@ -135,7 +135,7 @@ private fun BuyInBrowserTextButton() {
}
@Composable
private fun BuyInBrowserButton(modalManager: ModalManager) {
private fun BuyInBrowserButton(modalManager: ModalManager, unwindToDepth: Int) {
val uriHandler = LocalUriHandler.current
OnboardingActionButton(
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
@@ -145,7 +145,7 @@ private fun BuyInBrowserButton(modalManager: ModalManager) {
uriHandler.openUriCatching(badgePageUrl)
// desktop has no scheme to bring the code back, so the code is pasted into this screen, opened beside the browser
if (appPlatform.isDesktop && !modalManager.hasModalOpen(ModalViewId.BADGE_REDEEM_CODE)) {
modalManager.showModal(id = ModalViewId.BADGE_REDEEM_CODE) { BadgesRedeemCodeView(modalManager) }
modalManager.showModal(id = ModalViewId.BADGE_REDEEM_CODE) { BadgesRedeemCodeView(modalManager, unwindToDepth) }
}
}
)
@@ -3,6 +3,7 @@ package chat.simplex.common.views.badges
import androidx.compose.animation.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import chat.simplex.common.model.BadgeModel
import chat.simplex.common.model.BadgeState
import chat.simplex.common.platform.chatModel
@@ -13,6 +14,7 @@ import chat.simplex.common.views.helpers.ModalView
@Composable
fun BadgesView(modalManager: ModalManager, close: () -> Unit) {
val shownBadge = currentShownBadge()
val unwindToDepth = remember { modalManager.openModalCount() }
// the card look is a modal setting, so the modal is composed here to follow the screen shown
ModalView(close, cardScreen = shownBadge != null) {
@@ -27,7 +29,7 @@ fun BadgesView(modalManager: ModalManager, close: () -> Unit) {
// holds the purchase screens' slot, so a consumable cannot be bought twice
BadgesPurchaseStateView(purchaseState, onDismiss = close)
} else {
BadgesSupportSimplexView(modalManager)
BadgesSupportSimplexView(modalManager, unwindToDepth)
}
}
}
@@ -39,13 +41,12 @@ fun currentShownBadge(): BadgeState? {
return if (badgeState != null && badgeState.shown) badgeState else null
}
// each purchase screen closes itself when it recomposes with a purchase in flight or a badge shown, as iOS
// pops them when Support SimpleX gives way to either
// runs in the screen on top, the only one composed, and closes every screen above the badges modal in one go
@Composable
fun CloseWhenSupportGivesWay(modalManager: ModalManager) {
fun CloseWhenSupportGivesWay(modalManager: ModalManager, unwindToDepth: Int) {
val gaveWay = BadgeStore.purchaseState(chatModel.currentUser.value?.userId) != null || currentShownBadge() != null
LaunchedEffect(gaveWay) {
if (gaveWay) modalManager.closeModal()
if (gaveWay) while (modalManager.openModalCount() > unwindToDepth) modalManager.closeModal()
}
}