diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt index 5b4027c7df..abe01c1e39 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt @@ -25,7 +25,6 @@ import org.signal.appsettings.account.AccountSettingsScreen import org.signal.core.ui.compose.CollectActions import org.signal.core.ui.compose.ComposeFragment import org.signal.core.util.ServiceUtil -import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.compose.BiometricsAuthentication import org.thoughtcrime.securesms.components.compose.rememberBiometricsAuthentication @@ -33,6 +32,7 @@ import org.thoughtcrime.securesms.components.settings.app.account.authenticator. import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity import org.thoughtcrime.securesms.registration.ui.RegistrationActivity +import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.PlayStoreUtil import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.signal.appsettings.R as AppSettingsR @@ -43,10 +43,6 @@ import org.signal.appsettings.R as AppSettingsR */ class AccountSettingsFragment : ComposeFragment() { - companion object { - private val TAG = Log.tag(AccountSettingsFragment::class) - } - private val viewModel: AccountSettingsViewModel by viewModels() private lateinit var pinFlowLauncher: ActivityResultLauncher @@ -117,8 +113,7 @@ class AccountSettingsFragment : ComposeFragment() { AccountSettingsAction.ShowAuthenticationFailed -> toast(AppSettingsR.string.AccountSettingsFragment__authentication_required) AccountSettingsAction.ShowTotpAppRemoved -> toast(AppSettingsR.string.AccountSettingsFragment__authenticator_app_removed) AccountSettingsAction.ShowTotpAppRemovalFailed -> toast(AppSettingsR.string.AccountSettingsFragment__couldnt_remove_authenticator_app) - // TODO Open the two-factor authentication support article once one exists. - AccountSettingsAction.OpenLearnMore -> Log.w(TAG, "There's no support article to open yet.") + is AccountSettingsAction.OpenSupportArticle -> CommunicationActions.openBrowserLink(requireContext(), action.url) AccountSettingsAction.NavigateToAdvancedPinSettings -> findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_advancedPinSettingsActivity) AccountSettingsAction.NavigateToChangePhoneNumber -> findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_changePhoneNumberFragment) AccountSettingsAction.NavigateToDeviceTransfer -> findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_oldDeviceTransferActivity) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModel.kt index 55e6a85181..c5b1f7b658 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModel.kt @@ -91,8 +91,8 @@ class AccountSettingsViewModel( AccountSettingsEvent.AddTotpAppClicked -> { applyAddTotpAppClicked() } - AccountSettingsEvent.LearnMoreClicked -> { - _actions.send(AccountSettingsAction.OpenLearnMore) + is AccountSettingsEvent.LearnMoreClicked -> { + _actions.send(AccountSettingsAction.OpenSupportArticle(event.url)) } is AccountSettingsEvent.RenameMethodClicked -> { applyRenameMethodClicked(event.method) diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModelTest.kt index 2bb33be7ff..ffa690af6e 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModelTest.kt @@ -540,9 +540,9 @@ class AccountSettingsViewModelTest { val viewModel = createViewModel() val actions = collectActions(viewModel.actions) - viewModel.onEvent(AccountSettingsEvent.LearnMoreClicked) + viewModel.onEvent(AccountSettingsEvent.LearnMoreClicked("https://support.signal.org/hc/articles/11228705649690")) - assertThat(actions.last()).isEqualTo(AccountSettingsAction.OpenLearnMore) + assertThat(actions.last()).isEqualTo(AccountSettingsAction.OpenSupportArticle("https://support.signal.org/hc/articles/11228705649690")) } @Test diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsAction.kt b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsAction.kt index 30c02439ea..8195654e74 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsAction.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsAction.kt @@ -51,8 +51,8 @@ sealed interface AccountSettingsAction { /** Tell the user the removal didn't go through, so they know the app is still on the account. */ data object ShowTotpAppRemovalFailed : AccountSettingsAction - /** Send the user to a support article about two-factor authentication. */ - data object OpenLearnMore : AccountSettingsAction + /** Send the user to the support article at [url]. */ + data class OpenSupportArticle(val url: String) : AccountSettingsAction /** Open the advanced PIN settings screen. */ data object NavigateToAdvancedPinSettings : AccountSettingsAction diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsEvent.kt b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsEvent.kt index 800551f950..b88e9657dc 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsEvent.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsEvent.kt @@ -51,8 +51,8 @@ sealed interface AccountSettingsEvent { /** The user tapped the authenticator app option in the two-factor set-up menu. */ data object AddTotpAppClicked : AccountSettingsEvent - /** The user tapped the learn more link on the dialog explaining the authenticator app limit. */ - data object LearnMoreClicked : AccountSettingsEvent + /** The user tapped a learn more link that should open the support article at [url]. */ + data class LearnMoreClicked(val url: String) : AccountSettingsEvent /** The user tapped the rename option in [method]'s overflow menu. */ data class RenameMethodClicked(val method: TwoFactorMethod) : AccountSettingsEvent diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsScreen.kt b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsScreen.kt index 569527b7d1..8af3e0dcba 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsScreen.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsScreen.kt @@ -43,12 +43,14 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource 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.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.withStyle +import androidx.compose.ui.text.withLink import androidx.compose.ui.unit.dp import org.signal.appsettings.R import org.signal.appsettings.account.AccountSettingsState.Dialog @@ -73,6 +75,7 @@ import org.signal.core.ui.R as CoreUiR object AccountSettingsTestTags { const val SCROLLER = "scroller" const val CARD_SIGNAL_LOGIN = "card-signal-login" + const val LINK_SIGNAL_LOGIN_LEARN_MORE = "link-signal-login-learn-more" const val ROW_SET_UP_TWO_FACTOR = "row-set-up-two-factor" const val MENU_ITEM_AUTHENTICATOR_APP = "menu-item-authenticator-app" const val ROW_TWO_FACTOR_METHOD = "row-two-factor-method" @@ -81,6 +84,7 @@ object AccountSettingsTestTags { const val MENU_ITEM_REMOVE = "menu-item-remove" const val TWO_FACTOR_LOADING = "two-factor-loading" const val TWO_FACTOR_LOAD_FAILED_MESSAGE = "two-factor-load-failed-message" + const val LINK_TWO_FACTOR_LEARN_MORE = "link-two-factor-learn-more" const val ROW_MODIFY_PIN = "row-modify-pin" const val ROW_PIN_REMINDER = "row-pin-reminder" const val ROW_REGISTRATION_LOCK = "row-registration-lock" @@ -139,7 +143,12 @@ fun AccountSettingsScreen( } item { - SectionFooter(text = stringResource(R.string.AccountSettingsFragment__your_signal_login_is_used_to_recover)) + SectionFooter( + text = stringResource(R.string.AccountSettingsFragment__your_signal_login_is_used_to_recover), + url = "https://support.signal.org/hc/articles/11197884108826", + onEvent = onEvent, + modifier = Modifier.testTag(AccountSettingsTestTags.LINK_SIGNAL_LOGIN_LEARN_MORE) + ) } item { @@ -192,7 +201,12 @@ fun AccountSettingsScreen( } item { - SectionFooter(text = stringResource(R.string.AccountSettingsFragment__use_a_second_form_of_authentication)) + SectionFooter( + text = stringResource(R.string.AccountSettingsFragment__use_a_second_form_of_authentication), + url = "https://support.signal.org/hc/articles/11228705649690", + onEvent = onEvent, + modifier = Modifier.testTag(AccountSettingsTestTags.LINK_TWO_FACTOR_LEARN_MORE) + ) } item { @@ -556,24 +570,28 @@ private fun SignalLoginCard( } /** - * Explanatory text shown underneath a section, ending in a "Learn more" link that has nowhere to go yet. + * Explanatory text shown underneath a section, ending in a "Learn more" link that opens the support article at [url]. */ @Composable private fun SectionFooter( text: String, + url: String, + onEvent: (AccountSettingsEvent) -> Unit, modifier: Modifier = Modifier ) { - val learnMore = stringResource(R.string.AccountSettingsFragment__learn_more) - val primaryColor = MaterialTheme.colorScheme.primary - Text( - text = remember(text, learnMore, primaryColor) { - buildAnnotatedString { - append(text) - append(" ") - withStyle(SpanStyle(color = primaryColor)) { - append(learnMore) - } + text = buildAnnotatedString { + append(text) + append(" ") + + withLink( + LinkAnnotation.Clickable( + tag = "learn-more", + styles = TextLinkStyles(style = SpanStyle(color = MaterialTheme.colorScheme.primary)), + linkInteractionListener = { onEvent(AccountSettingsEvent.LearnMoreClicked(url)) } + ) + ) { + append(stringResource(R.string.AccountSettingsFragment__learn_more)) } }, style = MaterialTheme.typography.bodyMedium, @@ -625,7 +643,7 @@ private fun MaxTotpAppsReachedDialog( onConfirm = {}, onDismiss = { onEvent(AccountSettingsEvent.DialogDismissed) }, dismiss = stringResource(R.string.AccountSettingsFragment__learn_more), - onDeny = { onEvent(AccountSettingsEvent.LearnMoreClicked) }, + onDeny = { onEvent(AccountSettingsEvent.LearnMoreClicked("https://support.signal.org/hc/articles/11228705649690")) }, onDismissRequest = { onEvent(AccountSettingsEvent.DialogDismissed) }, modifier = Modifier.testTag(AccountSettingsTestTags.DIALOG_MAX_TOTP_APPS_REACHED) ) diff --git a/feature/app-settings/src/test/java/org/signal/appsettings/account/AccountSettingsScreenTest.kt b/feature/app-settings/src/test/java/org/signal/appsettings/account/AccountSettingsScreenTest.kt index 04101d035d..2eb44bdd82 100644 --- a/feature/app-settings/src/test/java/org/signal/appsettings/account/AccountSettingsScreenTest.kt +++ b/feature/app-settings/src/test/java/org/signal/appsettings/account/AccountSettingsScreenTest.kt @@ -7,6 +7,7 @@ package org.signal.appsettings.account import android.app.Application import android.text.format.DateUtils +import androidx.compose.ui.semantics.SemanticsActions import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsEnabled @@ -15,10 +16,13 @@ import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onChildren +import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToNode +import androidx.compose.ui.test.performSemanticsAction import assertk.assertThat import assertk.assertions.contains import org.junit.Rule @@ -422,10 +426,30 @@ class AccountSettingsScreenTest { composeTestRule.onNodeWithTag(AccountSettingsTestTags.DIALOG_MAX_TOTP_APPS_REACHED).assertIsDisplayed() composeTestRule.onNodeWithTag(Dialogs.TEST_TAG_ALERT_DIALOG_DISMISS_BUTTON).performClick() - assertThat(events).contains(AccountSettingsEvent.LearnMoreClicked) + assertThat(events).contains(AccountSettingsEvent.LearnMoreClicked("https://support.signal.org/hc/articles/11228705649690")) assertThat(events).contains(AccountSettingsEvent.DialogDismissed) } + @Test + fun whenIClickTheSignalLoginLearnMore_thenIExpectLearnMoreForTheSignalLoginArticle() { + setContent(createState(signalLogin = signalLogin())) + + scrollTo(AccountSettingsTestTags.LINK_SIGNAL_LOGIN_LEARN_MORE) + clickLink(AccountSettingsTestTags.LINK_SIGNAL_LOGIN_LEARN_MORE) + + assertThat(events).contains(AccountSettingsEvent.LearnMoreClicked("https://support.signal.org/hc/articles/11197884108826")) + } + + @Test + fun whenIClickTheTwoFactorLearnMore_thenIExpectLearnMoreForTheTwoFactorArticle() { + setContent(createState(signalLogin = signalLogin())) + + scrollTo(AccountSettingsTestTags.LINK_TWO_FACTOR_LEARN_MORE) + clickLink(AccountSettingsTestTags.LINK_TWO_FACTOR_LEARN_MORE) + + assertThat(events).contains(AccountSettingsEvent.LearnMoreClicked("https://support.signal.org/hc/articles/11228705649690")) + } + @Test fun whenTheTwoFactorListHasntArrived_thenIExpectASpinnerRatherThanAnEmptyList() { setContent(createState(signalLogin = signalLogin(loadState = LoadState.LOADING))) @@ -478,6 +502,14 @@ class AccountSettingsScreenTest { return AccountSettingsState.SignalLogin(twoFactorMethods = twoFactorMethods, loadState = loadState, maxTotpApps = maxTotpApps) } + /** Links inside an [androidx.compose.ui.text.AnnotatedString] have no bounds to tap, so their click action is invoked directly. */ + private fun clickLink(testTag: String) { + composeTestRule.onNodeWithTag(testTag) + .onChildren() + .onFirst() + .performSemanticsAction(SemanticsActions.OnClick) + } + private fun scrollTo(testTag: String) { composeTestRule.onNodeWithTag(AccountSettingsTestTags.SCROLLER) .performScrollToNode(hasTestTag(testTag)) diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt index 1dd6d3b7ba..1305d10156 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt @@ -62,6 +62,7 @@ import org.signal.registration.screens.accountlocked.AccountLockedScreen import org.signal.registration.screens.accountlocked.AccountLockedScreenEvents import org.signal.registration.screens.accountlocked.AccountLockedState import org.signal.registration.screens.addusername.AddUsernameScreen +import org.signal.registration.screens.addusername.AddUsernameScreenActions import org.signal.registration.screens.addusername.AddUsernameViewModel import org.signal.registration.screens.aepentry.EnterAepForLocalBackupResult import org.signal.registration.screens.aepentry.EnterAepForLocalBackupViewModel @@ -405,12 +406,8 @@ 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" -private const val PIN_LEARN_MORE_URL = "https://support.signal.org/hc/articles/360007059792" private const val CLIPBOARD_TIMEOUT_SECONDS = 60 -// TODO [phonenumberless] Point at the real support article once it exists. -private const val SIGNAL_LOGIN_LEARN_MORE_URL = "https://support.signal.org/" - /** Opens [url] in a browser, surfacing a toast if the device has none. */ private fun openUrl(context: Context, url: String) { LinkActions.openUrl(context, url) { error -> @@ -724,7 +721,7 @@ private fun EntryProviderScope.navigationEntries( val purchaseScope = rememberCoroutineScope() CollectActions(viewModel.actions) { action -> when (action) { - SignalLoginPaymentScreenActions.OpenLearnMoreArticle -> openUrl(context, SIGNAL_LOGIN_LEARN_MORE_URL) + SignalLoginPaymentScreenActions.OpenLearnMoreArticle -> openUrl(context, "https://support.signal.org/hc/articles/11197884108826") is SignalLoginPaymentScreenActions.LaunchPurchaseFlow -> { purchaseScope.launch { @@ -900,7 +897,7 @@ private fun EntryProviderScope.navigationEntries( val context = LocalContext.current CollectActions(viewModel.actions) { action -> when (action) { - SignalLoginCredentialEntryScreenActions.OpenNeedHelpArticle -> openUrl(context, SIGNAL_LOGIN_LEARN_MORE_URL) + SignalLoginCredentialEntryScreenActions.OpenNeedHelpArticle -> openUrl(context, "https://support.signal.org/hc/articles/11197884108826") } } @@ -961,6 +958,12 @@ private fun EntryProviderScope.navigationEntries( ) } val state by viewModel.state.collectAsStateWithLifecycle() + val context = LocalContext.current + CollectActions(viewModel.actions) { action -> + when (action) { + AddUsernameScreenActions.OpenLearnMoreArticle -> openUrl(context, "https://support.signal.org/hc/articles/6712070553754") + } + } AddUsernameScreen( state = state, @@ -998,7 +1001,7 @@ private fun EntryProviderScope.navigationEntries( val context = LocalContext.current CollectActions(viewModel.actions) { action -> when (action) { - PinCreationScreenActions.OpenLearnMoreArticle -> openUrl(context, PIN_LEARN_MORE_URL) + PinCreationScreenActions.OpenLearnMoreArticle -> openUrl(context, "https://support.signal.org/hc/articles/360007059792") } } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameScreen.kt index 2eea9f0d9f..9304ee4e19 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameScreen.kt @@ -115,15 +115,6 @@ fun AddUsernameScreen( ) } - if (state.dialogs.learnMore) { - Dialogs.SimpleMessageDialog( - title = stringResource(R.string.AddUsernameScreen__what_is_this_number), - message = stringResource(R.string.AddUsernameScreen__these_digits_help_keep), - dismiss = stringResource(android.R.string.ok), - onDismiss = { onEvent(AddUsernameScreenEvents.LearnMoreDialogDismissed) } - ) - } - Surface( modifier = modifier .fillMaxSize() diff --git a/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameScreenActions.kt b/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameScreenActions.kt new file mode 100644 index 0000000000..97ae7d4f4a --- /dev/null +++ b/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameScreenActions.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.addusername + +sealed interface AddUsernameScreenActions { + /** Open the article explaining phone number privacy and usernames. */ + data object OpenLearnMoreArticle : AddUsernameScreenActions +} diff --git a/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameScreenEvents.kt b/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameScreenEvents.kt index 74bfc00bfd..6d7caf3f48 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameScreenEvents.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameScreenEvents.kt @@ -37,9 +37,6 @@ sealed class AddUsernameScreenEvents { /** The user tapped the "learn more" link under the username field. */ data object LearnMoreClicked : AddUsernameScreenEvents() - /** The user dismissed the dialog explaining the digits after the username. */ - data object LearnMoreDialogDismissed : AddUsernameScreenEvents() - /** The user tapped the skip button, asking to opt out of choosing a username. */ data object SkipClicked : AddUsernameScreenEvents() diff --git a/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameState.kt b/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameState.kt index 9eea660f36..c92f6cf7cb 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameState.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameState.kt @@ -58,8 +58,6 @@ data class AddUsernameState( } data class Dialogs( - /** Explains what the digits after the username are for. */ - val learnMore: Boolean = false, /** Confirms the user wants to skip choosing a username. */ val confirmSkip: Boolean = false, val networkError: Boolean = false, diff --git a/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameViewModel.kt index 2f6c5ebf9a..67e81485ef 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameViewModel.kt @@ -10,6 +10,8 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -18,6 +20,7 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import org.signal.core.ui.compose.EventDrivenViewModel import org.signal.core.util.UsernameUtil @@ -53,6 +56,9 @@ class AddUsernameViewModel( private val _state = MutableStateFlow(AddUsernameState()) val state: StateFlow = _state.asStateFlow() + private val _actions = Channel(Channel.BUFFERED) + val actions: Flow = _actions.receiveAsFlow() + private val entryChanges = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) /** The in-flight reservation request. Only one may be live at a time -- starting a new one cancels the old one. */ @@ -86,8 +92,7 @@ class AddUsernameViewModel( is AddUsernameScreenEvents.DiscriminatorChanged -> applyDiscriminatorChanged(state, event.value, stateEmitter) is AddUsernameScreenEvents.EntrySettled -> applyEntrySettled(state, event, stateEmitter) is AddUsernameScreenEvents.ReservationCompleted -> applyReservationCompleted(state, event, stateEmitter) - is AddUsernameScreenEvents.LearnMoreClicked -> stateEmitter(state.copy(dialogs = state.dialogs.copy(learnMore = true))) - is AddUsernameScreenEvents.LearnMoreDialogDismissed -> stateEmitter(state.copy(dialogs = state.dialogs.copy(learnMore = false))) + is AddUsernameScreenEvents.LearnMoreClicked -> _actions.trySend(AddUsernameScreenActions.OpenLearnMoreArticle) is AddUsernameScreenEvents.SkipClicked -> stateEmitter(state.copy(dialogs = state.dialogs.copy(confirmSkip = true))) is AddUsernameScreenEvents.SkipConfirmed -> applySkipConfirmed(parentEventEmitter) is AddUsernameScreenEvents.SkipDialogDismissed -> stateEmitter(state.copy(dialogs = state.dialogs.copy(confirmSkip = false))) diff --git a/feature/registration/src/main/res/values/strings.xml b/feature/registration/src/main/res/values/strings.xml index 5e3f9271dc..2bbc55dfa8 100644 --- a/feature/registration/src/main/res/values/strings.xml +++ b/feature/registration/src/main/res/values/strings.xml @@ -747,12 +747,8 @@ Username Usernames are always paired with a set of numbers. - + Learn more - - What is this number? - - These digits help keep your username private so you can avoid unwanted messages. Share your username with only the people and groups you\'d like to chat with. If you change usernames you\'ll get a new set of digits. Skip diff --git a/feature/registration/src/test/java/org/signal/registration/screens/addusername/AddUsernameViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/addusername/AddUsernameViewModelTest.kt index ff6b7873d7..f684429846 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/addusername/AddUsernameViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/addusername/AddUsernameViewModelTest.kt @@ -17,6 +17,7 @@ import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain @@ -61,16 +62,14 @@ class AddUsernameViewModelTest { } @Test - fun `LearnMoreClicked shows the dialog explaining the discriminator`() = runTest(testDispatcher) { + fun `LearnMoreClicked emits an action to open the learn more article`() = runTest(testDispatcher) { + val actions = mutableListOf() + backgroundScope.launch(testDispatcher) { viewModel.actions.collect { actions.add(it) } } + viewModel.onEvent(AddUsernameScreenEvents.LearnMoreClicked) advanceUntilIdle() - assertThat(viewModel.state.value.dialogs.learnMore).isTrue() - - viewModel.onEvent(AddUsernameScreenEvents.LearnMoreDialogDismissed) - advanceUntilIdle() - - assertThat(viewModel.state.value.dialogs.learnMore).isFalse() + assertThat(actions).containsExactly(AddUsernameScreenActions.OpenLearnMoreArticle) } @Test