From c74e313d02307abfbc484d198586252b09f44c66 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:22:52 +0400 Subject: [PATCH] android: close the badge purchase screens in one step; ios, android: drop the order screen spinner --- .../Views/Badges/BadgesCheckOrderView.swift | 15 --------- .../views/badges/BadgesCheckOrderView.kt | 31 ++----------------- .../views/badges/BadgesChooseBadgeView.kt | 12 +++---- .../common/views/badges/BadgesHowLongView.kt | 10 +++--- .../views/badges/BadgesRedeemCodeView.kt | 4 +-- .../views/badges/BadgesSupportSimplexView.kt | 22 ++++++------- .../simplex/common/views/badges/BadgesView.kt | 11 ++++--- 7 files changed, 32 insertions(+), 73 deletions(-) diff --git a/apps/ios/Shared/Views/Badges/BadgesCheckOrderView.swift b/apps/ios/Shared/Views/Badges/BadgesCheckOrderView.swift index aee15324e6..2c0dd851ed 100644 --- a/apps/ios/Shared/Views/Badges/BadgesCheckOrderView.swift +++ b/apps/ios/Shared/Views/Badges/BadgesCheckOrderView.swift @@ -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() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesCheckOrderView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesCheckOrderView.kt index b0450d2a89..458dc601c2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesCheckOrderView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesCheckOrderView.kt @@ -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 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesChooseBadgeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesChooseBadgeView.kt index 110b68ca70..0fac45ac81 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesChooseBadgeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesChooseBadgeView.kt @@ -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) } } ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesHowLongView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesHowLongView.kt index 5b430b7350..0dfc3ac742 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesHowLongView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesHowLongView.kt @@ -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) } } ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesRedeemCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesRedeemCodeView.kt index a7e4b433e6..11367134f7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesRedeemCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesRedeemCodeView.kt @@ -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(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 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesSupportSimplexView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesSupportSimplexView.kt index 81d840c9a0..aa2144ff59 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesSupportSimplexView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesSupportSimplexView.kt @@ -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) } } } ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesView.kt index a21e60b661..6f16ba537e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesView.kt @@ -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() } }