Remove the unused profile screen.

This commit is contained in:
Greyson Parrelli
2026-09-30 13:34:03 -03:00
committed by Alex Hart
parent b87b12ad97
commit aed11c8a1d
35 changed files with 0 additions and 1438 deletions
@@ -82,9 +82,6 @@ import org.signal.registration.screens.countrycode.Country
import org.signal.registration.screens.countrycode.CountryCodePickerRepository
import org.signal.registration.screens.countrycode.CountryCodePickerScreen
import org.signal.registration.screens.countrycode.CountryCodePickerViewModel
import org.signal.registration.screens.createprofile.CreateProfileScreen
import org.signal.registration.screens.createprofile.CreateProfileScreenEvents
import org.signal.registration.screens.createprofile.CreateProfileViewModel
import org.signal.registration.screens.devicetransfer.complete.DeviceTransferCompleteScreen
import org.signal.registration.screens.devicetransfer.complete.DeviceTransferCompleteViewModel
import org.signal.registration.screens.devicetransfer.instructions.DeviceTransferInstructionsScreen
@@ -93,8 +90,6 @@ import org.signal.registration.screens.devicetransfer.progress.DeviceTransferPro
import org.signal.registration.screens.devicetransfer.progress.DeviceTransferProgressViewModel
import org.signal.registration.screens.devicetransfer.setup.DeviceTransferSetupScreen
import org.signal.registration.screens.devicetransfer.setup.DeviceTransferSetupViewModel
import org.signal.registration.screens.discoverability.PhoneNumberDiscoverabilityScreen
import org.signal.registration.screens.discoverability.PhoneNumberDiscoverabilityViewModel
import org.signal.registration.screens.linkaccount.LinkAccountScreen
import org.signal.registration.screens.linkaccount.LinkAccountScreenAction
import org.signal.registration.screens.linkaccount.LinkAccountViewModel
@@ -394,12 +389,6 @@ sealed interface RegistrationRoute : NavKey, Parcelable {
@Serializable
data object DeviceTransferComplete : RegistrationRoute
@Serializable
data object Profile : RegistrationRoute
@Serializable
data class PhoneNumberDiscoverability(val initialDiscoverable: Boolean) : RegistrationRoute
@Serializable
data object FullyComplete : RegistrationRoute
}
@@ -409,7 +398,6 @@ private const val COUNTRY_CODE_RESULT = "country_code_result"
private const val BACKUP_CREDENTIAL_RESULT = "backup_credential_result"
private const val AEP_FOR_LOCAL_BACKUP_RESULT = "aep_for_local_backup_result"
private const val LOCAL_BACKUP_RESTORE_RESULT = "local_backup_restore_result"
private const val PHONE_NUMBER_DISCOVERABILITY_RESULT = "phone_number_discoverability_result"
private const val TWO_FACTOR_CODE_RESULT = "two_factor_code_result"
/** Opens [url] in a browser, surfacing a toast if the device has none. */
@@ -1321,42 +1309,6 @@ private fun EntryProviderScope<NavKey>.navigationEntries(
)
}
entry<RegistrationRoute.Profile> {
val viewModel: CreateProfileViewModel = viewModel {
CreateProfileViewModel(
repository = registrationRepository,
parentEventEmitter = registrationViewModel::onEvent
)
}
val state by viewModel.state.collectAsStateWithLifecycle()
ResultEffect<Boolean>(registrationViewModel.resultBus, PHONE_NUMBER_DISCOVERABILITY_RESULT) { discoverable ->
viewModel.onEvent(CreateProfileScreenEvents.DiscoverabilityChanged(discoverable))
}
CreateProfileScreen(
state = state,
onEvent = viewModel::onEvent
)
}
entry<RegistrationRoute.PhoneNumberDiscoverability> { key ->
val viewModel: PhoneNumberDiscoverabilityViewModel = viewModel {
PhoneNumberDiscoverabilityViewModel(
initialDiscoverable = key.initialDiscoverable,
parentEventEmitter = registrationViewModel::onEvent,
resultBus = registrationViewModel.resultBus,
resultKey = PHONE_NUMBER_DISCOVERABILITY_RESULT
)
}
val state by viewModel.state.collectAsStateWithLifecycle()
PhoneNumberDiscoverabilityScreen(
state = state,
onEvent = viewModel::onEvent
)
}
entry<RegistrationRoute.FullyComplete> {
LaunchedEffect(Unit) {
onRegistrationComplete()
@@ -1,419 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.screens.createprofile
import android.graphics.BitmapFactory
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.signal.core.ui.WindowBreakpoint
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.TextFields
import org.signal.core.ui.rememberWindowBreakpoint
import org.signal.registration.R
import org.signal.registration.screens.RegistrationScaffold
import org.signal.registration.test.TestTags
/**
* Profile creation screen for the registration flow. Captures the user's given name, family name,
* avatar, and phone-number discoverability before completing registration.
*
* Dispatches to a per-[WindowBreakpoint] layout following the pattern in `WelcomeScreen`. All three
* breakpoints currently share the [CompactLayout] body — Medium/Large variants can be split out
* later without changing this entry point.
*/
@Composable
fun CreateProfileScreen(
state: CreateProfileState,
onEvent: (CreateProfileScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
val pickAvatarLauncher = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
if (uri != null) {
val bytes = runCatching {
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
}.getOrNull()
if (bytes != null) {
onEvent(CreateProfileScreenEvents.AvatarSelected(bytes))
}
}
}
val onAvatarClick = {
pickAvatarLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
if (state.showUploadFailedDialog) {
Dialogs.SimpleMessageDialog(
message = stringResource(R.string.VerificationCodeScreen__an_unexpected_error_occurred),
dismiss = stringResource(android.R.string.ok),
onDismiss = { onEvent(CreateProfileScreenEvents.UploadFailedDialogDismissed) }
)
}
if (state.isLoading) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
return
}
when (rememberWindowBreakpoint()) {
is WindowBreakpoint.Small -> CompactLayout(state, onEvent, onAvatarClick, modifier)
is WindowBreakpoint.Medium -> MediumLayout(state, onEvent, onAvatarClick, modifier)
is WindowBreakpoint.Large -> LargeLayout(state, onEvent, onAvatarClick, modifier)
}
}
@Composable
private fun CompactLayout(
state: CreateProfileState,
onEvent: (CreateProfileScreenEvents) -> Unit,
onAvatarClick: () -> Unit,
modifier: Modifier = Modifier
) {
val givenNameInteractionSource = remember { MutableInteractionSource() }
val familyNameInteractionSource = remember { MutableInteractionSource() }
RegistrationScaffold(
modifier = modifier
.fillMaxSize()
.testTag(TestTags.CREATE_PROFILE_SCREEN),
content = {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.CreateProfileScreen__set_up_your_profile),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = stringResource(R.string.CreateProfileScreen__your_profile_is_end_to_end_encrypted),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(32.dp))
Avatar(avatarBytes = state.avatar, onClick = onAvatarClick)
Spacer(modifier = Modifier.height(32.dp))
OutlinedTextField(
value = state.givenName,
onValueChange = { onEvent(CreateProfileScreenEvents.GivenNameChanged(it)) },
label = { TextFields.Label(stringResource(R.string.CreateProfileScreen__first_name_required), state.givenName.isNotEmpty(), givenNameInteractionSource) },
interactionSource = givenNameInteractionSource,
singleLine = true,
enabled = !state.isSubmitting,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Words,
imeAction = ImeAction.Next
),
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.CREATE_PROFILE_GIVEN_NAME_FIELD)
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = state.familyName,
onValueChange = { onEvent(CreateProfileScreenEvents.FamilyNameChanged(it)) },
label = { TextFields.Label(stringResource(R.string.CreateProfileScreen__last_name_optional), state.familyName.isNotEmpty(), familyNameInteractionSource) },
interactionSource = familyNameInteractionSource,
singleLine = true,
enabled = !state.isSubmitting,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Words,
imeAction = ImeAction.Done
),
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.CREATE_PROFILE_FAMILY_NAME_FIELD)
)
Spacer(modifier = Modifier.height(16.dp))
WhoCanFindMeRow(
discoverable = state.discoverableByPhoneNumber,
enabled = !state.isSubmitting,
onClick = { onEvent(CreateProfileScreenEvents.WhoCanFindMeClicked) }
)
}
},
footer = {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp),
contentAlignment = Alignment.Center
) {
Buttons.LargeTonal(
onClick = { onEvent(CreateProfileScreenEvents.NextClicked) },
enabled = state.isFormValid && !state.isSubmitting,
modifier = Modifier
.fillMaxWidth()
.widthIn(max = 320.dp)
.testTag(TestTags.CREATE_PROFILE_NEXT_BUTTON)
) {
if (state.isSubmitting) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.onSecondaryContainer,
strokeWidth = 2.dp,
modifier = Modifier.size(20.dp)
)
} else {
Text(stringResource(R.string.CreateProfileScreen__next))
}
}
}
}
)
}
@Composable
private fun MediumLayout(
state: CreateProfileState,
onEvent: (CreateProfileScreenEvents) -> Unit,
onAvatarClick: () -> Unit,
modifier: Modifier = Modifier
) {
// TODO [registration] dedicated medium-width layout. For now, reuse the compact body.
CompactLayout(state = state, onEvent = onEvent, onAvatarClick = onAvatarClick, modifier = modifier)
}
@Composable
private fun LargeLayout(
state: CreateProfileState,
onEvent: (CreateProfileScreenEvents) -> Unit,
onAvatarClick: () -> Unit,
modifier: Modifier = Modifier
) {
// TODO [registration] dedicated large-width layout. For now, reuse the compact body.
CompactLayout(state = state, onEvent = onEvent, onAvatarClick = onAvatarClick, modifier = modifier)
}
@Composable
private fun WhoCanFindMeRow(
discoverable: Boolean,
enabled: Boolean,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = enabled, onClick = onClick)
.padding(vertical = 12.dp)
.testTag(TestTags.CREATE_PROFILE_WHO_CAN_FIND_ME_ROW),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = if (discoverable) painterResource(R.drawable.symbol_group_24) else SignalIcons.Lock.painter,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.padding(horizontal = 8.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(R.string.WhoCanSeeMyPhoneNumberFragment__who_can_find_me_by_number),
style = MaterialTheme.typography.bodyLarge
)
Text(
text = stringResource(
if (discoverable) R.string.PhoneNumberPrivacy_everyone else R.string.PhoneNumberPrivacy_nobody
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Icon(
painter = SignalIcons.ChevronRight.painter,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Composable
private fun Avatar(
avatarBytes: ByteArray?,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val bitmap = remember(avatarBytes) {
avatarBytes?.let {
runCatching { BitmapFactory.decodeByteArray(it, 0, it.size) }.getOrNull()
}
}
Box(modifier = modifier.size(112.dp)) {
Box(
modifier = Modifier
.size(112.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center
) {
if (bitmap != null) {
androidx.compose.foundation.Image(
bitmap = bitmap.asImageBitmap(),
contentDescription = stringResource(R.string.CreateProfileScreen__set_avatar_description),
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
} else {
Icon(
painter = SignalIcons.Camera.painter,
contentDescription = stringResource(R.string.CreateProfileScreen__set_avatar_description),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(40.dp)
)
}
}
Box(
modifier = Modifier
.align(Alignment.BottomEnd)
.offset(x = 4.dp, y = 4.dp)
.size(36.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center
) {
Icon(
painter = SignalIcons.Camera.painter,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(20.dp)
)
}
}
}
@AllDevicePreviews
@Composable
private fun CreateProfileScreenLoadingPreview() {
Previews.Preview {
CreateProfileScreen(
state = CreateProfileState(isLoading = true),
onEvent = {}
)
}
}
@AllDevicePreviews
@Composable
private fun CreateProfileScreenEmptyPreview() {
Previews.Preview {
CreateProfileScreen(
state = CreateProfileState(isLoading = false),
onEvent = {}
)
}
}
@AllDevicePreviews
@Composable
private fun CreateProfileScreenWithNamePreview() {
Previews.Preview {
CreateProfileScreen(
state = CreateProfileState(
givenName = "Alice",
familyName = "Anderson",
isLoading = false
),
onEvent = {}
)
}
}
@AllDevicePreviews
@Composable
private fun CreateProfileScreenNobodyPreview() {
Previews.Preview {
CreateProfileScreen(
state = CreateProfileState(
givenName = "Alice",
familyName = "Anderson",
discoverableByPhoneNumber = false,
isLoading = false
),
onEvent = {}
)
}
}
@AllDevicePreviews
@Composable
private fun CreateProfileScreenSubmittingPreview() {
Previews.Preview {
CreateProfileScreen(
state = CreateProfileState(
givenName = "Alice",
familyName = "Anderson",
isLoading = false,
isSubmitting = true
),
onEvent = {}
)
}
}
@@ -1,29 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.screens.createprofile
sealed class CreateProfileScreenEvents {
data class GivenNameChanged(val value: String) : CreateProfileScreenEvents() {
override fun toString(): String = "GivenNameChanged(value=${value.length} chars)"
}
data class FamilyNameChanged(val value: String) : CreateProfileScreenEvents() {
override fun toString(): String = "FamilyNameChanged(value=${value.length} chars)"
}
data class AvatarSelected(val bytes: ByteArray) : CreateProfileScreenEvents() {
override fun toString(): String = "AvatarSelected(${bytes.size} bytes)"
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is AvatarSelected) return false
return bytes.contentEquals(other.bytes)
}
override fun hashCode(): Int = bytes.contentHashCode()
}
data object AvatarCleared : CreateProfileScreenEvents()
data object WhoCanFindMeClicked : CreateProfileScreenEvents()
data class DiscoverabilityChanged(val discoverable: Boolean) : CreateProfileScreenEvents()
data object NextClicked : CreateProfileScreenEvents()
data object UploadFailedDialogDismissed : CreateProfileScreenEvents()
}
@@ -1,53 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.screens.createprofile
data class CreateProfileState(
val givenName: String = "",
val familyName: String = "",
val avatar: ByteArray? = null,
val discoverableByPhoneNumber: Boolean = true,
val isLoading: Boolean = true,
val isSubmitting: Boolean = false,
val showUploadFailedDialog: Boolean = false
) {
val isFormValid: Boolean
get() = givenName.trim().isNotEmpty()
override fun toString(): String {
return "CreateProfileState(givenName=${givenName.length} chars, familyName=${familyName.length} chars, avatar=${avatar?.size ?: 0} bytes, discoverableByPhoneNumber=$discoverableByPhoneNumber, isLoading=$isLoading, isSubmitting=$isSubmitting, showUploadFailedDialog=$showUploadFailedDialog)"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is CreateProfileState) return false
if (givenName != other.givenName) return false
if (familyName != other.familyName) return false
if (avatar != null) {
if (other.avatar == null) return false
if (!avatar.contentEquals(other.avatar)) return false
} else if (other.avatar != null) {
return false
}
if (discoverableByPhoneNumber != other.discoverableByPhoneNumber) return false
if (isLoading != other.isLoading) return false
if (isSubmitting != other.isSubmitting) return false
if (showUploadFailedDialog != other.showUploadFailedDialog) return false
return true
}
override fun hashCode(): Int {
var result = givenName.hashCode()
result = 31 * result + familyName.hashCode()
result = 31 * result + (avatar?.contentHashCode() ?: 0)
result = 31 * result + discoverableByPhoneNumber.hashCode()
result = 31 * result + isLoading.hashCode()
result = 31 * result + isSubmitting.hashCode()
result = 31 * result + showUploadFailedDialog.hashCode()
return result
}
}
@@ -1,139 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.screens.createprofile
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.signal.core.ui.compose.EventDrivenViewModel
import org.signal.core.util.logging.Log
import org.signal.libsignal.net.RequestResult
import org.signal.registration.RegistrationFlowEvent
import org.signal.registration.RegistrationRepository
import org.signal.registration.RegistrationRoute
import org.signal.registration.screens.util.navigateTo
/**
* ViewModel for the registration profile-creation screen. Holds the user's typed name and selected
* avatar bytes and submits them to [RegistrationRepository.setProfile] when the user advances.
*/
class CreateProfileViewModel(
private val repository: RegistrationRepository,
private val parentEventEmitter: (RegistrationFlowEvent) -> Unit
) : EventDrivenViewModel<CreateProfileScreenEvents>(TAG, shouldLogEvents = true) {
companion object {
private val TAG = Log.tag(CreateProfileViewModel::class)
}
private val _state = MutableStateFlow(CreateProfileState())
val state: StateFlow<CreateProfileState> = _state
init {
viewModelScope.launch {
val stored = repository.getStoredProfileData()
Log.i(TAG, "[init] Loaded stored profile data. givenName=${stored.givenName.isNotEmpty()}, familyName=${stored.familyName.isNotEmpty()}, avatar=${stored.avatar != null}")
val seeded = _state.value.copy(
givenName = stored.givenName,
familyName = stored.familyName,
avatar = stored.avatar,
discoverableByPhoneNumber = stored.discoverableByPhoneNumber ?: true,
isLoading = false
)
if (stored.givenName.isNotEmpty() && stored.avatar != null) {
Log.i(TAG, "[init] Profile name + avatar already present. Auto-submitting and skipping screen.")
_state.value = seeded.copy(isSubmitting = true)
submitProfile(seeded)
} else {
_state.value = seeded
}
}
}
override suspend fun processEvent(event: CreateProfileScreenEvents) {
applyEvent(state.value, event, parentEventEmitter, repository) { _state.value = it }
}
@VisibleForTesting
suspend fun applyEvent(
state: CreateProfileState,
event: CreateProfileScreenEvents,
parentEventEmitter: (RegistrationFlowEvent) -> Unit,
repository: RegistrationRepository,
stateEmitter: (CreateProfileState) -> Unit
) {
when (event) {
is CreateProfileScreenEvents.GivenNameChanged -> {
stateEmitter(state.copy(givenName = event.value))
}
is CreateProfileScreenEvents.FamilyNameChanged -> {
stateEmitter(state.copy(familyName = event.value))
}
is CreateProfileScreenEvents.AvatarSelected -> {
stateEmitter(state.copy(avatar = event.bytes))
}
CreateProfileScreenEvents.AvatarCleared -> {
stateEmitter(state.copy(avatar = null))
}
CreateProfileScreenEvents.WhoCanFindMeClicked -> {
parentEventEmitter.navigateTo(RegistrationRoute.PhoneNumberDiscoverability(state.discoverableByPhoneNumber))
}
is CreateProfileScreenEvents.DiscoverabilityChanged -> {
stateEmitter(state.copy(discoverableByPhoneNumber = event.discoverable))
}
CreateProfileScreenEvents.NextClicked -> {
if (state.isSubmitting || !state.isFormValid) {
return
}
stateEmitter(state.copy(isSubmitting = true))
submitProfile(state, parentEventEmitter, repository, stateEmitter)
}
CreateProfileScreenEvents.UploadFailedDialogDismissed -> {
stateEmitter(state.copy(showUploadFailedDialog = false))
}
}
}
private suspend fun submitProfile(state: CreateProfileState) {
submitProfile(state, parentEventEmitter, repository) { _state.value = it }
}
private suspend fun submitProfile(
state: CreateProfileState,
parentEventEmitter: (RegistrationFlowEvent) -> Unit,
repository: RegistrationRepository,
stateEmitter: (CreateProfileState) -> Unit
) {
val result = repository.setProfile(
givenName = state.givenName.trim(),
familyName = state.familyName.trim(),
avatar = state.avatar,
discoverableByPhoneNumber = state.discoverableByPhoneNumber
)
when (result) {
is RequestResult.Success -> {
Log.i(TAG, "[submitProfile] Profile saved.")
parentEventEmitter(RegistrationFlowEvent.RegistrationComplete)
}
is RequestResult.NonSuccess -> {
Log.w(TAG, "[submitProfile] Profile save failed: ${result.error}")
stateEmitter(state.copy(isSubmitting = false, showUploadFailedDialog = true))
}
is RequestResult.RetryableNetworkError -> {
Log.w(TAG, "[submitProfile] Network error saving profile.", result.networkError)
stateEmitter(state.copy(isSubmitting = false, showUploadFailedDialog = true))
}
is RequestResult.ApplicationError -> {
Log.w(TAG, "[submitProfile] Application error saving profile.", result.cause)
stateEmitter(state.copy(isSubmitting = false, showUploadFailedDialog = true))
}
}
}
}
@@ -1,198 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.screens.discoverability
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalIcons
import org.signal.registration.R
import org.signal.registration.screens.RegistrationScaffold
import org.signal.registration.test.TestTags
@Composable
fun PhoneNumberDiscoverabilityScreen(
state: PhoneNumberDiscoverabilityState,
onEvent: (PhoneNumberDiscoverabilityScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
RegistrationScaffold(
modifier = modifier
.fillMaxSize()
.testTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_SCREEN),
topBar = {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = { onEvent(PhoneNumberDiscoverabilityScreenEvents.BackClicked) },
modifier = Modifier.testTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_BACK_BUTTON)
) {
Icon(
imageVector = SignalIcons.ArrowStart.imageVector,
contentDescription = stringResource(R.string.PhoneNumberDiscoverabilityScreen__back)
)
}
Spacer(modifier = Modifier.padding(horizontal = 4.dp))
Text(
text = stringResource(R.string.WhoCanSeeMyPhoneNumberFragment__who_can_find_me_by_number),
style = MaterialTheme.typography.titleLarge
)
}
},
content = {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 8.dp)
) {
DiscoverabilityOption(
label = stringResource(R.string.PhoneNumberPrivacy_everyone),
selected = state.discoverable,
onClick = { onEvent(PhoneNumberDiscoverabilityScreenEvents.EveryoneSelected) },
modifier = Modifier.testTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_EVERYONE_OPTION)
)
DiscoverabilityOption(
label = stringResource(R.string.PhoneNumberPrivacy_nobody),
selected = !state.discoverable,
onClick = { onEvent(PhoneNumberDiscoverabilityScreenEvents.NobodySelected) },
modifier = Modifier.testTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_NOBODY_OPTION)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(
if (state.discoverable) {
R.string.WhoCanSeeMyPhoneNumberFragment__anyone_who_has_your
} else {
R.string.WhoCanSeeMyPhoneNumberFragment__nobody_will_be_able
}
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp)
)
}
},
footer = {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp),
contentAlignment = Alignment.Center
) {
Buttons.LargeTonal(
onClick = { onEvent(PhoneNumberDiscoverabilityScreenEvents.SaveClicked) },
modifier = Modifier
.fillMaxWidth()
.widthIn(max = 320.dp)
.testTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_SAVE_BUTTON)
) {
Text(stringResource(R.string.PhoneNumberDiscoverabilityScreen__save))
}
}
}
)
if (state.showNobodyConfirmation) {
AlertDialog(
onDismissRequest = { onEvent(PhoneNumberDiscoverabilityScreenEvents.NobodyDismissed) },
title = { Text(stringResource(R.string.PhoneNumberPrivacySettingsFragment__nobody_can_find_me_warning_title)) },
text = { Text(stringResource(R.string.PhoneNumberPrivacySettingsFragment__nobody_can_find_me_warning_message)) },
dismissButton = {
TextButton(onClick = { onEvent(PhoneNumberDiscoverabilityScreenEvents.NobodyDismissed) }) {
Text(stringResource(R.string.PhoneNumberPrivacySettingsFragment__cancel))
}
},
confirmButton = {
TextButton(onClick = { onEvent(PhoneNumberDiscoverabilityScreenEvents.NobodyConfirmed) }) {
Text(stringResource(android.R.string.ok))
}
}
)
}
}
@Composable
private fun DiscoverabilityOption(
label: String,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
RadioButton(selected = selected, onClick = onClick)
Spacer(modifier = Modifier.padding(horizontal = 8.dp))
Text(text = label, style = MaterialTheme.typography.bodyLarge)
}
}
@AllDevicePreviews
@Composable
private fun PhoneNumberDiscoverabilityScreenEveryonePreview() {
Previews.Preview {
PhoneNumberDiscoverabilityScreen(
state = PhoneNumberDiscoverabilityState(discoverable = true),
onEvent = {}
)
}
}
@AllDevicePreviews
@Composable
private fun PhoneNumberDiscoverabilityScreenNobodyPreview() {
Previews.Preview {
PhoneNumberDiscoverabilityScreen(
state = PhoneNumberDiscoverabilityState(discoverable = false),
onEvent = {}
)
}
}
@AllDevicePreviews
@Composable
private fun PhoneNumberDiscoverabilityScreenConfirmPreview() {
Previews.Preview {
PhoneNumberDiscoverabilityScreen(
state = PhoneNumberDiscoverabilityState(discoverable = true, showNobodyConfirmation = true),
onEvent = {}
)
}
}
@@ -1,15 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.screens.discoverability
sealed class PhoneNumberDiscoverabilityScreenEvents {
data object EveryoneSelected : PhoneNumberDiscoverabilityScreenEvents()
data object NobodySelected : PhoneNumberDiscoverabilityScreenEvents()
data object NobodyConfirmed : PhoneNumberDiscoverabilityScreenEvents()
data object NobodyDismissed : PhoneNumberDiscoverabilityScreenEvents()
data object SaveClicked : PhoneNumberDiscoverabilityScreenEvents()
data object BackClicked : PhoneNumberDiscoverabilityScreenEvents()
}
@@ -1,11 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.screens.discoverability
data class PhoneNumberDiscoverabilityState(
val discoverable: Boolean = true,
val showNobodyConfirmation: Boolean = false
)
@@ -1,75 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.screens.discoverability
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.signal.core.ui.compose.EventDrivenViewModel
import org.signal.core.ui.navigation.ResultEventBus
import org.signal.core.util.logging.Log
import org.signal.registration.RegistrationFlowEvent
import org.signal.registration.screens.util.navigateBack
class PhoneNumberDiscoverabilityViewModel(
initialDiscoverable: Boolean,
private val parentEventEmitter: (RegistrationFlowEvent) -> Unit,
private val resultBus: ResultEventBus,
private val resultKey: String
) : EventDrivenViewModel<PhoneNumberDiscoverabilityScreenEvents>(TAG, shouldLogEvents = true) {
companion object {
private val TAG = Log.tag(PhoneNumberDiscoverabilityViewModel::class)
}
private val _state = MutableStateFlow(PhoneNumberDiscoverabilityState(discoverable = initialDiscoverable))
val state: StateFlow<PhoneNumberDiscoverabilityState> = _state
init {
_state
.onEach { Log.d(TAG, "[State] $it") }
.launchIn(viewModelScope)
}
override suspend fun processEvent(event: PhoneNumberDiscoverabilityScreenEvents) {
applyEvent(state.value, event, parentEventEmitter, resultBus, resultKey) { _state.value = it }
}
@VisibleForTesting
suspend fun applyEvent(
state: PhoneNumberDiscoverabilityState,
event: PhoneNumberDiscoverabilityScreenEvents,
parentEventEmitter: (RegistrationFlowEvent) -> Unit,
resultBus: ResultEventBus,
resultKey: String,
stateEmitter: (PhoneNumberDiscoverabilityState) -> Unit
) {
when (event) {
PhoneNumberDiscoverabilityScreenEvents.EveryoneSelected -> {
stateEmitter(state.copy(discoverable = true))
}
PhoneNumberDiscoverabilityScreenEvents.NobodySelected -> {
stateEmitter(state.copy(showNobodyConfirmation = true))
}
PhoneNumberDiscoverabilityScreenEvents.NobodyConfirmed -> {
stateEmitter(state.copy(discoverable = false, showNobodyConfirmation = false))
}
PhoneNumberDiscoverabilityScreenEvents.NobodyDismissed -> {
stateEmitter(state.copy(showNobodyConfirmation = false))
}
PhoneNumberDiscoverabilityScreenEvents.SaveClicked -> {
resultBus.sendResult(resultKey, state.discoverable)
parentEventEmitter.navigateBack()
}
PhoneNumberDiscoverabilityScreenEvents.BackClicked -> {
parentEventEmitter.navigateBack()
}
}
}
}
@@ -142,20 +142,6 @@ object TestTags {
const val COUNTRY_CODE_SEARCH_FIELD = "country_code_search_field"
const val COUNTRY_CODE_CLOSE_BUTTON = "country_code_close_button"
// Create Profile Screen
const val CREATE_PROFILE_SCREEN = "create_profile_screen"
const val CREATE_PROFILE_GIVEN_NAME_FIELD = "create_profile_given_name_field"
const val CREATE_PROFILE_FAMILY_NAME_FIELD = "create_profile_family_name_field"
const val CREATE_PROFILE_WHO_CAN_FIND_ME_ROW = "create_profile_who_can_find_me_row"
const val CREATE_PROFILE_NEXT_BUTTON = "create_profile_next_button"
// Phone Number Discoverability Screen
const val PHONE_NUMBER_DISCOVERABILITY_SCREEN = "phone_number_discoverability_screen"
const val PHONE_NUMBER_DISCOVERABILITY_EVERYONE_OPTION = "phone_number_discoverability_everyone_option"
const val PHONE_NUMBER_DISCOVERABILITY_NOBODY_OPTION = "phone_number_discoverability_nobody_option"
const val PHONE_NUMBER_DISCOVERABILITY_SAVE_BUTTON = "phone_number_discoverability_save_button"
const val PHONE_NUMBER_DISCOVERABILITY_BACK_BUTTON = "phone_number_discoverability_back_button"
// Device Transfer Complete Screen
const val DEVICE_TRANSFER_COMPLETE_SCREEN = "device_transfer_complete_screen"
const val DEVICE_TRANSFER_COMPLETE_CONTINUE_BUTTON = "device_transfer_complete_continue_button"
@@ -558,41 +558,6 @@
<string name="DeviceTransferComplete__your_account_is_now_on_this_device">Your account is now on this device.</string>
<string name="DeviceTransferComplete__continue_registration">Continue</string>
<!-- Create profile screen -->
<!-- Title displayed at the top of the create-profile screen during registration. -->
<string name="CreateProfileScreen__set_up_your_profile">Set up your profile</string>
<!-- Subtitle on create-profile screen explaining that the profile is end-to-end encrypted and visible to recipients. -->
<string name="CreateProfileScreen__your_profile_is_end_to_end_encrypted">Your profile and changes to it will be visible to people you message, contacts, and groups.</string>
<!-- Label/hint for the first-name field, indicating it is required. -->
<string name="CreateProfileScreen__first_name_required">First name (required)</string>
<!-- Label/hint for the last-name field, indicating it is optional. -->
<string name="CreateProfileScreen__last_name_optional">Last name (optional)</string>
<!-- Action button text to advance from create-profile screen. -->
<string name="CreateProfileScreen__next">Next</string>
<!-- Content description for the avatar selection control. -->
<string name="CreateProfileScreen__set_avatar_description">Set avatar</string>
<!-- Phone number discoverability picker -->
<!-- Title row label shown on the create-profile screen pointing at the discoverability picker. -->
<string name="WhoCanSeeMyPhoneNumberFragment__who_can_find_me_by_number">Who can find me by number?</string>
<!-- Description shown on the picker when "Everyone" is selected. -->
<string name="WhoCanSeeMyPhoneNumberFragment__anyone_who_has_your">Anyone who has your phone number will see you\'re on Signal and can start chats with you.</string>
<!-- Description shown on the picker when "Nobody" is selected. -->
<string name="WhoCanSeeMyPhoneNumberFragment__nobody_will_be_able">Nobody will be able to see you\'re on Signal unless you message them or have an existing chat with them.</string>
<!-- Radio option label for "Everyone can find me by number". -->
<string name="PhoneNumberPrivacy_everyone">Everyone</string>
<!-- Radio option label for "Nobody can find me by number". -->
<string name="PhoneNumberPrivacy_nobody">Nobody</string>
<!-- Title for the confirmation dialog shown when the user selects "Nobody". -->
<string name="PhoneNumberPrivacySettingsFragment__nobody_can_find_me_warning_title">Are you sure?</string>
<!-- Body for the confirmation dialog shown when the user selects "Nobody". -->
<string name="PhoneNumberPrivacySettingsFragment__nobody_can_find_me_warning_message">Setting \"Who can find me by number\" to \"Nobody\" will make it harder for people to find you on Signal.</string>
<!-- Cancel button on the "Nobody" confirmation dialog. -->
<string name="PhoneNumberPrivacySettingsFragment__cancel">Cancel</string>
<!-- Save button on the discoverability picker. -->
<string name="PhoneNumberDiscoverabilityScreen__save">Save</string>
<!-- Content description for the back arrow on the discoverability picker. -->
<string name="PhoneNumberDiscoverabilityScreen__back">Back</string>
<!-- Error message shown when the captcha fails to load. -->
<string name="CaptchaScreen__failed_to_load_captcha">Failed to load captcha</string>
<!-- Button to cancel out of the captcha screen. -->
@@ -1,29 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.screens.createprofile
import androidx.compose.runtime.Composable
import com.android.tools.screenshot.PreviewTest
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.ScreenshotPreviews
class CreateProfileScreenScreenshotTests {
@PreviewTest
@ScreenshotPreviews
@Composable
fun CreateProfileScreenPreview() {
Previews.Preview {
CreateProfileScreen(
state = CreateProfileState(
givenName = "Alice",
familyName = "Anderson",
isLoading = false
),
onEvent = {}
)
}
}
}
@@ -1,25 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.screens.discoverability
import androidx.compose.runtime.Composable
import com.android.tools.screenshot.PreviewTest
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.ScreenshotPreviews
class PhoneNumberDiscoverabilityScreenScreenshotTests {
@PreviewTest
@ScreenshotPreviews
@Composable
fun PhoneNumberDiscoverabilityScreenPreview() {
Previews.Preview {
PhoneNumberDiscoverabilityScreen(
state = PhoneNumberDiscoverabilityState(discoverable = true),
onEvent = {}
)
}
}
}
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:51630b23f4e64fc135694b7d31ac9e43d427feb0fa6a718e7636ecb2d08a847d
size 91390
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e2ee6b25920fa70f88c8e8d0ca0358b7b43084d6b89d52eb1ff9f1e94e09563e
size 91661
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:af8930683c82650d79b7b4d4f38440c1a39525daf57378d840275ce06ea11903
size 92268
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:694beaa30a8a2792b814a50b26d158be58584d0b0d01db87b48dd707a98b301b
size 45982
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6b134de26394d46d6059994120ba14349a2fdcf38c087c7f7478bcadf9b65a88
size 76287
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a76c35f2864ec618ad9d7ef86b8e5ba79f5f6800f887f8c74f5e613bb7b209cf
size 76803
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3d9ce7423ba1282fdce59422bd8024f714bcde34cbd997759af2730bdfc7d4e7
size 81162
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:70ef3f6c5afc2001e9b2a3d5fe466e2a98187825f700e2a64b5815f410e0221d
size 99034
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:17b3e3cca904d031cd2f227d15fd4120d65708b6f413bf21d14a258c1c92e925
size 99666
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5bf789154fda500496e14266d711b1c573704c60028726b2b9867a071e861e73
size 101423
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:910fcc416ccb82ec783522ef29e84b551963c6f25da6f72bfe74aca489b1ae40
size 62012
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c9bd57cd415076915a586adbc966d00a4d5aed5d1aec07ec8ab31bb128394240
size 62348
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2710c2b37c152ad9ab4b53e2fa9accc65da9ab03378b4354b942d7a61bf78f5c
size 62663
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d93740b86d36302ba2d558b39191d19a6376c6d2ccc0e771fc2a38254a4518b2
size 46567
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9814d822e306de2f75fb28589e80ac7b38c254fb51a4e29e259eac99ae5eee21
size 49107
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:afa8a206a7ca45b8493ead733a42c07fc548a42b4160a5cc4e1852e44c59701b
size 49414
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0326780c0ad5687c1253c4383572d1c7cc2716a3b6e269fddfd20bee5ad572c2
size 52908
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:25a5116d58974a3b411cb7b19428427d85b7ab0ece367243be88cf31312d28f3
size 69766
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bf046e84391f1fb88f9635efa86413bd421ba770e3b3e15066b8a01906d3ce08
size 70015
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a5c9feb5ada26e088f75f47be47e56e4fca5b585b554cd1a5d5be744fecab005
size 72279
@@ -1,142 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.screens.createprofile
import android.app.Application
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.test.core.app.ApplicationProvider
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.signal.core.ui.CoreUiDependenciesRule
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.registration.test.TestTags
/**
* Tests for CreateProfileScreen that validate event emissions.
* Uses Robolectric to run fast JUnit tests without an emulator.
*/
@RunWith(RobolectricTestRunner::class)
@Config(application = Application::class)
class CreateProfileScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@get:Rule
val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext())
@Test
fun `screen displays given name and family name fields`() {
// Given
composeTestRule.setContent {
SignalTheme {
CreateProfileScreen(
state = CreateProfileState(isLoading = false),
onEvent = {}
)
}
}
// Then
composeTestRule.onNodeWithTag(TestTags.CREATE_PROFILE_GIVEN_NAME_FIELD).assertIsDisplayed()
composeTestRule.onNodeWithTag(TestTags.CREATE_PROFILE_FAMILY_NAME_FIELD).assertIsDisplayed()
}
@Test
fun `when typing in given name field, GivenNameChanged event is emitted`() {
// Given
var emittedEvent: CreateProfileScreenEvents? = null
composeTestRule.setContent {
SignalTheme {
CreateProfileScreen(
state = CreateProfileState(isLoading = false),
onEvent = { event ->
emittedEvent = event
}
)
}
}
// When
composeTestRule.onNodeWithTag(TestTags.CREATE_PROFILE_GIVEN_NAME_FIELD).performTextInput("Alice")
// Then
assert(emittedEvent is CreateProfileScreenEvents.GivenNameChanged)
}
@Config(qualifiers = "w360dp-h1200dp")
@Test
fun `when who can find me row is clicked, WhoCanFindMeClicked event is emitted`() {
// Given
var emittedEvent: CreateProfileScreenEvents? = null
composeTestRule.setContent {
SignalTheme {
CreateProfileScreen(
state = CreateProfileState(isLoading = false),
onEvent = { event ->
emittedEvent = event
}
)
}
}
// When
composeTestRule.onNodeWithTag(TestTags.CREATE_PROFILE_WHO_CAN_FIND_ME_ROW).performClick()
// Then
assert(emittedEvent == CreateProfileScreenEvents.WhoCanFindMeClicked)
}
@Test
fun `when given name is blank, Next button is disabled`() {
// Given
composeTestRule.setContent {
SignalTheme {
CreateProfileScreen(
state = CreateProfileState(isLoading = false),
onEvent = {}
)
}
}
// Then
composeTestRule.onNodeWithTag(TestTags.CREATE_PROFILE_NEXT_BUTTON).assertIsNotEnabled()
}
@Test
fun `when given name is present, Next button is enabled and emits NextClicked`() {
// Given
var emittedEvent: CreateProfileScreenEvents? = null
composeTestRule.setContent {
SignalTheme {
CreateProfileScreen(
state = CreateProfileState(givenName = "Alice", isLoading = false),
onEvent = { event ->
emittedEvent = event
}
)
}
}
// When
composeTestRule.onNodeWithTag(TestTags.CREATE_PROFILE_NEXT_BUTTON).assertIsEnabled().performClick()
// Then
assert(emittedEvent == CreateProfileScreenEvents.NextClicked)
}
}
@@ -1,146 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.screens.discoverability
import android.app.Application
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.core.app.ApplicationProvider
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.signal.core.ui.CoreUiDependenciesRule
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.registration.test.TestTags
/**
* Tests for PhoneNumberDiscoverabilityScreen that validate event emissions.
* Uses Robolectric to run fast JUnit tests without an emulator.
*/
@RunWith(RobolectricTestRunner::class)
@Config(application = Application::class)
class PhoneNumberDiscoverabilityScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@get:Rule
val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext())
@Test
fun `screen displays everyone and nobody options and save button`() {
// Given
composeTestRule.setContent {
SignalTheme {
PhoneNumberDiscoverabilityScreen(
state = PhoneNumberDiscoverabilityState(),
onEvent = {}
)
}
}
// Then
composeTestRule.onNodeWithTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_EVERYONE_OPTION).assertIsDisplayed()
composeTestRule.onNodeWithTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_NOBODY_OPTION).assertIsDisplayed()
composeTestRule.onNodeWithTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_SAVE_BUTTON).assertIsDisplayed()
}
@Test
fun `when nobody option is clicked, NobodySelected event is emitted`() {
// Given
var emittedEvent: PhoneNumberDiscoverabilityScreenEvents? = null
composeTestRule.setContent {
SignalTheme {
PhoneNumberDiscoverabilityScreen(
state = PhoneNumberDiscoverabilityState(),
onEvent = { event ->
emittedEvent = event
}
)
}
}
// When
composeTestRule.onNodeWithTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_NOBODY_OPTION).performClick()
// Then
assert(emittedEvent == PhoneNumberDiscoverabilityScreenEvents.NobodySelected)
}
@Test
fun `when everyone option is clicked, EveryoneSelected event is emitted`() {
// Given
var emittedEvent: PhoneNumberDiscoverabilityScreenEvents? = null
composeTestRule.setContent {
SignalTheme {
PhoneNumberDiscoverabilityScreen(
state = PhoneNumberDiscoverabilityState(discoverable = false),
onEvent = { event ->
emittedEvent = event
}
)
}
}
// When
composeTestRule.onNodeWithTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_EVERYONE_OPTION).performClick()
// Then
assert(emittedEvent == PhoneNumberDiscoverabilityScreenEvents.EveryoneSelected)
}
@Test
fun `when save button is clicked, SaveClicked event is emitted`() {
// Given
var emittedEvent: PhoneNumberDiscoverabilityScreenEvents? = null
composeTestRule.setContent {
SignalTheme {
PhoneNumberDiscoverabilityScreen(
state = PhoneNumberDiscoverabilityState(),
onEvent = { event ->
emittedEvent = event
}
)
}
}
// When
composeTestRule.onNodeWithTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_SAVE_BUTTON).performClick()
// Then
assert(emittedEvent == PhoneNumberDiscoverabilityScreenEvents.SaveClicked)
}
@Test
fun `when back button is clicked, BackClicked event is emitted`() {
// Given
var emittedEvent: PhoneNumberDiscoverabilityScreenEvents? = null
composeTestRule.setContent {
SignalTheme {
PhoneNumberDiscoverabilityScreen(
state = PhoneNumberDiscoverabilityState(),
onEvent = { event ->
emittedEvent = event
}
)
}
}
// When
composeTestRule.onNodeWithTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_BACK_BUTTON).performClick()
// Then
assert(emittedEvent == PhoneNumberDiscoverabilityScreenEvents.BackClicked)
}
}