mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 18:30:20 +01:00
Add scaffolding for backupsV2.
This commit is contained in:
committed by
Nicholas Tinsley
parent
91920319c7
commit
1234c63836
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
||||
import org.thoughtcrime.securesms.databinding.PaypalButtonBinding
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MessageBackupsCheckoutSheet(
|
||||
messageBackupsType: MessageBackupsType,
|
||||
availablePaymentGateways: List<GatewayResponse.Gateway>,
|
||||
onDismissRequest: () -> Unit,
|
||||
onPaymentGatewaySelected: (GatewayResponse.Gateway) -> Unit
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dragHandle = { BottomSheets.Handle() },
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
) {
|
||||
SheetContent(
|
||||
messageBackupsType = messageBackupsType,
|
||||
availablePaymentGateways = availablePaymentGateways,
|
||||
onPaymentGatewaySelected = onPaymentGatewaySelected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SheetContent(
|
||||
messageBackupsType: MessageBackupsType,
|
||||
availablePaymentGateways: List<GatewayResponse.Gateway>,
|
||||
onPaymentGatewaySelected: (GatewayResponse.Gateway) -> Unit
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
val formattedPrice = remember(messageBackupsType.pricePerMonth) {
|
||||
FiatMoneyUtil.format(resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Pay $formattedPrice/month to Signal", // TODO [message-backups] Finalized copy
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(top = 48.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "You'll get:", // TODO [message-backups] Finalized copy
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 5.dp)
|
||||
)
|
||||
|
||||
MessageBackupsTypeBlock(
|
||||
messageBackupsType = messageBackupsType,
|
||||
isSelected = false,
|
||||
onSelected = {},
|
||||
enabled = false,
|
||||
modifier = Modifier.padding(top = 24.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = spacedBy(12.dp),
|
||||
modifier = Modifier.padding(top = 48.dp, bottom = 24.dp)
|
||||
) {
|
||||
availablePaymentGateways.forEach {
|
||||
when (it) {
|
||||
GatewayResponse.Gateway.GOOGLE_PAY -> GooglePayButton {
|
||||
onPaymentGatewaySelected(GatewayResponse.Gateway.GOOGLE_PAY)
|
||||
}
|
||||
|
||||
GatewayResponse.Gateway.PAYPAL -> PayPalButton {
|
||||
onPaymentGatewaySelected(GatewayResponse.Gateway.PAYPAL)
|
||||
}
|
||||
|
||||
GatewayResponse.Gateway.CREDIT_CARD -> CreditOrDebitCardButton {
|
||||
onPaymentGatewaySelected(GatewayResponse.Gateway.CREDIT_CARD)
|
||||
}
|
||||
|
||||
GatewayResponse.Gateway.SEPA_DEBIT -> SepaButton {
|
||||
onPaymentGatewaySelected(GatewayResponse.Gateway.SEPA_DEBIT)
|
||||
}
|
||||
|
||||
GatewayResponse.Gateway.IDEAL -> IdealButton {
|
||||
onPaymentGatewaySelected(GatewayResponse.Gateway.IDEAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PayPalButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
AndroidView(factory = {
|
||||
val view = LayoutInflater.from(it).inflate(R.layout.paypal_button, null)
|
||||
view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
view
|
||||
}) {
|
||||
val binding = PaypalButtonBinding.bind(it)
|
||||
binding.paypalButton.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
marginStart = 0
|
||||
marginEnd = 0
|
||||
}
|
||||
|
||||
binding.paypalButton.setOnClickListener {
|
||||
onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GooglePayButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val model = GooglePayButton.Model(onClick, true)
|
||||
|
||||
AndroidView(factory = {
|
||||
LayoutInflater.from(it).inflate(R.layout.google_pay_button_pref, null)
|
||||
}) {
|
||||
val holder = GooglePayButton.ViewHolder(it)
|
||||
holder.bind(model)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SepaButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.bank_transfer),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
|
||||
Text(text = stringResource(id = R.string.GatewaySelectorBottomSheet__bank_transfer))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IdealButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.logo_ideal),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.padding(end = 8.dp)
|
||||
)
|
||||
|
||||
Text(text = stringResource(id = R.string.GatewaySelectorBottomSheet__ideal))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreditOrDebitCardButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Buttons.LargePrimary(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.credit_card),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.GatewaySelectorBottomSheet__credit_or_debit_card)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun MessageBackupsCheckoutSheetPreview() {
|
||||
val paidTier = MessageBackupsType(
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), 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!"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val availablePaymentGateways = GatewayResponse.Gateway.values().toList()
|
||||
|
||||
Previews.Preview {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
) {
|
||||
SheetContent(
|
||||
messageBackupsType = paidTier,
|
||||
availablePaymentGateways = availablePaymentGateways,
|
||||
onPaymentGatewaySelected = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Educational content which allows user to proceed to set up automatic backups
|
||||
* or navigate to a support page to learn more.
|
||||
*/
|
||||
@Composable
|
||||
fun MessageBackupsEducationScreen(
|
||||
onNavigationClick: () -> Unit,
|
||||
onEnableBackups: () -> Unit,
|
||||
onLearnMore: () -> Unit
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
onNavigationClick = onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_x_24),
|
||||
title = "Chat backups" // TODO [message-backups] Finalized copy
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it)
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
) {
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
item {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_signal_logo_large), // TODO [message-backups] Final image asset
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(top = 48.dp)
|
||||
.size(88.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "Chat Backups", // TODO [message-backups] Finalized copy
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(top = 15.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "Back up your messages and media and using Signal’s secure, end-to-end encrypted storage service. Never lose a message when you get a new phone or reinstall Signal.", // TODO [message-backups] Finalized copy
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier.padding(top = 32.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
) {
|
||||
NotableFeatureRow(
|
||||
painter = painterResource(id = R.drawable.symbol_lock_compact_20),
|
||||
text = "End-to-end Encrypted" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
|
||||
NotableFeatureRow(
|
||||
painter = painterResource(id = R.drawable.symbol_check_square_compact_20),
|
||||
text = "Optional, always" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
|
||||
NotableFeatureRow(
|
||||
painter = painterResource(id = R.drawable.symbol_trash_compact_20),
|
||||
text = "Delete your backup anytime" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = onEnableBackups,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "Enable backups" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onLearnMore,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Learn more" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun MessageBackupsEducationSheetPreview() {
|
||||
Previews.Preview {
|
||||
MessageBackupsEducationScreen(
|
||||
onNavigationClick = {},
|
||||
onEnableBackups = {},
|
||||
onLearnMore = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun NotableFeatureRowPreview() {
|
||||
Previews.Preview {
|
||||
NotableFeatureRow(
|
||||
painter = painterResource(id = R.drawable.symbol_lock_compact_20),
|
||||
text = "Notable feature information"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotableFeatureRow(
|
||||
painter: Painter,
|
||||
text: String
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painter,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.size(32.dp)
|
||||
.background(color = SignalTheme.colors.colorSurface2, shape = CircleShape)
|
||||
.padding(6.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.dialog
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
class MessageBackupsFlowActivity : PassphraseRequiredActivity() {
|
||||
|
||||
private val viewModel: MessageBackupsFlowViewModel by viewModel { MessageBackupsFlowViewModel() }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
setContent {
|
||||
SignalTheme {
|
||||
val state by viewModel.state
|
||||
val navController = rememberNavController()
|
||||
|
||||
fun MessageBackupsScreen.next() {
|
||||
val nextScreen = viewModel.goToNextScreen(this)
|
||||
if (nextScreen != this) {
|
||||
navController.navigate(nextScreen.name)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavController.popOrFinish() {
|
||||
if (popBackStack()) {
|
||||
return
|
||||
}
|
||||
|
||||
finishAfterTransition()
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
navController.setLifecycleOwner(this@MessageBackupsFlowActivity)
|
||||
navController.setOnBackPressedDispatcher(this@MessageBackupsFlowActivity.onBackPressedDispatcher)
|
||||
navController.enableOnBackPressed(true)
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = MessageBackupsScreen.EDUCATION.name,
|
||||
enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
|
||||
exitTransition = { slideOutHorizontally(targetOffsetX = { -it }) },
|
||||
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
|
||||
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) }
|
||||
) {
|
||||
composable(route = MessageBackupsScreen.EDUCATION.name) {
|
||||
MessageBackupsEducationScreen(
|
||||
onNavigationClick = navController::popOrFinish,
|
||||
onEnableBackups = { MessageBackupsScreen.EDUCATION.next() },
|
||||
onLearnMore = {}
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = MessageBackupsScreen.PIN_EDUCATION.name) {
|
||||
MessageBackupsPinEducationScreen(
|
||||
onNavigationClick = navController::popOrFinish,
|
||||
onGeneratePinClick = {},
|
||||
onUseCurrentPinClick = { MessageBackupsScreen.PIN_EDUCATION.next() },
|
||||
recommendedPinSize = 16 // TODO [message-backups] This value should come from some kind of config
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = MessageBackupsScreen.PIN_CONFIRMATION.name) {
|
||||
MessageBackupsPinConfirmationScreen(
|
||||
pin = state.pin,
|
||||
onPinChanged = viewModel::onPinEntryUpdated,
|
||||
pinKeyboardType = state.pinKeyboardType,
|
||||
onPinKeyboardTypeSelected = viewModel::onPinKeyboardTypeUpdated,
|
||||
onNextClick = { MessageBackupsScreen.PIN_CONFIRMATION.next() }
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = MessageBackupsScreen.TYPE_SELECTION.name) {
|
||||
MessageBackupsTypeSelectionScreen(
|
||||
selectedBackupsType = state.selectedMessageBackupsType,
|
||||
availableBackupsTypes = state.availableBackupsTypes,
|
||||
onMessageBackupsTypeSelected = viewModel::onMessageBackupsTypeUpdated,
|
||||
onNavigationClick = navController::popOrFinish,
|
||||
onReadMoreClicked = {},
|
||||
onNextClicked = { MessageBackupsScreen.TYPE_SELECTION.next() }
|
||||
)
|
||||
}
|
||||
|
||||
dialog(route = MessageBackupsScreen.CHECKOUT_SHEET.name) {
|
||||
MessageBackupsCheckoutSheet(
|
||||
messageBackupsType = state.selectedMessageBackupsType!!,
|
||||
availablePaymentGateways = state.availablePaymentGateways,
|
||||
onDismissRequest = navController::popOrFinish,
|
||||
onPaymentGatewaySelected = {
|
||||
viewModel.onPaymentGatewayUpdated(it)
|
||||
MessageBackupsScreen.CHECKOUT_SHEET.next()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
class MessageBackupsFlowRepository
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
|
||||
data class MessageBackupsFlowState(
|
||||
val selectedMessageBackupsType: MessageBackupsType? = null,
|
||||
val availableBackupsTypes: List<MessageBackupsType> = emptyList(),
|
||||
val selectedPaymentGateway: GatewayResponse.Gateway? = null,
|
||||
val availablePaymentGateways: List<GatewayResponse.Gateway> = emptyList(),
|
||||
val pin: String = "",
|
||||
val pinKeyboardType: PinKeyboardType = SignalStore.pinValues().keyboardType
|
||||
)
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
|
||||
class MessageBackupsFlowViewModel : ViewModel() {
|
||||
private val internalState = mutableStateOf(MessageBackupsFlowState())
|
||||
|
||||
val state: State<MessageBackupsFlowState> = internalState
|
||||
|
||||
fun goToNextScreen(currentScreen: MessageBackupsScreen): MessageBackupsScreen {
|
||||
return when (currentScreen) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
fun onPinEntryUpdated(pin: String) {
|
||||
internalState.value = state.value.copy(pin = pin)
|
||||
}
|
||||
|
||||
fun onPinKeyboardTypeUpdated(pinKeyboardType: PinKeyboardType) {
|
||||
internalState.value = state.value.copy(pinKeyboardType = pinKeyboardType)
|
||||
}
|
||||
|
||||
fun onPaymentGatewayUpdated(gateway: GatewayResponse.Gateway) {
|
||||
internalState.value = state.value.copy(selectedPaymentGateway = gateway)
|
||||
}
|
||||
|
||||
fun onMessageBackupsTypeUpdated(messageBackupsType: MessageBackupsType) {
|
||||
internalState.value = state.value.copy(selectedMessageBackupsType = messageBackupsType)
|
||||
}
|
||||
|
||||
private fun validatePinAndUpdateState(): MessageBackupsScreen {
|
||||
return MessageBackupsScreen.TYPE_SELECTION
|
||||
}
|
||||
|
||||
private fun validateTypeAndUpdateState(): MessageBackupsScreen {
|
||||
return MessageBackupsScreen.CHECKOUT_SHEET
|
||||
}
|
||||
|
||||
private fun validateGatewayAndUpdateState(): MessageBackupsScreen {
|
||||
return MessageBackupsScreen.PROCESS_PAYMENT
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
|
||||
/**
|
||||
* Screen which requires the user to enter their pin before enabling backups.
|
||||
*/
|
||||
@Composable
|
||||
fun MessageBackupsPinConfirmationScreen(
|
||||
pin: String,
|
||||
onPinChanged: (String) -> Unit,
|
||||
pinKeyboardType: PinKeyboardType,
|
||||
onPinKeyboardTypeSelected: (PinKeyboardType) -> Unit,
|
||||
onNextClick: () -> Unit
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = "Enter your PIN", // TODO [message-backups] Finalized copy
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(top = 40.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "Enter your Signal PIN to enable backups", // TODO [message-backups] Finalized copy
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
// TODO [message-backups] Confirm default focus state
|
||||
val keyboardType = remember(pinKeyboardType) {
|
||||
when (pinKeyboardType) {
|
||||
PinKeyboardType.NUMERIC -> KeyboardType.NumberPassword
|
||||
PinKeyboardType.ALPHA_NUMERIC -> KeyboardType.Password
|
||||
}
|
||||
}
|
||||
|
||||
TextField(
|
||||
value = pin,
|
||||
onValueChange = onPinChanged,
|
||||
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { onNextClick() }
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = keyboardType
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(top = 72.dp)
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 48.dp)
|
||||
) {
|
||||
PinKeyboardTypeToggle(
|
||||
pinKeyboardType = pinKeyboardType,
|
||||
onPinKeyboardTypeSelected = onPinKeyboardTypeSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.BottomEnd,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp)
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = onNextClick
|
||||
) {
|
||||
Text(
|
||||
text = "Next" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun MessageBackupsPinConfirmationScreenPreview() {
|
||||
Previews.Preview {
|
||||
MessageBackupsPinConfirmationScreen(
|
||||
pin = "",
|
||||
onPinChanged = {},
|
||||
pinKeyboardType = PinKeyboardType.ALPHA_NUMERIC,
|
||||
onPinKeyboardTypeSelected = {},
|
||||
onNextClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PinKeyboardTypeTogglePreview() {
|
||||
Previews.Preview {
|
||||
var type by remember { mutableStateOf(PinKeyboardType.ALPHA_NUMERIC) }
|
||||
PinKeyboardTypeToggle(
|
||||
pinKeyboardType = type,
|
||||
onPinKeyboardTypeSelected = { type = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PinKeyboardTypeToggle(
|
||||
pinKeyboardType: PinKeyboardType,
|
||||
onPinKeyboardTypeSelected: (PinKeyboardType) -> Unit
|
||||
) {
|
||||
val callback = remember(pinKeyboardType) {
|
||||
{ onPinKeyboardTypeSelected(pinKeyboardType.other) }
|
||||
}
|
||||
|
||||
val iconRes = remember(pinKeyboardType) {
|
||||
when (pinKeyboardType) {
|
||||
PinKeyboardType.NUMERIC -> R.drawable.symbol_keyboard_24
|
||||
PinKeyboardType.ALPHA_NUMERIC -> R.drawable.symbol_number_pad_24
|
||||
}
|
||||
}
|
||||
|
||||
TextButton(onClick = callback) {
|
||||
Icon(
|
||||
painter = painterResource(id = iconRes),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Switch keyboard" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Explanation screen that details how the user's pin is utilized with backups,
|
||||
* and how long they should make their pin.
|
||||
*/
|
||||
@Composable
|
||||
fun MessageBackupsPinEducationScreen(
|
||||
onNavigationClick: () -> Unit,
|
||||
onGeneratePinClick: () -> Unit,
|
||||
onUseCurrentPinClick: () -> Unit,
|
||||
recommendedPinSize: Int
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = "Backup type", // TODO [message-backups] Finalized copy
|
||||
onNavigationClick = onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it)
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
) {
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
item {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_signal_logo_large), // TODO [message-backups] Finalized image
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(top = 48.dp)
|
||||
.size(88.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "PINs protect your backup", // TODO [message-backups] Finalized copy
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "Your Signal PIN lets you restore your backup when you re-install Signal. For increased security, we recommend updating to a new $recommendedPinSize-digit PIN.", // TODO [message-backups] Finalized copy
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "If you forget your PIN, you will not be able to restore your backup. You can change your PIN at any time in settings.", // TODO [message-backups] Finalized copy
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = onGeneratePinClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "Generate a new $recommendedPinSize-digit PIN" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onUseCurrentPinClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Use current Signal PIN" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun MessageBackupsPinScreenPreview() {
|
||||
Previews.Preview {
|
||||
MessageBackupsPinEducationScreen(
|
||||
onNavigationClick = {},
|
||||
onGeneratePinClick = {},
|
||||
onUseCurrentPinClick = {},
|
||||
recommendedPinSize = 16
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
enum class MessageBackupsScreen {
|
||||
EDUCATION,
|
||||
PIN_EDUCATION,
|
||||
PIN_CONFIRMATION,
|
||||
TYPE_SELECTION,
|
||||
CHECKOUT_SHEET,
|
||||
PROCESS_PAYMENT,
|
||||
COMPLETED
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
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.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
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.text.ExperimentalTextApi
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withAnnotation
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
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.
|
||||
*/
|
||||
@OptIn(ExperimentalTextApi::class)
|
||||
@Composable
|
||||
fun MessageBackupsTypeSelectionScreen(
|
||||
selectedBackupsType: MessageBackupsType?,
|
||||
availableBackupsTypes: List<MessageBackupsType>,
|
||||
onMessageBackupsTypeSelected: (MessageBackupsType) -> Unit,
|
||||
onNavigationClick: () -> Unit,
|
||||
onReadMoreClicked: () -> Unit,
|
||||
onNextClicked: () -> Unit
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = "",
|
||||
onNavigationClick = onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24)
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
.fillMaxSize()
|
||||
) {
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
item {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_signal_logo_large), // TODO [message-backups] Finalized art asset
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(88.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "Choose your backup type", // TODO [message-backups] Finalized copy
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
// TODO [message-backups] Finalized copy
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
val readMoreString = buildAnnotatedString {
|
||||
append("All backups are end-to-end encrypted. Signal is a non-profit—paying for backups helps support our mission. ")
|
||||
withAnnotation(tag = "URL", annotation = "read-more") {
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = primaryColor
|
||||
)
|
||||
) {
|
||||
append("Read more")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = readMoreString,
|
||||
style = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Center),
|
||||
onClick = { offset ->
|
||||
readMoreString
|
||||
.getStringAnnotations(tag = "URL", start = offset, end = offset)
|
||||
.firstOrNull()?.let { onReadMoreClicked() }
|
||||
},
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
availableBackupsTypes,
|
||||
{ _, item -> item.title }
|
||||
) { index, item ->
|
||||
MessageBackupsTypeBlock(
|
||||
messageBackupsType = item,
|
||||
isSelected = item == selectedBackupsType,
|
||||
onSelected = { onMessageBackupsTypeSelected(item) },
|
||||
modifier = Modifier.padding(top = if (index == 0) 20.dp else 18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = onNextClicked,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Next" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun MessageBackupsTypeSelectionScreenPreview() {
|
||||
val freeTier = MessageBackupsType(
|
||||
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"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val paidTier = MessageBackupsType(
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), 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!"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
var selectedBackupsType by remember { mutableStateOf(freeTier) }
|
||||
|
||||
Previews.Preview {
|
||||
MessageBackupsTypeSelectionScreen(
|
||||
selectedBackupsType = selectedBackupsType,
|
||||
availableBackupsTypes = listOf(freeTier, paidTier),
|
||||
onMessageBackupsTypeSelected = { selectedBackupsType = it },
|
||||
onNavigationClick = {},
|
||||
onReadMoreClicked = {},
|
||||
onNextClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageBackupsTypeBlock(
|
||||
messageBackupsType: MessageBackupsType,
|
||||
isSelected: Boolean,
|
||||
onSelected: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
val borderColor = if (isSelected) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
Color.Transparent
|
||||
}
|
||||
|
||||
val background = if (isSelected) {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
} else {
|
||||
SignalTheme.colors.colorSurface2
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = background, shape = RoundedCornerShape(18.dp))
|
||||
.border(width = 2.dp, color = borderColor, shape = RoundedCornerShape(18.dp))
|
||||
.clip(shape = RoundedCornerShape(18.dp))
|
||||
.clickable(onClick = onSelected, enabled = enabled)
|
||||
.padding(vertical = 16.dp, horizontal = 20.dp)
|
||||
) {
|
||||
Text(
|
||||
text = formatCostPerMonth(messageBackupsType.pricePerMonth),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun formatCostPerMonth(pricePerMonth: FiatMoney): String {
|
||||
return if (pricePerMonth.amount == BigDecimal.ZERO) {
|
||||
"Free"
|
||||
} else {
|
||||
"${FiatMoneyUtil.format(LocalContext.current.resources, pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())}/month"
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageBackupsTypeFeatureRow(messageBackupsTypeFeature: MessageBackupsTypeFeature) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = messageBackupsTypeFeature.iconResourceId),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = messageBackupsTypeFeature.label,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class MessageBackupsType(
|
||||
val pricePerMonth: FiatMoney,
|
||||
val title: String,
|
||||
val features: ImmutableList<MessageBackupsTypeFeature>
|
||||
)
|
||||
|
||||
data class MessageBackupsTypeFeature(
|
||||
val iconResourceId: Int,
|
||||
val label: String
|
||||
)
|
||||
@@ -196,7 +196,6 @@ import java.util.stream.Collectors;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
import static android.app.Activity.RESULT_CANCELED;
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,15 @@ class SavedStateViewModelFactory<MODEL : ViewModel>(
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
inline fun <reified VM : ViewModel> ComponentActivity.viewModel(
|
||||
noinline create: () -> VM
|
||||
): Lazy<VM> {
|
||||
return viewModels(
|
||||
factoryProducer = ViewModelFactory.factoryProducer(create)
|
||||
)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
inline fun <reified VM : ViewModel> Fragment.viewModel(
|
||||
noinline create: () -> VM
|
||||
|
||||
Reference in New Issue
Block a user