From db899e92f513b5f37009582c7bd767447b360c62 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 1 Jul 2026 13:11:57 -0400 Subject: [PATCH] Add contact support capability to regV5. --- .../securesms/ApplicationContext.java | 7 ++++++ .../securesms/util/SupportEmailUtil.java | 23 +++++++++++++------ .../sample/RegistrationApplication.kt | 6 +++++ .../registration/RegistrationDependencies.kt | 5 +++- .../PinEntryForRegistrationLockViewModel.kt | 3 +-- .../pinentry/PinEntryForSmsBypassViewModel.kt | 3 +-- .../PinEntryForSvrRestoreViewModel.kt | 3 +-- .../screens/pinentry/PinEntryScreen.kt | 18 ++++++++++++--- .../screens/pinentry/PinEntryScreenEvents.kt | 1 - .../RemoteBackupRestoreScreen.kt | 9 +++++++- .../src/main/res/values/strings.xml | 4 ++++ 11 files changed, 63 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 38d61bbf92..ca849ec12b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -119,6 +119,7 @@ import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.AppStartup; import org.thoughtcrime.securesms.util.BatterySnapshotTracker; +import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.DeviceProperties; import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.Environment; @@ -127,6 +128,7 @@ import org.thoughtcrime.securesms.util.RemoteConfig; import org.thoughtcrime.securesms.util.SignalLocalMetrics; import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler; import org.thoughtcrime.securesms.util.SqlCipherLogTarget; +import org.thoughtcrime.securesms.util.SupportEmailUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.VersionTracker; import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper; @@ -438,6 +440,11 @@ public class ApplicationContext extends Application implements AppForegroundObse context -> { context.startActivity(AppSettingsActivity.proxy(context)); return Unit.INSTANCE; + }, + (context, subject) -> { + String body = SupportEmailUtil.generateSupportEmailBody(context, subject, null, null); + CommunicationActions.openEmail(context, SupportEmailUtil.getSupportEmailAddress(context), subject, body); + return Unit.INSTANCE; } ) ); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SupportEmailUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/SupportEmailUtil.java index 7a5daab42a..8387d606a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SupportEmailUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SupportEmailUtil.java @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.util; import android.content.Context; -import android.content.res.Resources; import android.os.Build; import androidx.annotation.NonNull; @@ -48,18 +47,28 @@ public final class SupportEmailUtil { @Nullable String suffix) { filterSuffix = Util.emptyIfNull(filterSuffix); - prefix = Util.emptyIfNull(prefix); - suffix = Util.emptyIfNull(suffix); - return String.format("%s\n%s\n%s", prefix, buildSystemInfo(context, filter, filterSuffix), suffix); + return generateSupportEmailBody(context, ResourceUtil.getEnglishResources(context).getString(filter) + filterSuffix, prefix, suffix); } - private static @NonNull String buildSystemInfo(@NonNull Context context, @StringRes int filter, @NonNull String filterSuffix) { - Resources englishResources = ResourceUtil.getEnglishResources(context); + /** + * Generates a support email body with system info near the top, using the given already-resolved filter text. + */ + public static @NonNull String generateSupportEmailBody(@NonNull Context context, + @NonNull String filter, + @Nullable String prefix, + @Nullable String suffix) + { + prefix = Util.emptyIfNull(prefix); + suffix = Util.emptyIfNull(suffix); + return String.format("%s\n%s\n%s", prefix, buildSystemInfo(context, filter), suffix); + } + + private static @NonNull String buildSystemInfo(@NonNull Context context, @NonNull String filter) { return "--- " + context.getString(R.string.HelpFragment__support_info) + " ---" + "\n" + - context.getString(R.string.SupportEmailUtil_filter) + " " + englishResources.getString(filter) + filterSuffix + + context.getString(R.string.SupportEmailUtil_filter) + " " + filter + "\n" + context.getString(R.string.SupportEmailUtil_device_info) + " " + getDeviceInfo() + "\n" + diff --git a/demo/registration/src/main/java/org/signal/registration/sample/RegistrationApplication.kt b/demo/registration/src/main/java/org/signal/registration/sample/RegistrationApplication.kt index 3e17a06365..5becd20654 100644 --- a/demo/registration/src/main/java/org/signal/registration/sample/RegistrationApplication.kt +++ b/demo/registration/src/main/java/org/signal/registration/sample/RegistrationApplication.kt @@ -69,6 +69,12 @@ class RegistrationApplication : Application() { .setPositiveButton(android.R.string.ok, null) .show() }, + contactSupportCallback = { context, subject -> + MaterialAlertDialogBuilder(context) + .setMessage("Contact support not supported in the demo. Subject: $subject") + .setPositiveButton(android.R.string.ok, null) + .show() + }, isLinkAndSyncAvailable = true ) ) diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationDependencies.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationDependencies.kt index 46248a4517..37ba24ca6d 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationDependencies.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationDependencies.kt @@ -16,6 +16,8 @@ import org.signal.registration.util.SensitiveLog * the actual app would just pass null. * @param debugLogCallback Callback to launch the debug log viewer. The actual app provides the real implementation. * @param proxyConfigCallback Callback to launch the proxy configuration settings. The actual app provides the real implementation. + * @param contactSupportCallback Callback to let the user contact support, using the provided email subject. The actual app provides the real + * implementation. */ class RegistrationDependencies( val networkController: NetworkController, @@ -23,7 +25,8 @@ class RegistrationDependencies( val isLinkAndSyncAvailable: Boolean, val sensitiveLogger: Log.Logger?, val debugLogCallback: ((Context) -> Unit)?, - val proxyConfigCallback: ((Context) -> Unit)? + val proxyConfigCallback: ((Context) -> Unit)?, + val contactSupportCallback: ((Context, subject: String) -> Unit)? ) { companion object { lateinit var dependencies: RegistrationDependencies diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt index 0be254815a..d7e071f3d1 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt @@ -70,8 +70,7 @@ class PinEntryForRegistrationLockViewModel( } is PinEntryScreenEvents.CreateNewPin, is PinEntryScreenEvents.ContactSupport -> Unit - is PinEntryScreenEvents.ToggleKeyboard, - is PinEntryScreenEvents.NeedHelp -> { + is PinEntryScreenEvents.ToggleKeyboard -> { stateEmitter(PinEntryScreenEventHandler.applyEvent(state, event)) } } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModel.kt index 2f7e5bdce7..b29a871995 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModel.kt @@ -77,8 +77,7 @@ class PinEntryForSmsBypassViewModel( } is PinEntryScreenEvents.CreateNewPin, is PinEntryScreenEvents.ContactSupport -> Unit - is PinEntryScreenEvents.ToggleKeyboard, - is PinEntryScreenEvents.NeedHelp -> { + is PinEntryScreenEvents.ToggleKeyboard -> { stateEmitter(PinEntryScreenEventHandler.applyEvent(state, event)) } } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt index 94f13f1d55..a090e65108 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt @@ -80,8 +80,7 @@ class PinEntryForSvrRestoreViewModel( Log.i(TAG, "[ContactSupport] User opted to contact support after no data was found.") stateEmitter(state.copy(showNoDataToRestoreDialog = false)) } - is PinEntryScreenEvents.ToggleKeyboard, - is PinEntryScreenEvents.NeedHelp -> { + is PinEntryScreenEvents.ToggleKeyboard -> { stateEmitter(PinEntryScreenEventHandler.applyEvent(state, event)) } } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreen.kt index ffb7b97438..f49ea99663 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreen.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -52,6 +53,7 @@ import org.signal.core.ui.compose.Dialogs import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.SignalIcons import org.signal.registration.R +import org.signal.registration.RegistrationDependencies import org.signal.registration.screens.PinVisualTransformation import org.signal.registration.screens.RegistrationScaffold import org.signal.registration.screens.TwoPaneRegistrationScaffold @@ -67,10 +69,13 @@ fun PinEntryScreen( onEvent: (PinEntryScreenEvents) -> Unit, modifier: Modifier = Modifier ) { + val context = LocalContext.current var pin by rememberSaveable { mutableStateOf("") } var showSkipDialog by rememberSaveable { mutableStateOf(false) } val focusRequester = remember { FocusRequester() } val canSubmitPin = pin.isNotEmpty() + val supportEmailSubject = stringResource(R.string.PinEntryScreen__contact_support_email_subject) + val onContactSupport: () -> Unit = { RegistrationDependencies.get().contactSupportCallback?.invoke(context, supportEmailSubject) } when (val params = RegistrationScaffold.rememberLayoutParams()) { is RegistrationScaffold.Params.OnePane -> OnePaneLayout( @@ -81,6 +86,7 @@ fun PinEntryScreen( focusRequester = focusRequester, onPinChanged = { pin = it }, onSkip = { showSkipDialog = true }, + onContactSupport = onContactSupport, onEvent = onEvent, modifier = modifier ) @@ -93,6 +99,7 @@ fun PinEntryScreen( focusRequester = focusRequester, onPinChanged = { pin = it }, onSkip = { showSkipDialog = true }, + onContactSupport = onContactSupport, onEvent = onEvent, modifier = modifier ) @@ -119,7 +126,10 @@ fun PinEntryScreen( confirm = stringResource(R.string.PinEntryScreen__create_new_pin), dismiss = stringResource(R.string.PinEntryScreen__contact_support), onConfirm = { onEvent(PinEntryScreenEvents.CreateNewPin) }, - onDeny = { onEvent(PinEntryScreenEvents.ContactSupport) }, + onDeny = { + onContactSupport() + onEvent(PinEntryScreenEvents.ContactSupport) + }, onDismissRequest = { onEvent(PinEntryScreenEvents.ContactSupport) }, properties = DialogProperties( dismissOnBackPress = false, @@ -143,6 +153,7 @@ private fun OnePaneLayout( focusRequester: FocusRequester, onPinChanged: (String) -> Unit, onSkip: () -> Unit, + onContactSupport: () -> Unit, onEvent: (PinEntryScreenEvents) -> Unit, modifier: Modifier = Modifier ) { @@ -174,7 +185,7 @@ private fun OnePaneLayout( focusRequester = focusRequester, onPinChanged = onPinChanged, onSubmit = { onEvent(PinEntryScreenEvents.PinEntered(pin)) }, - onNeedsHelp = { onEvent(PinEntryScreenEvents.NeedHelp) }, + onNeedsHelp = onContactSupport, modifier = Modifier.fillMaxWidth() ) @@ -214,6 +225,7 @@ private fun TwoPaneLayout( focusRequester: FocusRequester, onPinChanged: (String) -> Unit, onSkip: () -> Unit, + onContactSupport: () -> Unit, onEvent: (PinEntryScreenEvents) -> Unit, modifier: Modifier = Modifier ) { @@ -255,7 +267,7 @@ private fun TwoPaneLayout( focusRequester = focusRequester, onPinChanged = onPinChanged, onSubmit = { onEvent(PinEntryScreenEvents.PinEntered(pin)) }, - onNeedsHelp = { onEvent(PinEntryScreenEvents.NeedHelp) }, + onNeedsHelp = onContactSupport, modifier = Modifier.fillMaxWidth() ) diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreenEvents.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreenEvents.kt index 6957617ba1..850ad8a529 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreenEvents.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreenEvents.kt @@ -10,7 +10,6 @@ sealed class PinEntryScreenEvents { override fun toString(): String = "PinEntered(pin=${pin.length} chars)" } data object ToggleKeyboard : PinEntryScreenEvents() - data object NeedHelp : PinEntryScreenEvents() data object Skip : PinEntryScreenEvents() data object CreateNewPin : PinEntryScreenEvents() data object ContactSupport : PinEntryScreenEvents() diff --git a/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreScreen.kt index 186c65833c..4e6f75b1cd 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreScreen.kt @@ -46,6 +46,7 @@ import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.SignalIcons import org.signal.core.ui.compose.theme.SignalTheme import org.signal.registration.R +import org.signal.registration.RegistrationDependencies import org.signal.registration.screens.OnePaneRegistrationScaffold import org.signal.registration.screens.RegistrationScaffold import org.signal.registration.screens.TwoPaneRegistrationScaffold @@ -296,6 +297,9 @@ private fun RestoreStateDialogs( state: RemoteBackupRestoreState, onEvent: (RemoteBackupRestoreScreenEvents) -> Unit ) { + val context = LocalContext.current + val contactSupportEmailSubject = stringResource(R.string.RemoteRestoreScreen__contact_support_email_subject) + when (state.restoreState) { RemoteBackupRestoreState.RestoreState.None -> Unit RemoteBackupRestoreState.RestoreState.InProgress -> { @@ -330,7 +334,10 @@ private fun RestoreStateDialogs( body = stringResource(R.string.RemoteRestoreScreen__your_backup_is_not_recoverable), confirm = stringResource(R.string.RemoteRestoreScreen__contact_support), dismiss = stringResource(android.R.string.ok), - onConfirm = { onEvent(RemoteBackupRestoreScreenEvents.DismissError) }, + onConfirm = { + RegistrationDependencies.get().contactSupportCallback?.invoke(context, contactSupportEmailSubject) + onEvent(RemoteBackupRestoreScreenEvents.DismissError) + }, onDismiss = { onEvent(RemoteBackupRestoreScreenEvents.DismissError) } ) } diff --git a/feature/registration/src/main/res/values/strings.xml b/feature/registration/src/main/res/values/strings.xml index e0a5383497..0f4c508c03 100644 --- a/feature/registration/src/main/res/values/strings.xml +++ b/feature/registration/src/main/res/values/strings.xml @@ -270,6 +270,8 @@ An error occurred while restoring your backup. Your backup is not recoverable. Please contact support for help. Contact support + + Signal Android Backup restore permanent failure Downloading backup… @@ -379,6 +381,8 @@ No data could be found to restore your account. Please create a new PIN. Contact support + + Signal Registration - Need Help with PIN for Android Allow Notifications