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

View File

@@ -7130,5 +7130,23 @@
<!-- The body of an alert dialog shown when we detect the user may be confused by the lock screen -->
<string name="PassphrasePromptActivity_help_prompt_body">Please enter your device pin, password or pattern.</string>
<!-- Primary action button label when selecting a backup tier without a current selection -->
<string name="MessageBackupsTypeSelectionScreen__next">Next</string>
<!-- Primary action button label when selecting a backup tier with a current selection -->
<string name="MessageBackupsTypeSelectionScreen__change_backup_type">Change backup type</string>
<!-- Secondary action button label when selecting a backup tier with a current selection -->
<string name="MessageBackupsTypeSelectionScreen__cancel_subscription">Cancel subscription</string>
<!-- ConfirmBackupCancellationDialog -->
<!-- Dialog title -->
<string name="ConfirmBackupCancellationDialog__confirm_cancellation">Confirm cancellation</string>
<!-- Dialog body -->
<string name="ConfirmBackupCancellationDialog__you_wont_be_charged_again">You won\'t be charged again. Backups will be turned off at the end of your billing cycle. After that date you will have 30 days to download the media you\'re currently backing up.</string>
<!-- Action button label to cancel subscription and download now -->
<string name="ConfirmBackupCancellationDialog__confirm_and_download_now">Confirm and download now</string>
<!-- Action button label to cancel subscription and download later -->
<string name="ConfirmBackupCancellationDialog__confirm_and_download_later">Confirm and download later</string>
<!-- Action button label to keep current subscription -->
<string name="ConfirmBackupCancellationDialog__keep_subscription">Keep subscription</string>
<!-- EOF -->
</resources>