From 89900889803c8b67b7b8171383ce8f8b9878f161 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 3 Oct 2024 10:06:04 -0300 Subject: [PATCH] Add biometric prompt to reveal backup key from settings and other fixes. --- .../MessageBackupsCheckoutActivity.kt | 13 +- .../MessageBackupsFlowFragment.kt | 42 +++- .../MessageBackupsFlowViewModel.kt | 6 +- .../settings/app/AppSettingsFragment.kt | 2 +- .../app/backups/BackupsSettingsFragment.kt | 5 +- .../remote/BackupKeyDisplayFragment.kt | 28 +++ .../remote/RemoteBackupsSettingsFragment.kt | 97 ++++++-- .../remote/RemoteBackupsSettingsViewModel.kt | 2 +- .../type/BackupsTypeSettingsFragment.kt | 227 ------------------ .../backups/type/BackupsTypeSettingsState.kt | 17 -- .../type/BackupsTypeSettingsViewModel.kt | 46 ---- .../app/chats/ChatsSettingsFragment.kt | 5 +- .../UpgradeToEnableOptimizedStorageSheet.kt | 5 +- .../MessageBackupsCheckoutLauncher.kt | 3 +- app/src/main/res/navigation/app_settings.xml | 10 +- .../app_settings_with_change_number.xml | 10 +- app/src/main/res/values/strings.xml | 7 + 17 files changed, 188 insertions(+), 337 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsFragment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsState.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsViewModel.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutActivity.kt index 13ef1a4e52..f2131345fa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutActivity.kt @@ -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() { + class Contract : ActivityResultContract() { - 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? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt index ddabf9e879..a490d61339 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt @@ -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.") diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt index 0a13d5ffee..e140e0298e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt @@ -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 ) ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt index 560e291f76..af380361cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt @@ -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) }, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsFragment.kt index 135ec715d3..cd7e4a75df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsFragment.kt @@ -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 + private lateinit var checkoutLauncher: ActivityResultLauncher private val viewModel: BackupsSettingsViewModel by viewModels() @@ -86,7 +87,7 @@ class BackupsSettingsFragment : ComposeFragment() { } BackupsSettingsState.EnabledState.Never -> { - checkoutLauncher.launch(Unit) + checkoutLauncher.launch(null) } else -> Unit diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt new file mode 100644 index 0000000000..40dce01740 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt @@ -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() } + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt index efeea40c24..f7417a5d71 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt @@ -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 + private lateinit var checkoutLauncher: ActivityResultLauncher + 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() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt index 3bf3fbd45a..0ae28905e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt @@ -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) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsFragment.kt deleted file mode 100644 index 1e1772333f..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsFragment.kt +++ /dev/null @@ -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 - - 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 {} - ) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsState.kt deleted file mode 100644 index 52dde1e5e9..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsState.kt +++ /dev/null @@ -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 -) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsViewModel.kt deleted file mode 100644 index 54c16164e5..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsViewModel.kt +++ /dev/null @@ -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 = 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() - ) - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt index 7e34176914..53925dbe45 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt @@ -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 + private lateinit var checkoutLauncher: ActivityResultLauncher 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) } } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageSheet.kt index 31b9214410..45d7048f94 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageSheet.kt @@ -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 + private lateinit var checkoutLauncher: ActivityResultLauncher 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 = { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/MessageBackupsCheckoutLauncher.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/MessageBackupsCheckoutLauncher.kt index 70ae6cb962..9e246e7ac6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/MessageBackupsCheckoutLauncher.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/MessageBackupsCheckoutLauncher.kt @@ -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 { + ): ActivityResultLauncher { childFragmentManager.setFragmentResultListener(CreateBackupBottomSheet.REQUEST_KEY, viewLifecycleOwner) { requestKey, bundle -> if (requestKey == CreateBackupBottomSheet.REQUEST_KEY) { val result = bundle.getSerializableCompat(CreateBackupBottomSheet.REQUEST_KEY, CreateBackupBottomSheet.Result::class.java) diff --git a/app/src/main/res/navigation/app_settings.xml b/app/src/main/res/navigation/app_settings.xml index ae627bd003..967988588e 100644 --- a/app/src/main/res/navigation/app_settings.xml +++ b/app/src/main/res/navigation/app_settings.xml @@ -985,8 +985,8 @@ android:name="org.thoughtcrime.securesms.components.settings.app.backups.remote.RemoteBackupsSettingsFragment"> - - + android:id="@+id/backupKeyDisplayFragment" + android:name="org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyDisplayFragment" /> diff --git a/app/src/main/res/navigation/app_settings_with_change_number.xml b/app/src/main/res/navigation/app_settings_with_change_number.xml index a1b616ebfb..7b5386c0d7 100644 --- a/app/src/main/res/navigation/app_settings_with_change_number.xml +++ b/app/src/main/res/navigation/app_settings_with_change_number.xml @@ -999,8 +999,8 @@ android:name="org.thoughtcrime.securesms.components.settings.app.backups.remote.RemoteBackupsSettingsFragment"> - - + android:id="@+id/backupKeyDisplayFragment" + android:name="org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyDisplayFragment" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 87762db825..71bfbdd9df 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,6 +23,9 @@ https://support.signal.org/hc/articles/360031949872#pending https://support.signal.org/hc/articles/360031949872#donate + + https://play.google.com/store/account/subscriptions?sku=%1$s&package=%2$s + Yes No Delete @@ -7390,6 +7393,10 @@ Backup frequency Back up using cellular + + View backup key + + Unlock to view backup key Turn off and delete backup