mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-26 12:44:38 +00:00
Implements a bunch of missing things in the backup checkout flow stuff.
This commit is contained in:
committed by
Cody Henthorne
parent
079a3d4fee
commit
8bbb7d56e0
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ConfirmBackupCancellationDialog(
|
||||
onConfirmAndDownloadNow: () -> Unit,
|
||||
onConfirmAndDownloadLater: () -> Unit,
|
||||
onKeepSubscriptionClick: () -> Unit
|
||||
) {
|
||||
BasicAlertDialog(onDismissRequest = onKeepSubscriptionClick) {
|
||||
Surface(
|
||||
shape = AlertDialogDefaults.shape,
|
||||
color = AlertDialogDefaults.containerColor
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(id = R.string.ConfirmBackupCancellationDialog__confirm_cancellation),
|
||||
color = AlertDialogDefaults.titleContentColor,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier
|
||||
.padding(top = 24.dp)
|
||||
.padding(horizontal = 24.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.ConfirmBackupCancellationDialog__you_wont_be_charged_again),
|
||||
color = AlertDialogDefaults.textContentColor,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp)
|
||||
.padding(horizontal = 24.dp)
|
||||
)
|
||||
|
||||
TextButton(
|
||||
onClick = onConfirmAndDownloadNow,
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(end = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.ConfirmBackupCancellationDialog__confirm_and_download_now)
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onConfirmAndDownloadLater,
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(end = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.ConfirmBackupCancellationDialog__confirm_and_download_later)
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onKeepSubscriptionClick,
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(end = 12.dp, bottom = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.ConfirmBackupCancellationDialog__keep_subscription)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun ConfirmCancellationDialogPreview() {
|
||||
Previews.Preview {
|
||||
ConfirmBackupCancellationDialog(
|
||||
onKeepSubscriptionClick = {},
|
||||
onConfirmAndDownloadNow = {},
|
||||
onConfirmAndDownloadLater = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,7 @@ private fun SheetContent(
|
||||
|
||||
MessageBackupsTypeBlock(
|
||||
messageBackupsType = messageBackupsType,
|
||||
isCurrent = false,
|
||||
isSelected = false,
|
||||
onSelected = {},
|
||||
enabled = false,
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.navigation.compose.composable
|
||||
@@ -36,7 +37,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), DonationCheckoutDelegate.C
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state
|
||||
val state by viewModel.stateFlow.collectAsState()
|
||||
val navController = rememberNavController()
|
||||
|
||||
val checkoutDelegate = remember {
|
||||
@@ -93,11 +94,13 @@ class MessageBackupsFlowFragment : ComposeFragment(), DonationCheckoutDelegate.C
|
||||
|
||||
composable(route = MessageBackupsScreen.TYPE_SELECTION.name) {
|
||||
MessageBackupsTypeSelectionScreen(
|
||||
currentBackupTier = state.currentMessageBackupTier,
|
||||
selectedBackupTier = state.selectedMessageBackupTier,
|
||||
availableBackupTypes = state.availableBackupTypes,
|
||||
onMessageBackupsTierSelected = viewModel::onMessageBackupTierUpdated,
|
||||
onNavigationClick = viewModel::goToPreviousScreen,
|
||||
onReadMoreClicked = {},
|
||||
onCancelSubscriptionClicked = viewModel::displayCancellationDialog,
|
||||
onNextClicked = viewModel::goToNextScreen
|
||||
)
|
||||
|
||||
@@ -115,6 +118,20 @@ class MessageBackupsFlowFragment : ComposeFragment(), DonationCheckoutDelegate.C
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (state.screen == MessageBackupsScreen.CANCELLATION_DIALOG) {
|
||||
ConfirmBackupCancellationDialog(
|
||||
onConfirmAndDownloadNow = {
|
||||
// TODO [message-backups] Set appropriate state to handle post-cancellation action.
|
||||
viewModel.goToNextScreen()
|
||||
},
|
||||
onConfirmAndDownloadLater = {
|
||||
// TODO [message-backups] Set appropriate state to handle post-cancellation action.
|
||||
viewModel.goToNextScreen()
|
||||
},
|
||||
onKeepSubscriptionClick = viewModel::goToPreviousScreen
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,16 +148,30 @@ class MessageBackupsFlowFragment : ComposeFragment(), DonationCheckoutDelegate.C
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (state.screen == MessageBackupsScreen.CREATING_IN_APP_PAYMENT) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (state.screen == MessageBackupsScreen.PROCESS_PAYMENT) {
|
||||
checkoutDelegate.handleGatewaySelectionResponse(state.inAppPayment!!)
|
||||
viewModel.goToPreviousScreen()
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (state.screen == MessageBackupsScreen.PROCESS_CANCELLATION) {
|
||||
cancelSubscription()
|
||||
viewModel.goToPreviousScreen()
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (state.screen == MessageBackupsScreen.CHECKOUT_SHEET) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (state.screen == MessageBackupsScreen.CANCELLATION_DIALOG) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
val routeScreen = MessageBackupsScreen.valueOf(route)
|
||||
if (routeScreen.isAfter(state.screen)) {
|
||||
navController.popBackStack()
|
||||
@@ -150,6 +181,16 @@ class MessageBackupsFlowFragment : ComposeFragment(), DonationCheckoutDelegate.C
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelSubscription() {
|
||||
findNavController().safeNavigate(
|
||||
MessageBackupsFlowFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
|
||||
DonationProcessorAction.CANCEL_SUBSCRIPTION,
|
||||
null,
|
||||
InAppPaymentType.RECURRING_BACKUP
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun navigateToStripePaymentInProgress(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(
|
||||
MessageBackupsFlowFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
|
||||
@@ -195,7 +236,11 @@ class MessageBackupsFlowFragment : ComposeFragment(), DonationCheckoutDelegate.C
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSubscriptionCancelled(inAppPaymentType: InAppPaymentType) = error("This view doesn't support cancellation, that is done elsewhere.")
|
||||
override fun onSubscriptionCancelled(inAppPaymentType: InAppPaymentType) {
|
||||
if (!findNavController().popBackStack()) {
|
||||
requireActivity().finishAfterTransition()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProcessorActionProcessed() = Unit
|
||||
|
||||
|
||||
@@ -7,10 +7,12 @@ package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import android.text.TextUtils
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.donations.InAppPaymentType
|
||||
@@ -32,7 +34,7 @@ import org.whispersystems.signalservice.api.kbs.PinHashUtil.verifyLocalPinHash
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
|
||||
class MessageBackupsFlowViewModel : ViewModel() {
|
||||
private val internalState = mutableStateOf(
|
||||
private val internalStateFlow = MutableStateFlow(
|
||||
MessageBackupsFlowState(
|
||||
availableBackupTypes = emptyList(),
|
||||
selectedMessageBackupTier = SignalStore.backup.backupTier,
|
||||
@@ -41,70 +43,90 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||
)
|
||||
)
|
||||
|
||||
val state: State<MessageBackupsFlowState> = internalState
|
||||
val stateFlow: StateFlow<MessageBackupsFlowState> = internalStateFlow
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
internalState.value = internalState.value.copy(
|
||||
availableBackupTypes = BackupRepository.getAvailableBackupsTypes(
|
||||
if (!RemoteConfig.messageBackups) emptyList() else listOf(MessageBackupTier.FREE, MessageBackupTier.PAID)
|
||||
internalStateFlow.update {
|
||||
it.copy(
|
||||
availableBackupTypes = BackupRepository.getAvailableBackupsTypes(
|
||||
if (!RemoteConfig.messageBackups) emptyList() else listOf(MessageBackupTier.FREE, MessageBackupTier.PAID)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun goToNextScreen() {
|
||||
val nextScreen = when (internalState.value.screen) {
|
||||
MessageBackupsScreen.EDUCATION -> MessageBackupsScreen.PIN_EDUCATION
|
||||
MessageBackupsScreen.PIN_EDUCATION -> MessageBackupsScreen.PIN_CONFIRMATION
|
||||
MessageBackupsScreen.PIN_CONFIRMATION -> validatePinAndUpdateState()
|
||||
MessageBackupsScreen.TYPE_SELECTION -> validateTypeAndUpdateState()
|
||||
MessageBackupsScreen.CHECKOUT_SHEET -> validateGatewayAndUpdateState()
|
||||
MessageBackupsScreen.PROCESS_PAYMENT -> MessageBackupsScreen.COMPLETED
|
||||
MessageBackupsScreen.COMPLETED -> error("Unsupported state transition from terminal state COMPLETED")
|
||||
}
|
||||
internalStateFlow.update {
|
||||
val nextScreen = when (it.screen) {
|
||||
MessageBackupsScreen.EDUCATION -> MessageBackupsScreen.PIN_EDUCATION
|
||||
MessageBackupsScreen.PIN_EDUCATION -> MessageBackupsScreen.PIN_CONFIRMATION
|
||||
MessageBackupsScreen.PIN_CONFIRMATION -> validatePinAndUpdateState(it.pin)
|
||||
MessageBackupsScreen.TYPE_SELECTION -> validateTypeAndUpdateState(it.selectedMessageBackupTier!!)
|
||||
MessageBackupsScreen.CHECKOUT_SHEET -> validateGatewayAndUpdateState(it)
|
||||
MessageBackupsScreen.CREATING_IN_APP_PAYMENT -> error("This is driven by an async coroutine.")
|
||||
MessageBackupsScreen.CANCELLATION_DIALOG -> MessageBackupsScreen.PROCESS_CANCELLATION
|
||||
MessageBackupsScreen.PROCESS_PAYMENT -> MessageBackupsScreen.COMPLETED
|
||||
MessageBackupsScreen.PROCESS_CANCELLATION -> MessageBackupsScreen.COMPLETED
|
||||
MessageBackupsScreen.COMPLETED -> error("Unsupported state transition from terminal state COMPLETED")
|
||||
}
|
||||
|
||||
internalState.value = state.value.copy(screen = nextScreen)
|
||||
it.copy(screen = nextScreen)
|
||||
}
|
||||
}
|
||||
|
||||
fun goToPreviousScreen() {
|
||||
if (internalState.value.screen == internalState.value.startScreen) {
|
||||
internalState.value = state.value.copy(screen = MessageBackupsScreen.COMPLETED)
|
||||
return
|
||||
}
|
||||
internalStateFlow.update {
|
||||
if (it.screen == it.startScreen) {
|
||||
it.copy(screen = MessageBackupsScreen.COMPLETED)
|
||||
} else {
|
||||
val previousScreen = when (it.screen) {
|
||||
MessageBackupsScreen.EDUCATION -> MessageBackupsScreen.COMPLETED
|
||||
MessageBackupsScreen.PIN_EDUCATION -> MessageBackupsScreen.EDUCATION
|
||||
MessageBackupsScreen.PIN_CONFIRMATION -> MessageBackupsScreen.PIN_EDUCATION
|
||||
MessageBackupsScreen.TYPE_SELECTION -> MessageBackupsScreen.PIN_CONFIRMATION
|
||||
MessageBackupsScreen.CHECKOUT_SHEET -> MessageBackupsScreen.TYPE_SELECTION
|
||||
MessageBackupsScreen.CREATING_IN_APP_PAYMENT -> MessageBackupsScreen.TYPE_SELECTION
|
||||
MessageBackupsScreen.PROCESS_PAYMENT -> MessageBackupsScreen.TYPE_SELECTION
|
||||
MessageBackupsScreen.PROCESS_CANCELLATION -> MessageBackupsScreen.TYPE_SELECTION
|
||||
MessageBackupsScreen.CANCELLATION_DIALOG -> MessageBackupsScreen.TYPE_SELECTION
|
||||
MessageBackupsScreen.COMPLETED -> error("Unsupported state transition from terminal state COMPLETED")
|
||||
}
|
||||
|
||||
val previousScreen = when (internalState.value.screen) {
|
||||
MessageBackupsScreen.EDUCATION -> MessageBackupsScreen.COMPLETED
|
||||
MessageBackupsScreen.PIN_EDUCATION -> MessageBackupsScreen.EDUCATION
|
||||
MessageBackupsScreen.PIN_CONFIRMATION -> MessageBackupsScreen.PIN_EDUCATION
|
||||
MessageBackupsScreen.TYPE_SELECTION -> MessageBackupsScreen.PIN_CONFIRMATION
|
||||
MessageBackupsScreen.CHECKOUT_SHEET -> MessageBackupsScreen.TYPE_SELECTION
|
||||
MessageBackupsScreen.PROCESS_PAYMENT -> MessageBackupsScreen.TYPE_SELECTION
|
||||
MessageBackupsScreen.COMPLETED -> error("Unsupported state transition from terminal state COMPLETED")
|
||||
it.copy(screen = previousScreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internalState.value = state.value.copy(screen = previousScreen)
|
||||
fun displayCancellationDialog() {
|
||||
internalStateFlow.update {
|
||||
check(it.screen == MessageBackupsScreen.TYPE_SELECTION)
|
||||
it.copy(screen = MessageBackupsScreen.CANCELLATION_DIALOG)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPinEntryUpdated(pin: String) {
|
||||
internalState.value = state.value.copy(pin = pin)
|
||||
// TODO [alex] -- shouldn't store this in a flow
|
||||
internalStateFlow.update {
|
||||
it.copy(pin = pin)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPinKeyboardTypeUpdated(pinKeyboardType: PinKeyboardType) {
|
||||
internalState.value = state.value.copy(pinKeyboardType = pinKeyboardType)
|
||||
internalStateFlow.update { it.copy(pinKeyboardType = pinKeyboardType) }
|
||||
}
|
||||
|
||||
fun onPaymentMethodUpdated(paymentMethod: InAppPaymentData.PaymentMethodType) {
|
||||
internalState.value = state.value.copy(selectedPaymentMethod = paymentMethod)
|
||||
internalStateFlow.update { it.copy(selectedPaymentMethod = paymentMethod) }
|
||||
}
|
||||
|
||||
fun onMessageBackupTierUpdated(messageBackupTier: MessageBackupTier) {
|
||||
internalState.value = state.value.copy(selectedMessageBackupTier = messageBackupTier)
|
||||
internalStateFlow.update { it.copy(selectedMessageBackupTier = messageBackupTier) }
|
||||
}
|
||||
|
||||
private fun validatePinAndUpdateState(): MessageBackupsScreen {
|
||||
private fun validatePinAndUpdateState(pin: String): MessageBackupsScreen {
|
||||
val pinHash = SignalStore.svr.localPinHash
|
||||
val pin = state.value.pin
|
||||
|
||||
if (pinHash == null || TextUtils.isEmpty(pin) || pin.length < SvrConstants.MINIMUM_PIN_LENGTH) return MessageBackupsScreen.PIN_CONFIRMATION
|
||||
|
||||
@@ -114,25 +136,26 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||
return MessageBackupsScreen.TYPE_SELECTION
|
||||
}
|
||||
|
||||
private fun validateTypeAndUpdateState(): MessageBackupsScreen {
|
||||
private fun validateTypeAndUpdateState(tier: MessageBackupTier): MessageBackupsScreen {
|
||||
SignalStore.backup.areBackupsEnabled = true
|
||||
SignalStore.backup.backupTier = state.value.selectedMessageBackupTier!!
|
||||
SignalStore.backup.backupTier = tier
|
||||
|
||||
// TODO [message-backups] - Does anything need to be kicked off?
|
||||
|
||||
return when (state.value.selectedMessageBackupTier!!) {
|
||||
return when (tier) {
|
||||
MessageBackupTier.FREE -> MessageBackupsScreen.COMPLETED
|
||||
MessageBackupTier.PAID -> MessageBackupsScreen.CHECKOUT_SHEET
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateGatewayAndUpdateState(): MessageBackupsScreen {
|
||||
val stateSnapshot = state.value
|
||||
val backupsType = stateSnapshot.availableBackupTypes.first { it.tier == stateSnapshot.selectedMessageBackupTier }
|
||||
|
||||
internalState.value = state.value.copy(inAppPayment = null)
|
||||
private fun validateGatewayAndUpdateState(state: MessageBackupsFlowState): MessageBackupsScreen {
|
||||
val backupsType = state.availableBackupTypes.first { it.tier == state.selectedMessageBackupTier }
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.Main) {
|
||||
internalStateFlow.update { it.copy(inAppPayment = null) }
|
||||
}
|
||||
|
||||
SignalDatabase.inAppPayments.clearCreated()
|
||||
val id = SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.RECURRING_BACKUP,
|
||||
@@ -145,7 +168,7 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||
amount = backupsType.pricePerMonth.toFiatValue(),
|
||||
level = SubscriptionsConfiguration.BACKUPS_LEVEL.toLong(),
|
||||
recipientId = Recipient.self().id.serialize(),
|
||||
paymentMethodType = stateSnapshot.selectedPaymentMethod!!,
|
||||
paymentMethodType = state.selectedPaymentMethod!!,
|
||||
redemption = InAppPaymentData.RedemptionState(
|
||||
stage = InAppPaymentData.RedemptionState.Stage.INIT
|
||||
)
|
||||
@@ -155,10 +178,10 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(id)!!
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
internalState.value = state.value.copy(inAppPayment = inAppPayment)
|
||||
internalStateFlow.update { it.copy(inAppPayment = inAppPayment, screen = MessageBackupsScreen.PROCESS_PAYMENT) }
|
||||
}
|
||||
}
|
||||
|
||||
return MessageBackupsScreen.PROCESS_PAYMENT
|
||||
return MessageBackupsScreen.CREATING_IN_APP_PAYMENT
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,11 @@ enum class MessageBackupsScreen {
|
||||
PIN_EDUCATION,
|
||||
PIN_CONFIRMATION,
|
||||
TYPE_SELECTION,
|
||||
CANCELLATION_DIALOG,
|
||||
CHECKOUT_SHEET,
|
||||
CREATING_IN_APP_PAYMENT,
|
||||
PROCESS_PAYMENT,
|
||||
PROCESS_CANCELLATION,
|
||||
COMPLETED;
|
||||
|
||||
fun isAfter(other: MessageBackupsScreen): Boolean = ordinal > other.ordinal
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -18,8 +19,10 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -32,6 +35,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.ExperimentalTextApi
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
@@ -39,6 +43,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withAnnotation
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.Scaffolds
|
||||
@@ -49,6 +54,7 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
/**
|
||||
* Screen which allows the user to select their preferred backup type.
|
||||
@@ -56,12 +62,14 @@ import java.math.BigDecimal
|
||||
@OptIn(ExperimentalTextApi::class)
|
||||
@Composable
|
||||
fun MessageBackupsTypeSelectionScreen(
|
||||
currentBackupTier: MessageBackupTier?,
|
||||
selectedBackupTier: MessageBackupTier?,
|
||||
availableBackupTypes: List<MessageBackupsType>,
|
||||
onMessageBackupsTierSelected: (MessageBackupTier) -> Unit,
|
||||
onNavigationClick: () -> Unit,
|
||||
onReadMoreClicked: () -> Unit,
|
||||
onNextClicked: () -> Unit
|
||||
onNextClicked: () -> Unit,
|
||||
onCancelSubscriptionClicked: () -> Unit
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = "",
|
||||
@@ -130,6 +138,7 @@ fun MessageBackupsTypeSelectionScreen(
|
||||
) { index, item ->
|
||||
MessageBackupsTypeBlock(
|
||||
messageBackupsType = item,
|
||||
isCurrent = item.tier == currentBackupTier,
|
||||
isSelected = item.tier == selectedBackupTier,
|
||||
onSelected = { onMessageBackupsTierSelected(item.tier) },
|
||||
modifier = Modifier.padding(top = if (index == 0) 20.dp else 18.dp)
|
||||
@@ -137,17 +146,36 @@ fun MessageBackupsTypeSelectionScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val hasSelectedBackupTier = currentBackupTier != null
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = onNextClicked,
|
||||
enabled = selectedBackupTier != null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp)
|
||||
.padding(vertical = if (hasSelectedBackupTier) 10.dp else 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Next" // TODO [message-backups] Finalized copy
|
||||
text = stringResource(
|
||||
id = if (currentBackupTier == null) {
|
||||
R.string.MessageBackupsTypeSelectionScreen__next
|
||||
} else {
|
||||
R.string.MessageBackupsTypeSelectionScreen__change_backup_type
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (hasSelectedBackupTier) {
|
||||
TextButton(
|
||||
onClick = onCancelSubscriptionClicked,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 14.dp)
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.MessageBackupsTypeSelectionScreen__cancel_subscription))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,11 +188,32 @@ private fun MessageBackupsTypeSelectionScreenPreview() {
|
||||
Previews.Preview {
|
||||
MessageBackupsTypeSelectionScreen(
|
||||
selectedBackupTier = MessageBackupTier.FREE,
|
||||
availableBackupTypes = emptyList(),
|
||||
availableBackupTypes = testBackupTypes(),
|
||||
onMessageBackupsTierSelected = { selectedBackupsType = it },
|
||||
onNavigationClick = {},
|
||||
onReadMoreClicked = {},
|
||||
onNextClicked = {}
|
||||
onNextClicked = {},
|
||||
onCancelSubscriptionClicked = {},
|
||||
currentBackupTier = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun MessageBackupsTypeSelectionScreenWithCurrentTierPreview() {
|
||||
var selectedBackupsType by remember { mutableStateOf(MessageBackupTier.FREE) }
|
||||
|
||||
Previews.Preview {
|
||||
MessageBackupsTypeSelectionScreen(
|
||||
selectedBackupTier = MessageBackupTier.FREE,
|
||||
availableBackupTypes = testBackupTypes(),
|
||||
onMessageBackupsTierSelected = { selectedBackupsType = it },
|
||||
onNavigationClick = {},
|
||||
onReadMoreClicked = {},
|
||||
onNextClicked = {},
|
||||
onCancelSubscriptionClicked = {},
|
||||
currentBackupTier = MessageBackupTier.PAID
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -172,6 +221,7 @@ private fun MessageBackupsTypeSelectionScreenPreview() {
|
||||
@Composable
|
||||
fun MessageBackupsTypeBlock(
|
||||
messageBackupsType: MessageBackupsType,
|
||||
isCurrent: Boolean,
|
||||
isSelected: Boolean,
|
||||
onSelected: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -189,7 +239,7 @@ fun MessageBackupsTypeBlock(
|
||||
SignalTheme.colors.colorSurface2
|
||||
}
|
||||
|
||||
Column(
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = background, shape = RoundedCornerShape(18.dp))
|
||||
@@ -198,26 +248,36 @@ fun MessageBackupsTypeBlock(
|
||||
.clickable(onClick = onSelected, enabled = enabled)
|
||||
.padding(vertical = 16.dp, horizontal = 20.dp)
|
||||
) {
|
||||
Text(
|
||||
text = formatCostPerMonth(messageBackupsType.pricePerMonth),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = formatCostPerMonth(messageBackupsType.pricePerMonth),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
|
||||
Text(
|
||||
text = messageBackupsType.title,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = messageBackupsType.title,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = spacedBy(4.dp),
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
messageBackupsType.features.forEach {
|
||||
MessageBackupsTypeFeatureRow(messageBackupsTypeFeature = it)
|
||||
Column(
|
||||
verticalArrangement = spacedBy(4.dp),
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
messageBackupsType.features.forEach {
|
||||
MessageBackupsTypeFeatureRow(messageBackupsTypeFeature = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isCurrent) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_check_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,3 +289,46 @@ private fun formatCostPerMonth(pricePerMonth: FiatMoney): String {
|
||||
"${FiatMoneyUtil.format(LocalContext.current.resources, pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())}/month"
|
||||
}
|
||||
}
|
||||
|
||||
private fun testBackupTypes(): List<MessageBackupsType> {
|
||||
return listOf(
|
||||
MessageBackupsType(
|
||||
tier = MessageBackupTier.FREE,
|
||||
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
|
||||
title = "Text + 30 days of media",
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Full text message backup"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_album_compact_bold_16,
|
||||
label = "Last 30 days of media"
|
||||
)
|
||||
)
|
||||
),
|
||||
MessageBackupsType(
|
||||
tier = MessageBackupTier.PAID,
|
||||
pricePerMonth = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")),
|
||||
title = "Text + All your media",
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Full text message backup"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_album_compact_bold_16,
|
||||
label = "Full media backup"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "1TB of storage (~250K photos)"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
|
||||
label = "Thanks for supporting Signal!"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user