mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 20:18:36 +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!"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user