Add biometric prompt to reveal backup key from settings and other fixes.
This commit is contained in:
parent
321c344e77
commit
8990088980
17 changed files with 188 additions and 337 deletions
|
@ -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? {
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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&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 -->
|
||||
|
|
Loading…
Add table
Reference in a new issue