Add specific registration error cases for SVRB.

This commit is contained in:
Greyson Parrelli
2025-08-05 16:04:10 -04:00
committed by Cody Henthorne
parent 0726c29528
commit 9dcc704a9e
9 changed files with 97 additions and 30 deletions

View File

@@ -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 {

View File

@@ -27,6 +27,11 @@ interface ContactSupportCallbacks {
override fun submitWithoutDebuglog() = Unit
override fun cancel() = Unit
}
fun interface StringForReason<Reason> {
@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 <Reason> SendSupportEmailEffect(
contactSupportState: ContactSupportViewModel.ContactSupportState<Reason>,
subjectRes: ContactSupportCallbacks.StringForReason<Reason>,
filterRes: ContactSupportCallbacks.StringForReason<Reason>,
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()
}

View File

@@ -35,23 +35,20 @@ class ContactSupportDialogFragment : ComposeDialogFragment() {
}
}
private val contactSupportViewModel: ContactSupportViewModel by viewModel {
private val contactSupportViewModel: ContactSupportViewModel<Unit> 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()

View File

@@ -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<Reason>(
val showInitially: Boolean = false
) : ViewModel(), ContactSupportCallbacks {
private val submitDebugLogRepository: SubmitDebugLogRepository = SubmitDebugLogRepository()
private val store: MutableStateFlow<ContactSupportState> = MutableStateFlow(ContactSupportState(show = showInitially))
private val store: MutableStateFlow<ContactSupportState<Reason>> = MutableStateFlow(ContactSupportState(show = showInitially))
val state: StateFlow<ContactSupportState> = store.asStateFlow()
val state: StateFlow<ContactSupportState<Reason>> = 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<Reason>(
val show: Boolean = false,
val showAsProgress: Boolean = false,
val sendEmail: Boolean = false,
val debugLogUrl: String? = null
val debugLogUrl: String? = null,
val reason: Reason? = null
)
}

View File

@@ -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")
}

View File

@@ -45,7 +45,7 @@ class EnterBackupKeyFragment : ComposeFragment() {
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
private val viewModel by viewModels<EnterBackupKeyViewModel>()
private val contactSupportViewModel: ContactSupportViewModel by viewModels()
private val contactSupportViewModel: ContactSupportViewModel<Unit> 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<Unit> 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()
}

View File

@@ -99,7 +99,7 @@ class RemoteRestoreActivity : BaseActivity() {
RemoteRestoreViewModel(intent.getBooleanExtra(KEY_ONLY_OPTION, false))
}
private val contactSupportViewModel: ContactSupportViewModel by viewModels()
private val contactSupportViewModel: ContactSupportViewModel<ContactSupportReason> 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<ContactSupportReason> 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<RemoteRestoreActivity.ContactSupportReason> = 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 = {},

View File

@@ -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
}
}

View File

@@ -1483,6 +1483,12 @@
<string name="RemoteRestoreActivity__skip_restore">Skip restore</string>
<!-- Dialog title displayed when remote restore failed -->
<string name="RemoteRestoreActivity__couldnt_transfer">Couldn\'t finish transfer</string>
<!-- Dialog title displayed when remote restore failed and we want them to contact support -->
<string name="RemoteRestoreActivity__failure_with_log_prompt_title">Can\'t restore backup</string>
<!-- Dialog body displayed when remote restore failed and we want them to contact support -->
<string name="RemoteRestoreActivity__failure_with_log_prompt_body">An error occurred while restoring your backup. Your backup is not recoverable. Please contact support for help.</string>
<!-- Dialog action button that will link users to a flow to contact support, displayed when remote restore failed -->
<string name="RemoteRestoreActivity__failure_with_log_prompt_contact_button">Contact support</string>
<!-- Dialog message displayed when remote restore failed -->
<string name="RemoteRestoreActivity__error_occurred">An error occurred and your account couldnt be transferred. Try again by choosing your transfer method.</string>
<!-- Dialog title displayed when remote restore failed because of an outdated backup version. -->
@@ -8590,6 +8596,9 @@
<!-- Email subject when contacting support on a restore backup network issue -->
<string name="EnterBackupKey_network_failure_support_email">Signal Android Backup restore network error</string>
<string name="EnterBackupKey_network_failure_support_email_filter" translatable="false">Android SignalBackups Import Failed</string>
<!-- Email subject when contacting support on a permanent backup import failure -->
<string name="EnterBackupKey_permanent_failure_support_email">Signal Android Backup restore permanent failure</string>
<string name="EnterBackupKey_permanent_failure_support_email_filter" translatable="false">Android SignalBackups Import Permanent Failure</string>
<!-- Email subject when contacting support on a create backup failure -->
<string name="BackupAlertBottomSheet_network_failure_support_email">Signal Android Backup export network error</string>