From 9dcc704a9e8dc748809da67e67d7ad8f6cee4d16 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 5 Aug 2025 16:04:10 -0400 Subject: [PATCH] Add specific registration error cases for SVRB. --- .../securesms/backup/v2/BackupRepository.kt | 3 + .../contactsupport/ContactSupportDialog.kt | 17 ++++-- .../ContactSupportDialogFragment.kt | 9 +-- .../contactsupport/ContactSupportViewModel.kt | 18 +++--- .../InternalBackupPlaygroundViewModel.kt | 1 + .../ui/restore/EnterBackupKeyFragment.kt | 8 +-- .../ui/restore/RemoteRestoreActivity.kt | 56 ++++++++++++++++--- .../ui/restore/RemoteRestoreViewModel.kt | 6 ++ app/src/main/res/values/strings.xml | 9 +++ 9 files changed, 97 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index 9e36f92fe5..7342ba22e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -2178,6 +2178,9 @@ sealed interface RemoteRestoreResult { data object NetworkError : RemoteRestoreResult data object Canceled : RemoteRestoreResult data object Failure : RemoteRestoreResult + + /** SVRB has failed in such a way that recovering a backup is impossible. */ + data object PermanentSvrBFailure : RemoteRestoreResult } sealed interface RestoreTimestampResult { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportDialog.kt index 30f1a60755..67d78e6828 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportDialog.kt @@ -27,6 +27,11 @@ interface ContactSupportCallbacks { override fun submitWithoutDebuglog() = Unit override fun cancel() = Unit } + + fun interface StringForReason { + @StringRes + operator fun invoke(reason: Reason?): Int + } } /** @@ -58,23 +63,23 @@ fun ContactSupportDialog( * sending an email when ready. */ @Composable -fun SendSupportEmailEffect( - contactSupportState: ContactSupportViewModel.ContactSupportState, - @StringRes subjectRes: Int, - @StringRes filterRes: Int, +fun SendSupportEmailEffect( + contactSupportState: ContactSupportViewModel.ContactSupportState, + subjectRes: ContactSupportCallbacks.StringForReason, + filterRes: ContactSupportCallbacks.StringForReason, hide: () -> Unit ) { val context = LocalContext.current LaunchedEffect(contactSupportState.sendEmail) { if (contactSupportState.sendEmail) { - val subject = context.getString(subjectRes) + val subject = context.getString(subjectRes(contactSupportState.reason)) val prefix = if (contactSupportState.debugLogUrl != null) { "\n${context.getString(R.string.HelpFragment__debug_log)} ${contactSupportState.debugLogUrl}\n\n" } else { "" } - val body = SupportEmailUtil.generateSupportEmailBody(context, filterRes, prefix, null) + val body = SupportEmailUtil.generateSupportEmailBody(context, filterRes(contactSupportState.reason), prefix, null) CommunicationActions.openEmail(context, SupportEmailUtil.getSupportEmailAddress(context), subject, body) hide() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportDialogFragment.kt index 174e0dc607..32f48c1e65 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportDialogFragment.kt @@ -35,23 +35,20 @@ class ContactSupportDialogFragment : ComposeDialogFragment() { } } - private val contactSupportViewModel: ContactSupportViewModel by viewModel { + private val contactSupportViewModel: ContactSupportViewModel by viewModel { ContactSupportViewModel( showInitially = true ) } - private val subject: Int by lazy { requireArguments().getInt(SUBJECT) } - private val filter: Int by lazy { requireArguments().getInt(FILTER) } - @Composable override fun DialogContent() { val contactSupportState by contactSupportViewModel.state.collectAsStateWithLifecycle() SendSupportEmailEffect( contactSupportState = contactSupportState, - subjectRes = subject, - filterRes = filter + subjectRes = { requireArguments().getInt(SUBJECT) }, + filterRes = { requireArguments().getInt(FILTER) } ) { contactSupportViewModel.hideContactSupport() dismissAllowingStateLoss() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportViewModel.kt index 60eac499a9..ef5a5bcd70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportViewModel.kt @@ -17,18 +17,21 @@ import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository /** * Intended to be used to drive [ContactSupportDialog]. + * + * @param Reason A type that can be supplied as the reason for showing the dialog when you invoke [showContactSupport]. Useful for when you may want to show + * the option for different reasons. Will be given back to you, if set, via [state] in [ContactSupportState.reason]. */ -class ContactSupportViewModel( +class ContactSupportViewModel( val showInitially: Boolean = false ) : ViewModel(), ContactSupportCallbacks { private val submitDebugLogRepository: SubmitDebugLogRepository = SubmitDebugLogRepository() - private val store: MutableStateFlow = MutableStateFlow(ContactSupportState(show = showInitially)) + private val store: MutableStateFlow> = MutableStateFlow(ContactSupportState(show = showInitially)) - val state: StateFlow = store.asStateFlow() + val state: StateFlow> = store.asStateFlow() - fun showContactSupport() { - store.update { it.copy(show = true) } + fun showContactSupport(reason: Reason? = null) { + store.update { it.copy(show = true, reason = reason) } } fun hideContactSupport() { @@ -60,10 +63,11 @@ class ContactSupportViewModel( hideContactSupport() } - data class ContactSupportState( + data class ContactSupportState( val show: Boolean = false, val showAsProgress: Boolean = false, val sendEmail: Boolean = false, - val debugLogUrl: String? = null + val debugLogUrl: String? = null, + val reason: Reason? = null ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt index 8b2f6a0c7b..654d4e4607 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt @@ -360,6 +360,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() { RemoteRestoreResult.Success -> _state.value = _state.value.copy(statusMessage = "Import complete!") RemoteRestoreResult.Canceled, RemoteRestoreResult.Failure, + RemoteRestoreResult.PermanentSvrBFailure, RemoteRestoreResult.NetworkError -> { _state.value = _state.value.copy(statusMessage = "Import failed! $result") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyFragment.kt index 820b196fe9..11805e9299 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyFragment.kt @@ -45,7 +45,7 @@ class EnterBackupKeyFragment : ComposeFragment() { private val sharedViewModel by activityViewModels() private val viewModel by viewModels() - private val contactSupportViewModel: ContactSupportViewModel by viewModels() + private val contactSupportViewModel: ContactSupportViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -79,12 +79,12 @@ class EnterBackupKeyFragment : ComposeFragment() { override fun FragmentContent() { val state by viewModel.state.collectAsStateWithLifecycle() val sharedState by sharedViewModel.state.collectAsStateWithLifecycle() - val contactSupportState: ContactSupportViewModel.ContactSupportState by contactSupportViewModel.state.collectAsStateWithLifecycle() + val contactSupportState: ContactSupportViewModel.ContactSupportState by contactSupportViewModel.state.collectAsStateWithLifecycle() SendSupportEmailEffect( contactSupportState = contactSupportState, - subjectRes = R.string.EnterBackupKey_network_failure_support_email, - filterRes = R.string.EnterBackupKey_network_failure_support_email_filter + subjectRes = { R.string.EnterBackupKey_network_failure_support_email }, + filterRes = { R.string.EnterBackupKey_network_failure_support_email_filter } ) { contactSupportViewModel.hideContactSupport() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RemoteRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RemoteRestoreActivity.kt index 1200d81639..a260780256 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RemoteRestoreActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RemoteRestoreActivity.kt @@ -99,7 +99,7 @@ class RemoteRestoreActivity : BaseActivity() { RemoteRestoreViewModel(intent.getBooleanExtra(KEY_ONLY_OPTION, false)) } - private val contactSupportViewModel: ContactSupportViewModel by viewModels() + private val contactSupportViewModel: ContactSupportViewModel by viewModels() private lateinit var wakeLock: RemoteRestoreWakeLock @@ -146,12 +146,22 @@ class RemoteRestoreActivity : BaseActivity() { setContent { val state: RemoteRestoreViewModel.ScreenState by viewModel.state.collectAsStateWithLifecycle() - val contactSupportState: ContactSupportViewModel.ContactSupportState by contactSupportViewModel.state.collectAsStateWithLifecycle() + val contactSupportState: ContactSupportViewModel.ContactSupportState by contactSupportViewModel.state.collectAsStateWithLifecycle() SendSupportEmailEffect( contactSupportState = contactSupportState, - subjectRes = R.string.EnterBackupKey_network_failure_support_email, - filterRes = R.string.EnterBackupKey_network_failure_support_email_filter + subjectRes = { reason -> + when (reason) { + ContactSupportReason.SvrBFailure -> R.string.EnterBackupKey_permanent_failure_support_email + else -> R.string.EnterBackupKey_network_failure_support_email + } + }, + filterRes = { reason -> + when (reason) { + ContactSupportReason.SvrBFailure -> R.string.EnterBackupKey_permanent_failure_support_email_filter + else -> R.string.EnterBackupKey_network_failure_support_email_filter + } + } ) { contactSupportViewModel.hideContactSupport() } @@ -192,12 +202,16 @@ class RemoteRestoreActivity : BaseActivity() { fun onEvent(restoreEvent: RestoreV2Event) { viewModel.updateRestoreProgress(restoreEvent) } + + enum class ContactSupportReason { + NetworkError, SvrBFailure + } } @Composable private fun RestoreFromBackupContent( state: RemoteRestoreViewModel.ScreenState, - contactSupportState: ContactSupportViewModel.ContactSupportState = ContactSupportViewModel.ContactSupportState(), + contactSupportState: ContactSupportViewModel.ContactSupportState = ContactSupportViewModel.ContactSupportState(), onRestoreBackupClick: () -> Unit = {}, onRetryRestoreTier: () -> Unit = {}, onContactSupport: () -> Unit = {}, @@ -219,7 +233,8 @@ private fun RestoreFromBackupContent( onRestoreBackupClick = onRestoreBackupClick, onCancelClick = onCancelClick, onImportErrorDialogDismiss = onImportErrorDialogDismiss, - onUpdateSignal = onUpdateSignal + onUpdateSignal = onUpdateSignal, + onContactSupport = onContactSupport ) } @@ -255,7 +270,8 @@ private fun BackupAvailableContent( onRestoreBackupClick: () -> Unit, onCancelClick: () -> Unit, onImportErrorDialogDismiss: () -> Unit, - onUpdateSignal: () -> Unit + onUpdateSignal: () -> Unit, + onContactSupport: () -> Unit ) { val subtitle = if (state.backupSize.bytes > 0) { stringResource( @@ -377,6 +393,9 @@ private fun BackupAvailableContent( RestoreFailedDialog(onDismiss = onImportErrorDialogDismiss) } } + RemoteRestoreViewModel.ImportState.FailureWithLogPrompt -> { + RestoreFailedWithLogPromptDialog(onDismiss = onImportErrorDialogDismiss, onContactSupport = onContactSupport) + } } } } @@ -565,6 +584,21 @@ fun RestoreFailedDialog( ) } +@Composable +fun RestoreFailedWithLogPromptDialog( + onDismiss: () -> Unit = {}, + onContactSupport: () -> Unit = {} +) { + Dialogs.SimpleAlertDialog( + title = stringResource(R.string.RemoteRestoreActivity__failure_with_log_prompt_title), + body = stringResource(R.string.RemoteRestoreActivity__failure_with_log_prompt_body), + confirm = stringResource(R.string.RemoteRestoreActivity__failure_with_log_prompt_contact_button), + dismiss = stringResource(android.R.string.ok), + onConfirm = onContactSupport, + onDismiss = onDismiss + ) +} + @Composable fun RestoreNetworkFailedDialog( onDismiss: () -> Unit = {} @@ -617,6 +651,14 @@ private fun RestoreFailedDialogPreview() { } } +@SignalPreview +@Composable +private fun RestoreFailedWithLogPromptDialogPreview() { + Previews.Preview { + RestoreFailedWithLogPromptDialog() + } +} + @Composable fun InvalidBackupVersionDialog( onUpdateSignal: () -> Unit = {}, diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RemoteRestoreViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RemoteRestoreViewModel.kt index ddb6c0fbbd..b88565810c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RemoteRestoreViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RemoteRestoreViewModel.kt @@ -120,6 +120,11 @@ class RemoteRestoreViewModel(isOnlyRestoreOption: Boolean) : ViewModel() { Log.w(TAG, "Restore failed with $result") store.update { it.copy(importState = ImportState.Failed) } } + + RemoteRestoreResult.PermanentSvrBFailure -> { + Log.w(TAG, "Hit a permanent SVRB error.") + store.update { it.copy(importState = ImportState.FailureWithLogPrompt) } + } } } } @@ -181,5 +186,6 @@ class RemoteRestoreViewModel(isOnlyRestoreOption: Boolean) : ViewModel() { data object Restored : ImportState data object NetworkFailure : ImportState data object Failed : ImportState + data object FailureWithLogPrompt : ImportState } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 450ea9867d..0a0905c9ef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1483,6 +1483,12 @@ Skip restore Couldn\'t finish transfer + + Can\'t restore backup + + An error occurred while restoring your backup. Your backup is not recoverable. Please contact support for help. + + Contact support An error occurred and your account couldn’t be transferred. Try again by choosing your transfer method. @@ -8590,6 +8596,9 @@ Signal Android Backup restore network error Android SignalBackups Import Failed + + Signal Android Backup restore permanent failure + Android SignalBackups Import Permanent Failure Signal Android Backup export network error