Implements a bunch of missing things in the backup checkout flow stuff.

This commit is contained in:
Alex Hart
2024-06-27 09:55:53 -03:00
committed by Cody Henthorne
parent 079a3d4fee
commit 8bbb7d56e0
7 changed files with 368 additions and 71 deletions

View File

@@ -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 = {}
)
}
}

View File

@@ -101,6 +101,7 @@ private fun SheetContent(
MessageBackupsTypeBlock(
messageBackupsType = messageBackupsType,
isCurrent = false,
isSelected = false,
onSelected = {},
enabled = false,

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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!"
)
)
)
)
}