Add a 'having trouble' support bottom sheet in regV5.

This commit is contained in:
Greyson Parrelli
2026-07-07 15:47:47 -04:00
parent d695cd8b5a
commit 46006cbfde
6 changed files with 169 additions and 2 deletions
@@ -0,0 +1,143 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.screens.verificationcode
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.dismissWithAnimation
import org.signal.core.util.LinkActions
import org.signal.registration.R
import org.signal.registration.RegistrationDependencies
/**
* Bottom sheet shown during registration when the user is having trouble entering their verification code. Offers
* troubleshooting steps and a way to contact support, mirroring the old app-module ContactSupportBottomSheetFragment.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ContactSupportBottomSheet(onDismiss: () -> Unit) {
val sheetState = rememberModalBottomSheetState()
val scope = rememberCoroutineScope()
val context = LocalContext.current
val supportEmailSubject = stringResource(R.string.VerificationCodeScreen__contact_support_email_subject)
val supportCenterUrl = stringResource(R.string.VerificationCodeScreen__support_center_url)
BottomSheets.BottomSheet(
onDismissRequest = { sheetState.dismissWithAnimation(scope, onComplete = onDismiss) },
sheetState = sheetState
) {
ContactSupportBottomSheetContent(
onTroubleshootingStepsClick = {
LinkActions.openUrl(context, supportCenterUrl) {
Toast.makeText(context, R.string.LinkActions_error_no_browser_found, Toast.LENGTH_SHORT).show()
}
},
onContactSupportClick = {
RegistrationDependencies.get().contactSupportCallback?.invoke(context, supportEmailSubject)
}
)
}
}
@Composable
private fun ContactSupportBottomSheetContent(
onTroubleshootingStepsClick: () -> Unit,
onContactSupportClick: () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = buildAnnotatedString {
withStyle(SpanStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface)) {
append(stringResource(R.string.VerificationCodeScreen__support_bottom_sheet_title))
}
},
modifier = Modifier.padding(8.dp)
)
Text(
text = stringResource(R.string.VerificationCodeScreen__support_bottom_sheet_body_suggestions),
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(8.dp)
)
Text(
text = buildCallToActionString(onTroubleshootingStepsClick, onContactSupportClick),
modifier = Modifier.padding(8.dp)
)
}
}
@Composable
private fun buildCallToActionString(
onTroubleshootingStepsClick: () -> Unit,
onContactSupportClick: () -> Unit
) = buildAnnotatedString {
val troubleshootingStepsString = stringResource(R.string.VerificationCodeScreen__support_bottom_sheet_cta_troubleshooting_steps_substring)
val contactSupportString = stringResource(R.string.VerificationCodeScreen__support_bottom_sheet_cta_contact_support_substring)
val completeString = stringResource(R.string.VerificationCodeScreen__support_bottom_sheet_body_call_to_action, troubleshootingStepsString, contactSupportString)
val troubleshootingStartIndex = completeString.indexOf(troubleshootingStepsString)
val troubleshootingEndIndex = troubleshootingStartIndex + troubleshootingStepsString.length
val contactSupportStartIndex = completeString.indexOf(contactSupportString)
val contactSupportEndIndex = contactSupportStartIndex + contactSupportString.length
val bodyStyle = SpanStyle(color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Normal)
val linkStyles = TextLinkStyles(style = SpanStyle(color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold))
withStyle(bodyStyle) {
append(completeString.substring(0, troubleshootingStartIndex))
}
withLink(LinkAnnotation.Clickable(tag = "troubleshooting", styles = linkStyles) { onTroubleshootingStepsClick() }) {
append(troubleshootingStepsString)
}
withStyle(bodyStyle) {
append(completeString.substring(troubleshootingEndIndex, contactSupportStartIndex))
}
withLink(LinkAnnotation.Clickable(tag = "contact_support", styles = linkStyles) { onContactSupportClick() }) {
append(contactSupportString)
}
withStyle(bodyStyle) {
append(completeString.substring(contactSupportEndIndex))
}
}
@Preview
@Composable
private fun ContactSupportBottomSheetPreview() {
Previews.BottomSheetPreview {
ContactSupportBottomSheetContent(
onTroubleshootingStepsClick = {},
onContactSupportClick = {}
)
}
}
@@ -137,6 +137,12 @@ fun VerificationCodeScreen(
onEvent(VerificationCodeScreenEvents.Foregrounded)
}
if (state.showContactSupportSheet) {
ContactSupportBottomSheet(
onDismiss = { onEvent(VerificationCodeScreenEvents.DismissContactSupport) }
)
}
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
modifier = modifier
@@ -39,6 +39,8 @@ sealed class VerificationCodeScreenEvents {
data object HavingTrouble : VerificationCodeScreenEvents()
data object DismissContactSupport : VerificationCodeScreenEvents()
data object ConsumeInnerOneTimeEvent : VerificationCodeScreenEvents()
/**
@@ -18,9 +18,10 @@ data class VerificationCodeState(
val autoFillCode: String? = null,
val digits: List<String> = List(CODE_LENGTH) { "" },
val focusedDigitIndex: Int = 0,
val showContactSupportSheet: Boolean = false,
val oneTimeEvent: OneTimeEvent? = null
) {
override fun toString(): String = "VerificationCodeState(sessionMetadata=${sessionMetadata?.let { "present" }}, e164=$e164, isSubmittingCode=$isSubmittingCode, rateLimits=$rateLimits, incorrectCodeAttempts=$incorrectCodeAttempts, autoFillCode=${autoFillCode?.let { "present" }}, digitsEntered=${digits.count { it.isNotEmpty() }}, focusedDigitIndex=$focusedDigitIndex, oneTimeEvent=$oneTimeEvent)"
override fun toString(): String = "VerificationCodeState(sessionMetadata=${sessionMetadata?.let { "present" }}, e164=$e164, isSubmittingCode=$isSubmittingCode, rateLimits=$rateLimits, incorrectCodeAttempts=$incorrectCodeAttempts, autoFillCode=${autoFillCode?.let { "present" }}, digitsEntered=${digits.count { it.isNotEmpty() }}, focusedDigitIndex=$focusedDigitIndex, showContactSupportSheet=$showContactSupportSheet, oneTimeEvent=$oneTimeEvent)"
/**
* The full code as currently entered. Only meaningful when [isComplete] is true.
@@ -127,7 +127,8 @@ class VerificationCodeViewModel(
is VerificationCodeScreenEvents.WrongNumber -> state.also { parentEventEmitter.navigateTo(RegistrationRoute.PhoneNumberEntry) }
is VerificationCodeScreenEvents.ResendSms -> applyResendCode(state, NetworkController.VerificationCodeTransport.SMS)
is VerificationCodeScreenEvents.CallMe -> applyResendCode(state, NetworkController.VerificationCodeTransport.VOICE)
is VerificationCodeScreenEvents.HavingTrouble -> throw NotImplementedError("having trouble flow") // TODO [registration] - Having trouble flow
is VerificationCodeScreenEvents.HavingTrouble -> state.copy(showContactSupportSheet = true)
is VerificationCodeScreenEvents.DismissContactSupport -> state.copy(showContactSupportSheet = false)
is VerificationCodeScreenEvents.ConsumeInnerOneTimeEvent -> state.copy(oneTimeEvent = null)
is VerificationCodeScreenEvents.CountdownTick -> applyCountdownTick(state)
is VerificationCodeScreenEvents.Foregrounded -> applyForegrounded(state)
@@ -101,6 +101,20 @@
<string name="VerificationCodeScreen__registration_error">Registration failed. Please try again.</string>
<!-- Button text for having trouble with verification -->
<string name="VerificationCodeScreen__having_trouble">Having trouble?</string>
<!-- Title of the bottom sheet shown when the user is having trouble entering their verification code -->
<string name="VerificationCodeScreen__support_bottom_sheet_title">Having trouble registering?</string>
<!-- Bulleted list of suggestions shown in the having trouble bottom sheet -->
<string name="VerificationCodeScreen__support_bottom_sheet_body_suggestions">• Make sure your phone has a cellular signal to receive your SMS or call\n • Confirm you can receive a phone call to the number\n • Check that you have entered your phone number correctly.</string>
<!-- A call to action for a user having trouble entering the verification code to seek further help. The first placeholder is VerificationCodeScreen__support_bottom_sheet_cta_troubleshooting_steps_substring, the second is VerificationCodeScreen__support_bottom_sheet_cta_contact_support_substring -->
<string name="VerificationCodeScreen__support_bottom_sheet_body_call_to_action">For more information, please follow %1$s or %2$s</string>
<!-- Clickable substring linking to troubleshooting steps within VerificationCodeScreen__support_bottom_sheet_body_call_to_action -->
<string name="VerificationCodeScreen__support_bottom_sheet_cta_troubleshooting_steps_substring">these troubleshooting steps</string>
<!-- Clickable substring linking to contacting support within VerificationCodeScreen__support_bottom_sheet_body_call_to_action -->
<string name="VerificationCodeScreen__support_bottom_sheet_cta_contact_support_substring">Contact Support</string>
<!-- Email subject used when contacting support from the verification code screen -->
<string name="VerificationCodeScreen__contact_support_email_subject" translatable="false">Signal Registration - Verification Code for Android</string>
<!-- URL opened for troubleshooting steps from the verification code screen -->
<string name="VerificationCodeScreen__support_center_url" translatable="false">https://support.signal.org/</string>
<!-- RestoreWelcomeBottomSheet -->
<!-- Row title for restore/transfer using a previous registered phone/device -->