Add biometric prompt to reveal backup key from settings and other fixes.

This commit is contained in:
Alex Hart 2024-10-03 10:06:04 -03:00 committed by Greyson Parrelli
parent 321c344e77
commit 8990088980
17 changed files with 188 additions and 337 deletions

View file

@ -8,8 +8,10 @@ package org.thoughtcrime.securesms.backup.v2.ui.subscription
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.content.IntentCompat
import androidx.fragment.app.Fragment
import org.signal.core.util.getParcelableExtraCompat
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.FragmentWrapperActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.CheckoutFlowActivity.Result
@ -20,15 +22,18 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.Ch
class MessageBackupsCheckoutActivity : FragmentWrapperActivity() {
companion object {
private const val TIER = "tier"
private const val RESULT_DATA = "result_data"
}
override fun getFragment(): Fragment = MessageBackupsFlowFragment()
override fun getFragment(): Fragment = MessageBackupsFlowFragment.create(
IntentCompat.getSerializableExtra(intent, TIER, MessageBackupTier::class.java)
)
class Contract : ActivityResultContract<Unit, Result?>() {
class Contract : ActivityResultContract<MessageBackupTier?, Result?>() {
override fun createIntent(context: Context, input: Unit): Intent {
return Intent(context, MessageBackupsCheckoutActivity::class.java)
override fun createIntent(context: Context, input: MessageBackupTier?): Intent {
return Intent(context, MessageBackupsCheckoutActivity::class.java).putExtra(TIER, input)
}
override fun parseResult(resultCode: Int, intent: Intent?): Result? {

View file

@ -14,10 +14,12 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.core.os.bundleOf
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.rx3.asFlowable
import org.signal.core.util.getSerializableCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentCheckoutDelegate
@ -33,7 +35,21 @@ import org.thoughtcrime.securesms.util.viewModel
*/
class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelegate.ErrorHandlerCallback {
private val viewModel: MessageBackupsFlowViewModel by viewModel { MessageBackupsFlowViewModel() }
companion object {
private const val TIER = "tier"
fun create(messageBackupTier: MessageBackupTier?): MessageBackupsFlowFragment {
return MessageBackupsFlowFragment().apply {
arguments = bundleOf(TIER to messageBackupTier)
}
}
}
private val viewModel: MessageBackupsFlowViewModel by viewModel {
MessageBackupsFlowViewModel(requireArguments().getSerializableCompat(TIER, MessageBackupTier::class.java))
}
private val errorHandler = InAppPaymentCheckoutDelegate.ErrorHandler()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -65,6 +81,17 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
)
}
LaunchedEffect(
state.selectedMessageBackupTier,
state.selectedMessageBackupTierLabel,
state.availableBackupTypes
) {
if (state.selectedMessageBackupTierLabel == null && state.selectedMessageBackupTier != null && state.availableBackupTypes.isNotEmpty()) {
val type = state.availableBackupTypes.firstOrNull { it.tier == state.selectedMessageBackupTier } ?: return@LaunchedEffect
viewModel.onMessageBackupTierUpdated(type.tier, getTypeLabel(type))
}
}
Nav.Host(
navController = navController,
startDestination = state.startScreen.name
@ -105,12 +132,8 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
availableBackupTypes = state.availableBackupTypes.filter { it.tier == MessageBackupTier.FREE || state.hasBackupSubscriberAvailable },
onMessageBackupsTierSelected = { tier ->
val type = state.availableBackupTypes.first { it.tier == tier }
val label = when (type) {
is MessageBackupsType.Free -> requireContext().resources.getQuantityString(R.plurals.MessageBackupsTypeSelectionScreen__text_plus_d_days_of_media, type.mediaRetentionDays, type.mediaRetentionDays)
is MessageBackupsType.Paid -> requireContext().getString(R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media)
}
viewModel.onMessageBackupTierUpdated(tier, label)
viewModel.onMessageBackupTierUpdated(tier, getTypeLabel(type))
},
onNavigationClick = viewModel::goToPreviousStage,
onReadMoreClicked = {},
@ -141,6 +164,13 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
}
}
private fun getTypeLabel(type: MessageBackupsType): String {
return when (type) {
is MessageBackupsType.Free -> requireContext().resources.getQuantityString(R.plurals.MessageBackupsTypeSelectionScreen__text_plus_d_days_of_media, type.mediaRetentionDays, type.mediaRetentionDays)
is MessageBackupsType.Paid -> requireContext().getString(R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media)
}
}
override fun onUserLaunchedAnExternalApplication() = error("Not supported by this fragment.")
override fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment) = error("Not supported by this fragment.")

View file

@ -45,7 +45,9 @@ import org.thoughtcrime.securesms.util.RemoteConfig
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import kotlin.time.Duration.Companion.seconds
class MessageBackupsFlowViewModel : ViewModel() {
class MessageBackupsFlowViewModel(
initialTierSelection: MessageBackupTier?
) : ViewModel() {
companion object {
private val TAG = Log.tag(MessageBackupsFlowViewModel::class)
@ -54,7 +56,7 @@ class MessageBackupsFlowViewModel : ViewModel() {
private val internalStateFlow = MutableStateFlow(
MessageBackupsFlowState(
availableBackupTypes = emptyList(),
selectedMessageBackupTier = SignalStore.backup.backupTier,
selectedMessageBackupTier = initialTierSelection ?: SignalStore.backup.backupTier,
startScreen = if (SignalStore.backup.backupTier == null) MessageBackupsStage.EDUCATION else MessageBackupsStage.TYPE_SELECTION
)
)

View file

@ -204,7 +204,7 @@ class AppSettingsFragment : DSLSettingsFragment(
if (RemoteConfig.messageBackups) {
clickPref(
title = DSLSettingsText.from(R.string.preferences_chats__backups),
// TODO [message-backups] -- icon
icon = DSLSettingsIcon.from(R.drawable.symbol_backup_24),
onClick = {
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_backupsSettingsFragment)
},

View file

@ -41,6 +41,7 @@ import org.signal.core.ui.SignalPreview
import org.signal.core.ui.Texts
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher
import org.thoughtcrime.securesms.compose.ComposeFragment
@ -57,7 +58,7 @@ import kotlin.time.Duration.Companion.seconds
*/
class BackupsSettingsFragment : ComposeFragment() {
private lateinit var checkoutLauncher: ActivityResultLauncher<Unit>
private lateinit var checkoutLauncher: ActivityResultLauncher<MessageBackupTier?>
private val viewModel: BackupsSettingsViewModel by viewModels()
@ -86,7 +87,7 @@ class BackupsSettingsFragment : ComposeFragment() {
}
BackupsSettingsState.EnabledState.Never -> {
checkoutLauncher.launch(Unit)
checkoutLauncher.launch(null)
}
else -> Unit

View file

@ -0,0 +1,28 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.backups.remote
import androidx.compose.runtime.Composable
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyRecordScreen
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.Util
/**
* Fragment which only displays the backup key to the user.
*/
class BackupKeyDisplayFragment : ComposeFragment() {
@Composable
override fun FragmentContent() {
MessageBackupsKeyRecordScreen(
backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(),
onNavigationClick = { findNavController().popBackStack() },
onCopyToClipboardClick = { Util.copyToClipboard(requireContext(), it) },
onNextClick = { findNavController().popBackStack() }
)
}
}

View file

@ -5,9 +5,14 @@
package org.thoughtcrime.securesms.components.settings.app.backups.remote
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@ -49,9 +54,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.fragment.app.setFragmentResultListener
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.signal.core.ui.Buttons
import org.signal.core.ui.Dialogs
import org.signal.core.ui.Dividers
@ -62,20 +69,23 @@ import org.signal.core.ui.SignalPreview
import org.signal.core.ui.Snackbars
import org.signal.core.ui.Texts
import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.BiometricDeviceAuthentication
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.components.settings.app.backups.type.BackupsTypeSettingsFragment
import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.dependencies.GooglePlayBillingDependencies
import org.thoughtcrime.securesms.fonts.SignalSymbols
import org.thoughtcrime.securesms.fonts.SignalSymbols.SignalSymbol
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.viewModel
@ -90,13 +100,19 @@ import kotlin.time.Duration.Companion.seconds
*/
class RemoteBackupsSettingsFragment : ComposeFragment() {
companion object {
private val TAG = Log.tag(RemoteBackupsSettingsFragment::class)
private const val AUTHENTICATE_REQUEST_CODE = 1
}
private val viewModel by viewModel {
RemoteBackupsSettingsViewModel()
}
private val args: RemoteBackupsSettingsFragmentArgs by navArgs()
private lateinit var checkoutLauncher: ActivityResultLauncher<Unit>
private lateinit var checkoutLauncher: ActivityResultLauncher<MessageBackupTier?>
private lateinit var biometricDeviceAuthentication: BiometricDeviceAuthentication
@Composable
override fun FragmentContent() {
@ -125,7 +141,21 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
}
override fun onBackupTypeActionClick(tier: MessageBackupTier) {
// TODO [message-backups]
when (tier) {
MessageBackupTier.FREE -> checkoutLauncher.launch(MessageBackupTier.PAID)
MessageBackupTier.PAID -> lifecycleScope.launch(Dispatchers.Main) {
val uri = Uri.parse(
getString(
R.string.backup_subscription_management_url,
GooglePlayBillingDependencies.getProductId(),
requireContext().applicationInfo.packageName
)
)
val intent = Intent(Intent.ACTION_VIEW, uri)
startActivity(intent)
}
}
}
override fun onBackUpUsingCellularClick(canUseCellular: Boolean) {
@ -160,11 +190,24 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
viewModel.turnOffAndDeleteBackups()
}
override fun onBackupsTypeClick() {
findNavController().safeNavigate(R.id.action_remoteBackupsSettingsFragment_to_backupsTypeSettingsFragment)
override fun onViewBackupKeyClick() {
if (!biometricDeviceAuthentication.authenticate(requireContext(), true, this@RemoteBackupsSettingsFragment::showConfirmDeviceCredentialIntent)) {
displayBackupKey()
}
}
}
private fun displayBackupKey() {
findNavController().safeNavigate(R.id.action_remoteBackupsSettingsFragment_to_backupKeyDisplayFragment)
}
private fun showConfirmDeviceCredentialIntent() {
val keyguardManager = ServiceUtil.getKeyguardManager(requireContext())
val intent = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.RemoteBackupsSettingsFragment__unlock_to_view_backup_key), "")
startActivityForResult(intent, AUTHENTICATE_REQUEST_CODE)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
checkoutLauncher = createBackupsCheckoutLauncher { backUpLater ->
@ -173,22 +216,41 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
}
}
setFragmentResultListener(BackupsTypeSettingsFragment.REQUEST_KEY) { _, bundle ->
val backUpLater = bundle.getBoolean(BackupsTypeSettingsFragment.REQUEST_KEY)
if (backUpLater) {
viewModel.requestSnackbar(RemoteBackupsSettingsState.Snackbar.BACKUP_WILL_BE_CREATED_OVERNIGHT)
}
}
if (savedInstanceState == null && args.backupLaterSelected) {
viewModel.requestSnackbar(RemoteBackupsSettingsState.Snackbar.BACKUP_WILL_BE_CREATED_OVERNIGHT)
}
val biometricManager = BiometricManager.from(requireContext())
val biometricPrompt = BiometricPrompt(this, AuthListener())
val promptInfo: BiometricPrompt.PromptInfo = BiometricPrompt.PromptInfo.Builder()
.setAllowedAuthenticators(BiometricDeviceAuthentication.ALLOWED_AUTHENTICATORS)
.setTitle(getString(R.string.RemoteBackupsSettingsFragment__unlock_to_view_backup_key))
.build()
biometricDeviceAuthentication = BiometricDeviceAuthentication(biometricManager, biometricPrompt, promptInfo)
}
override fun onResume() {
super.onResume()
viewModel.refresh()
}
private inner class AuthListener : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationFailed() {
Log.w(TAG, "onAuthenticationFailed")
Toast.makeText(requireContext(), R.string.authentication_required, Toast.LENGTH_SHORT).show()
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
Log.i(TAG, "onAuthenticationSucceeded")
displayBackupKey()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
Log.w(TAG, "onAuthenticationError: $errorCode, $errString")
onAuthenticationFailed()
}
}
}
/**
@ -196,7 +258,6 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
*/
private interface ContentCallbacks {
fun onNavigationClick() = Unit
fun onBackupsTypeClick() = Unit
fun onBackupTypeActionClick(tier: MessageBackupTier) = Unit
fun onBackUpUsingCellularClick(canUseCellular: Boolean) = Unit
fun onBackupNowClick() = Unit
@ -206,6 +267,7 @@ private interface ContentCallbacks {
fun onSnackbarDismissed() = Unit
fun onSelectBackupsFrequencyChange(newFrequency: BackupFrequency) = Unit
fun onTurnOffAndDeleteBackupsConfirm() = Unit
fun onViewBackupKeyClick() = Unit
}
@Composable
@ -309,6 +371,13 @@ private fun RemoteBackupsSettingsContent(
)
}
item {
Rows.TextRow(
text = stringResource(R.string.RemoteBackupsSettingsFragment__view_backup_key),
onClick = contentCallbacks::onViewBackupKeyClick
)
}
item {
Dividers.Default()
}

View file

@ -51,7 +51,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
if (activeSubscription.isSuccess) {
val subscription = activeSubscription.getOrThrow().activeSubscription
if (subscription.isActive && subscription != null) {
if (subscription != null) {
_state.update { it.copy(renewalTime = subscription.endOfCurrentPeriod.seconds) }
}
}

View file

@ -1,227 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.backups.type
import android.os.Bundle
import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import androidx.navigation.fragment.findNavController
import org.signal.core.ui.Previews
import org.signal.core.ui.Rows
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.SignalPreview
import org.signal.core.util.money.FiatMoney
import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.viewModel
import java.math.BigDecimal
import java.util.Locale
/**
* Allows the user to modify their backup plan
*/
class BackupsTypeSettingsFragment : ComposeFragment() {
companion object {
const val REQUEST_KEY = "BackupsTypeSettingsFragment__result"
}
private val viewModel: BackupsTypeSettingsViewModel by viewModel {
BackupsTypeSettingsViewModel()
}
private lateinit var checkoutLauncher: ActivityResultLauncher<Unit>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
checkoutLauncher = createBackupsCheckoutLauncher { backUpLater ->
findNavController().popBackStack()
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to backUpLater))
}
}
@Composable
override fun FragmentContent() {
val contentCallbacks = remember {
Callbacks()
}
val state by viewModel.state.collectAsState()
BackupsTypeSettingsContent(
state = state,
contentCallbacks = contentCallbacks
)
}
private inner class Callbacks : ContentCallbacks {
override fun onNavigationClick() {
findNavController().popBackStack()
}
override fun onChangeOrCancelSubscriptionClick() {
checkoutLauncher.launch(Unit)
}
}
override fun onResume() {
super.onResume()
viewModel.refresh()
}
}
private interface ContentCallbacks {
fun onNavigationClick() = Unit
fun onPaymentHistoryClick() = Unit
fun onChangeOrCancelSubscriptionClick() = Unit
}
@Composable
private fun BackupsTypeSettingsContent(
state: BackupsTypeSettingsState,
contentCallbacks: ContentCallbacks
) {
if (state.messageBackupsType == null) {
return
}
Scaffolds.Settings(
title = "Backup Type",
onNavigationClick = contentCallbacks::onNavigationClick,
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24)
) {
LazyColumn(
modifier = Modifier.padding(it)
) {
item {
BackupsTypeRow(
messageBackupsType = state.messageBackupsType,
nextRenewalTimestamp = state.nextRenewalTimestamp
)
}
item {
PaymentSourceRow(
paymentSourceType = state.paymentSourceType
)
}
item {
Rows.TextRow(
text = stringResource(id = R.string.BackupsTypeSettingsFragment__change_or_cancel_subscription),
onClick = contentCallbacks::onChangeOrCancelSubscriptionClick
)
}
item {
Rows.TextRow(
text = stringResource(id = R.string.BackupsTypeSettingsFragment__payment_history),
onClick = contentCallbacks::onPaymentHistoryClick
)
}
}
}
}
@Composable
private fun BackupsTypeRow(
messageBackupsType: MessageBackupsType,
nextRenewalTimestamp: Long
) {
val resources = LocalContext.current.resources
val formattedAmount = remember(messageBackupsType) {
val amount = when (messageBackupsType) {
is MessageBackupsType.Paid -> messageBackupsType.pricePerMonth
else -> FiatMoney(BigDecimal.ZERO, SignalStore.inAppPayments.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.BACKUP))
}
FiatMoneyUtil.format(resources, amount, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
}
val title = when (messageBackupsType) {
is MessageBackupsType.Paid -> stringResource(id = R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media)
is MessageBackupsType.Free -> pluralStringResource(id = R.plurals.MessageBackupsTypeSelectionScreen__text_plus_d_days_of_media, count = messageBackupsType.mediaRetentionDays, messageBackupsType.mediaRetentionDays)
}
val renewal = remember(nextRenewalTimestamp) {
DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(), nextRenewalTimestamp)
}
Rows.TextRow(text = {
Column {
Text(text = title)
Text(
text = stringResource(id = R.string.BackupsTypeSettingsFragment__s_month_renews_s, formattedAmount, renewal),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
})
}
@Composable
private fun PaymentSourceRow(paymentSourceType: PaymentSourceType) {
val paymentSourceTextResId = remember(paymentSourceType) {
when (paymentSourceType) {
is PaymentSourceType.GooglePlayBilling -> R.string.BackupsTypeSettingsFragment__google_play
is PaymentSourceType.Stripe.CreditCard -> R.string.BackupsTypeSettingsFragment__credit_or_debit_card
is PaymentSourceType.Stripe.IDEAL -> R.string.BackupsTypeSettingsFragment__iDEAL
is PaymentSourceType.Stripe.GooglePay -> R.string.BackupsTypeSettingsFragment__google_pay
is PaymentSourceType.Stripe.SEPADebit -> R.string.BackupsTypeSettingsFragment__bank_transfer
is PaymentSourceType.PayPal -> R.string.BackupsTypeSettingsFragment__paypal
is PaymentSourceType.Unknown -> R.string.BackupsTypeSettingsFragment__unknown
}
}
Rows.TextRow(text = {
Column {
Text(text = "Payment method") // TOD [message-backups] Final copy
Text(
text = stringResource(id = paymentSourceTextResId),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
})
}
@SignalPreview
@Composable
private fun BackupsTypeSettingsContentPreview() {
Previews.Preview {
BackupsTypeSettingsContent(
state = BackupsTypeSettingsState(
messageBackupsType = MessageBackupsType.Free(
mediaRetentionDays = 30
)
),
contentCallbacks = object : ContentCallbacks {}
)
}
}

View file

@ -1,17 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.backups.type
import androidx.compose.runtime.Stable
import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
@Stable
data class BackupsTypeSettingsState(
val messageBackupsType: MessageBackupsType? = null,
val paymentSourceType: PaymentSourceType = PaymentSourceType.Unknown,
val nextRenewalTimestamp: Long = 0
)

View file

@ -1,46 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.backups.type
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.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.keyvalue.SignalStore
class BackupsTypeSettingsViewModel : ViewModel() {
private val internalState = MutableStateFlow(BackupsTypeSettingsState())
val state: StateFlow<BackupsTypeSettingsState> = internalState
init {
refresh()
}
fun refresh() {
viewModelScope.launch {
val tier = SignalStore.backup.backupTier
val paymentMethod = withContext(Dispatchers.IO) {
InAppPaymentsRepository.getLatestPaymentMethodType(InAppPaymentSubscriberRecord.Type.BACKUP)
}
internalState.update {
it.copy(
messageBackupsType = if (tier != null) BackupRepository.getBackupsType(tier) else null,
paymentSourceType = paymentMethod.toPaymentSourceType()
)
}
}
}
}

View file

@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
@ -17,7 +18,7 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate
class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__chats) {
private lateinit var viewModel: ChatsSettingsViewModel
private lateinit var checkoutLauncher: ActivityResultLauncher<Unit>
private lateinit var checkoutLauncher: ActivityResultLauncher<MessageBackupTier?>
override fun onResume() {
super.onResume()
@ -98,7 +99,7 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
if (state.canAccessRemoteBackupsSettings) {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_remoteBackupsSettingsFragment)
} else {
checkoutLauncher.launch(Unit)
checkoutLauncher.launch(null)
}
}
)

View file

@ -33,6 +33,7 @@ import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeBlock
@ -49,7 +50,7 @@ class UpgradeToEnableOptimizedStorageSheet : ComposeBottomSheetDialogFragment()
private val viewModel: UpgradeToEnableOptimizedStorageViewModel by viewModels()
private lateinit var checkoutLauncher: ActivityResultLauncher<Unit>
private lateinit var checkoutLauncher: ActivityResultLauncher<MessageBackupTier?>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -62,7 +63,7 @@ class UpgradeToEnableOptimizedStorageSheet : ComposeBottomSheetDialogFragment()
UpgradeToEnableOptimizedStorageSheetContent(
messageBackupsType = type,
onUpgradeNowClick = {
checkoutLauncher.launch(Unit)
checkoutLauncher.launch(MessageBackupTier.PAID)
dismissAllowingStateLoss()
},
onCancelClick = {

View file

@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription
import androidx.activity.result.ActivityResultLauncher
import androidx.fragment.app.Fragment
import org.signal.core.util.getSerializableCompat
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.ui.CreateBackupBottomSheet
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsCheckoutActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentProcessorAction
@ -17,7 +18,7 @@ object MessageBackupsCheckoutLauncher {
fun Fragment.createBackupsCheckoutLauncher(
onCreateBackupBottomSheetResultListener: OnCreateBackupBottomSheetResultListener = {} as OnCreateBackupBottomSheetResultListener
): ActivityResultLauncher<Unit> {
): ActivityResultLauncher<MessageBackupTier?> {
childFragmentManager.setFragmentResultListener(CreateBackupBottomSheet.REQUEST_KEY, viewLifecycleOwner) { requestKey, bundle ->
if (requestKey == CreateBackupBottomSheet.REQUEST_KEY) {
val result = bundle.getSerializableCompat(CreateBackupBottomSheet.REQUEST_KEY, CreateBackupBottomSheet.Result::class.java)

View file

@ -985,8 +985,8 @@
android:name="org.thoughtcrime.securesms.components.settings.app.backups.remote.RemoteBackupsSettingsFragment">
<action
android:id="@+id/action_remoteBackupsSettingsFragment_to_backupsTypeSettingsFragment"
app:destination="@id/backupsTypeSettingsFragment"
android:id="@+id/action_remoteBackupsSettingsFragment_to_backupKeyDisplayFragment"
app:destination="@id/backupKeyDisplayFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
@ -999,10 +999,8 @@
</fragment>
<fragment
android:id="@+id/backupsTypeSettingsFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.backups.type.BackupsTypeSettingsFragment">
</fragment>
android:id="@+id/backupKeyDisplayFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyDisplayFragment" />
<include app:graph="@navigation/username_link_settings" />
<include app:graph="@navigation/story_privacy_settings" />

View file

@ -999,8 +999,8 @@
android:name="org.thoughtcrime.securesms.components.settings.app.backups.remote.RemoteBackupsSettingsFragment">
<action
android:id="@+id/action_remoteBackupsSettingsFragment_to_backupsTypeSettingsFragment"
app:destination="@id/backupsTypeSettingsFragment"
android:id="@+id/action_remoteBackupsSettingsFragment_to_backupKeyDisplayFragment"
app:destination="@id/backupKeyDisplayFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
@ -1013,10 +1013,8 @@
</fragment>
<fragment
android:id="@+id/backupsTypeSettingsFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.backups.type.BackupsTypeSettingsFragment">
</fragment>
android:id="@+id/backupKeyDisplayFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyDisplayFragment" />
<include app:graph="@navigation/username_link_settings" />
<include app:graph="@navigation/story_privacy_settings" />

View file

@ -23,6 +23,9 @@
<string name="pending_transfer_url" translatable="false">https://support.signal.org/hc/articles/360031949872#pending</string>
<string name="donate_faq_url" translatable="false">https://support.signal.org/hc/articles/360031949872#donate</string>
<!-- First placeholder is productId, second placeholder is app package -->
<string name="backup_subscription_management_url">https://play.google.com/store/account/subscriptions?sku=%1$s&amp;package=%2$s</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="delete">Delete</string>
@ -7390,6 +7393,10 @@
<string name="RemoteBackupsSettingsFragment__backup_frequency">Backup frequency</string>
<!-- Toggle row label for allowing backups to occur while on cellular connection -->
<string name="RemoteBackupsSettingsFragment__back_up_using_cellular">Back up using cellular</string>
<!-- Row label for viewing backup key -->
<string name="RemoteBackupsSettingsFragment__view_backup_key">View backup key</string>
<!-- Prompt title for unlocking device to view backup key -->
<string name="RemoteBackupsSettingsFragment__unlock_to_view_backup_key">Unlock to view backup key</string>
<!-- Row label for cancelling and deleting backup -->
<string name="RemoteBackupsSettingsFragment__turn_off_and_delete_backup">Turn off and delete backup</string>
<!-- Snackbar text displayed when backup has been deleted and turned off -->