Add beta labeling to signal login feature.

This commit is contained in:
Greyson Parrelli
2026-09-09 16:44:26 -04:00
committed by Cody Henthorne
parent c30d017d10
commit 533fcdc604
32 changed files with 253 additions and 66 deletions
@@ -7,12 +7,15 @@ package org.signal.core.ui.compose
import android.text.Spanned
import android.text.style.URLSpan
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.text.AnnotatedString
@@ -26,21 +29,32 @@ import org.signal.core.ui.R
object Texts {
/**
* Header row for settings pages.
*
* @param trailingContent Optional content shown beside the header text, such as a status tag.
*/
@Composable
fun SectionHeader(
text: String,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
trailingContent: @Composable () -> Unit = {}
) {
Text(
text = text,
style = MaterialTheme.typography.titleSmall,
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier
.padding(
horizontal = dimensionResource(id = R.dimen.gutter)
)
.padding(top = 16.dp, bottom = 12.dp)
)
) {
Text(
text = text,
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.weight(1f, fill = false)
)
trailingContent()
}
}
@Composable
+1
View File
@@ -23,6 +23,7 @@ dependencies {
// Project dependencies
implementation(project(":core:ui"))
implementation(project(":core:util"))
implementation(project(":lib:signal-login"))
// Compose BOM
implementation(platform(libs.androidx.compose.bom))
@@ -65,6 +65,8 @@ import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.Texts
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.signallogin.beta.SignalLoginBetaDisclaimer
import org.signal.signallogin.beta.SignalLoginBetaTag
import org.signal.core.ui.R as CoreUiR
@VisibleForTesting
@@ -117,7 +119,16 @@ fun AccountSettingsScreen(
if (state.signalLogin != null) {
item {
Texts.SectionHeader(
text = stringResource(R.string.AccountSettingsFragment__signal_login)
text = stringResource(R.string.AccountSettingsFragment__signal_login),
trailingContent = { SignalLoginBetaTag() }
)
}
item {
SignalLoginBetaDisclaimer(
modifier = Modifier
.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter))
.padding(bottom = 12.dp)
)
}
@@ -31,6 +31,7 @@ import org.signal.appsettings.R
import org.signal.appsettings.account.AccountSettingsState.Dialog
import org.signal.appsettings.account.AccountSettingsState.LoadState
import org.signal.core.ui.compose.Dialogs
import org.signal.signallogin.SignalLoginTestTags
@RunWith(RobolectricTestRunner::class)
@Config(application = Application::class)
@@ -323,6 +324,14 @@ class AccountSettingsScreenTest {
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_SET_UP_TWO_FACTOR).assertDoesNotExist()
}
@Test
fun givenASignalLogin_whenScreenDisplayed_thenTheSectionIsLabelledBeta() {
setContent(createState(signalLogin = signalLogin()))
composeTestRule.onNodeWithTag(SignalLoginTestTags.BETA_TAG).assertIsDisplayed()
composeTestRule.onNodeWithTag(SignalLoginTestTags.BETA_DISCLAIMER).assertIsDisplayed()
}
@Test
fun givenASignalLogin_whenIClickTheSignalLoginCard_thenIExpectAccountAndRecoveryEvent() {
setContent(createState(signalLogin = signalLogin()))
@@ -82,6 +82,7 @@ import org.signal.registration.screens.shared.BackTopAppBar
import org.signal.registration.screens.shared.accountIdTextStyle
import org.signal.registration.test.TestTags
import org.signal.signallogin.RecoveryKeyGroups
import org.signal.signallogin.beta.SignalLoginBetaTag
/**
* Collects a Signal Login -- the account ID and the recovery key that pairs with it. What happens with the pair depends
@@ -220,14 +221,22 @@ private fun Header(mode: SignalLoginCredentialEntryState.Mode, twoPane: Boolean
Spacer(modifier = Modifier.height(20.dp))
Text(
text = stringResource(R.string.SignalLoginCredentialEntryScreen__signal_login),
style = if (twoPane) MaterialTheme.typography.headlineLarge else MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.attachDebugLogHelper()
)
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(R.string.SignalLoginCredentialEntryScreen__signal_login),
style = if (twoPane) MaterialTheme.typography.headlineLarge else MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
modifier = Modifier
.weight(1f, fill = false)
.attachDebugLogHelper()
)
SignalLoginBetaTag()
}
Spacer(modifier = Modifier.height(12.dp))
@@ -9,7 +9,6 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
@@ -67,6 +66,8 @@ import org.signal.registration.screens.attachDebugLogHelper
import org.signal.registration.screens.shared.BackTopAppBar
import org.signal.registration.screens.signalloginpayment.SignalLoginPaymentState.Option
import org.signal.registration.test.TestTags
import org.signal.signallogin.beta.SignalLoginBetaDisclaimer
import org.signal.signallogin.beta.SignalLoginBetaTag
private val CARD_SHAPE = RoundedCornerShape(18.dp)
private val CARD_BORDER_WIDTH = 3.5.dp
@@ -207,14 +208,22 @@ private fun Header(
Spacer(modifier = Modifier.height(20.dp))
Text(
text = stringResource(R.string.SignalLoginPaymentScreen__signal_login),
style = if (twoPane) MaterialTheme.typography.headlineLarge else MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.attachDebugLogHelper()
)
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(R.string.SignalLoginPaymentScreen__signal_login),
style = if (twoPane) MaterialTheme.typography.headlineLarge else MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
modifier = Modifier
.weight(1f, fill = false)
.attachDebugLogHelper()
)
SignalLoginBetaTag()
}
Spacer(modifier = Modifier.height(12.dp))
@@ -414,12 +423,20 @@ private fun Footer(
onEvent: (SignalLoginPaymentScreenEvents) -> Unit
) {
RegistrationScaffold.FooterSurface(isElevated = isElevated) {
Box(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.fillMaxWidth()
.padding(params.footerPadding),
contentAlignment = Alignment.Center
.padding(params.footerPadding)
) {
SignalLoginBetaDisclaimer(
textAlign = TextAlign.Center,
modifier = Modifier
.widthIn(max = params.maxButtonWidth)
.fillMaxWidth()
)
Buttons.LargeTonal(
onClick = { onEvent(SignalLoginPaymentScreenEvents.ContinueClicked) },
enabled = state.isActionEnabled,
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d7389b2053a751ace0084968a5bc776fab884f2f6571e2f85962cf18bd6d20d7
size 88744
oid sha256:fca0ef9fae65252cf8bd9594d131a590c5fc9ed551939980447791ceb1928d1b
size 91017
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d75adf75db57082d7995f1cd863536b64f9dd062af31437f0b3eab1dd5f3fc7f
size 88815
oid sha256:9ccbc07ac91dfacc2ffa87c6f3be362533195582b4af93c058eaf2e7fe03a294
size 91049
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3001618df981a37386ddd775c70c4a27fbeda7b0015fbf3b6485f0beb8a65ea9
size 89071
oid sha256:dc0a7fd19135b26b159086579543e454d3e26cd416d9d4d9c20b618a95f659b4
size 91431
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a9f46a712fc6b58e9d2d3d3507d2ba14c35a56bccc61353a8a256d0697a4d297
size 45706
oid sha256:42d11607ac1a969af793d47285f88ca5b6b1f597e4cdd5f83b2d4d1edd5c6093
size 47870
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b91f8c28f27edf17f052ca15418c8532c28eafeac50c13ad60374564110f76da
size 66777
oid sha256:caee9b7e8e43633ca94edddfccf9c99e8e693cece4965c77e4672685584a4961
size 68755
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7c8c547a282842eb464afbd33dc08abafeec7a7d020018964bd259c0aa555150
size 66958
oid sha256:2485b366b4b68a3aa3f93785b6a6446e4f4a9ab7c6663ab6f1d938ac03ed8f0e
size 69094
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6a6fed3e340fc156ccf26bea6b7ad17c5ca10fe09f9b81f666f9436d88feab09
size 78230
oid sha256:5fb32de6ebd29eb114bf0400c1689fe4a9cda963aad922f06f2d49ffd5b43a26
size 80366
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9809e7059b8245449c3010d800d126fdd2417f061441ecff1938bd7f4feb3466
size 96858
oid sha256:d64933e78ef36156e0bd0343d8323c24aef887799766af6fcfdb78ca2c08a975
size 99196
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0698018f8584d0b4bd538c4d26a214db993f12f2dd374647828041688e1de6c4
size 97064
oid sha256:ed8c3b2a02d8f8f7035348da333b8af91e0acfc8122422d0193bc24b45f62520
size 99524
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dc9b24b9dc3dc924fd85f67ca421a216fe556cb7c039f9d5eeb1bae1923d9289
size 93647
oid sha256:57b3df6b143fe5a5695e9877ba13200a967018279813f6374fbc7c1b4672768d
size 95941
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8fd3157a72751ebe1261add767d4453d4f602d5726616391ee1b0b934c80d3ee
size 113008
oid sha256:679318e082cc70edaddff7e5cd38ccd66578b9b030e6a34d1cf5548fae0cec5e
size 115274
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:645b768ff66687b4f4f9ff315ecf7ca08dce157e178f23f5b88dd368e23b7fc0
size 112997
oid sha256:4acd9ef5c3a32d880c8674a8f5fe361fa734bd53dfa423e5ede1863050d9f869
size 115296
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:db7fbb9082a567db444afb42b738aa1760b6f27708c278fd87fa1908023d7d99
size 112997
oid sha256:1cd754de8909cee31ad89c8ddb90d5e9d26b878f1fe41454a64810adca2a35cc
size 115409
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a9f46a712fc6b58e9d2d3d3507d2ba14c35a56bccc61353a8a256d0697a4d297
size 45706
oid sha256:42d11607ac1a969af793d47285f88ca5b6b1f597e4cdd5f83b2d4d1edd5c6093
size 47870
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:64e9723b94caea8384574ea5f1fbeb758e27d6aa21169da16cad17bc222598a9
size 89056
oid sha256:daa86cc2a16529afcbaa06a2b0aad3f3d32116814bfbc0ff580db136d9517e08
size 91066
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:938e042b23448da6b48c34eed715dbaa77cdc8ca1003b600241bd49f8a61e95f
size 88942
oid sha256:652314f57f607e0a06c822c343b1c190901053b239853049f3329882bbdd2eb6
size 91078
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:86ff6f8993b54accce75c4a6593f2d2ff6e96c30e907fa3cf52a43c1e0531683
size 100400
oid sha256:47dced5c24c0b217f375a08dc2cc6781263980624d5d753bbcfadf0b79cb2a3c
size 102296
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ba2f602ceb6de813eea078bfdfe88dc7054d7c554d64ea8685623f5837c4fdce
size 119428
oid sha256:d061a21b2ea715efc5e2e86fe8d4153adf8d67978a4c5ea17fb470beea95c70b
size 121888
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0ce9ff1c55b5bca66c46a44e9f1bf1a01f6fac48d38a265acb95398ba10442f8
size 119353
oid sha256:7332ce008660ad4e14855fa315bfabe23cf04f79ee3367f35cd47c2378b491d4
size 121974
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a45542dfe5ffed4cdcb2eea1248450b0095eb34c99293ab87931323dc75186d4
size 114513
oid sha256:efb7f4df11b4e8b31c85c9200db152bd455414a27bcb4d0fdf57497c86c45345
size 116659
@@ -9,6 +9,7 @@ import android.app.Application
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.getOrNull
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
@@ -38,6 +39,7 @@ import org.signal.passwordmanager.UsernamePasswordCredential
import org.signal.registration.screens.aepentry.AepInput
import org.signal.registration.screens.shared.AccountIdError
import org.signal.registration.test.TestTags
import org.signal.signallogin.SignalLoginTestTags
@RunWith(RobolectricTestRunner::class)
@Config(application = Application::class)
@@ -61,6 +63,13 @@ class SignalLoginCredentialEntryScreenTest {
unmockkAll()
}
@Test
fun `when the screen is displayed, the title is labelled beta`() {
setContent(SignalLoginCredentialEntryState())
composeTestRule.onNodeWithTag(SignalLoginTestTags.BETA_TAG).assertIsDisplayed()
}
@Test
fun `when an empty field is tapped, the password manager is prompted and the picked credential is emitted`() {
stubPasswordManager()
@@ -6,6 +6,7 @@
package org.signal.registration.screens.signallogindetails
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
@@ -42,6 +43,13 @@ class SignalLoginViewDetailsScreenTest {
private val events = mutableListOf<SignalLoginViewDetailsScreenEvents>()
@Test
fun `when the screen is displayed, the title is labelled beta`() {
setContent()
composeTestRule.onNodeWithTag(SignalLoginTestTags.BETA_TAG).assertIsDisplayed()
}
@Test
fun `when the account key copy button is clicked, CopyAccountIdClicked is emitted`() {
setContent()
@@ -9,6 +9,9 @@ package org.signal.signallogin
* Test tags for the composables in this module, so UI tests can find them.
*/
object SignalLoginTestTags {
const val BETA_TAG = "signal_login_beta_tag"
const val BETA_DISCLAIMER = "signal_login_beta_disclaimer"
const val CARD_VIEW_DETAILS_BUTTON = "signal_login_card_view_details_button"
const val VIEW_DETAILS_SCREEN = "signal_login_view_details_screen"
@@ -0,0 +1,81 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.signallogin.beta
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.ui.fonts.SignalSymbols
import org.signal.signallogin.R
import org.signal.signallogin.SignalLoginTestTags
/**
* Pill that marks Signal Login as an unfinished feature. Sits beside the title of every screen that is part of the
* feature.
*/
@Composable
fun SignalLoginBetaTag(modifier: Modifier = Modifier) {
Text(
text = stringResource(R.string.SignalLoginBeta__beta),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface,
modifier = modifier
.testTag(SignalLoginTestTags.BETA_TAG)
.clip(CircleShape)
.background(SignalTheme.colors.colorSurface4)
.padding(horizontal = 12.dp, vertical = 4.dp)
)
}
/**
* Longer form of [SignalLoginBetaTag], for the places that have room to warn the user that the feature is still
* changing underneath them.
*/
@Composable
fun SignalLoginBetaDisclaimer(
modifier: Modifier = Modifier,
textAlign: TextAlign = TextAlign.Start
) {
Text(
text = SignalSymbols.signalSymbolText(
text = stringResource(R.string.SignalLoginBeta__this_is_a_beta_feature_that_will_be_updated),
glyphStart = SignalSymbols.Glyph.INFO
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = textAlign,
modifier = modifier.testTag(SignalLoginTestTags.BETA_DISCLAIMER)
)
}
@DayNightPreviews
@Composable
private fun SignalLoginBetaPreview() {
Previews.Preview {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(16.dp)
) {
SignalLoginBetaTag()
SignalLoginBetaDisclaimer()
}
}
}
@@ -8,6 +8,7 @@ package org.signal.signallogin.viewdetails
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
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
@@ -37,6 +38,7 @@ import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalIcons
import org.signal.signallogin.R
import org.signal.signallogin.SignalLoginTestTags
import org.signal.signallogin.beta.SignalLoginBetaTag
import org.signal.signallogin.details.SignalLoginKeyDetails
/** Size of the miniature credential card artwork shown at the top of the screen, from the design. */
@@ -62,6 +64,7 @@ fun SignalLoginViewDetailsScreen(
onNavigationClick = { onEvent(SignalLoginViewDetailsScreenEvents.BackClicked) },
navigationIcon = SignalIcons.ArrowStart.imageVector,
navigationContentDescription = stringResource(R.string.SignalLoginViewDetailsScreen__navigate_back),
titleContent = { _, title -> TitleWithBetaTag(title) },
modifier = modifier.testTag(SignalLoginTestTags.VIEW_DETAILS_SCREEN)
) { paddingValues ->
Column(
@@ -96,6 +99,22 @@ fun SignalLoginViewDetailsScreen(
}
}
@Composable
private fun TitleWithBetaTag(title: String) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.weight(1f, fill = false)
)
SignalLoginBetaTag()
}
}
/**
* A miniature of the credential card artwork, without any of the card's content.
*/
@@ -1,5 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Signal Login beta labeling -->
<!-- Tag shown beside the title of Signal Login screens, marking the feature as unfinished. Keep it short. -->
<string name="SignalLoginBeta__beta">BETA</string>
<!-- Disclaimer shown on Signal Login screens, warning that the feature is still changing. -->
<string name="SignalLoginBeta__this_is_a_beta_feature_that_will_be_updated">This is a Beta feature that will be updated on an ongoing basis.</string>
<!-- Signal Login credential card -->
<!-- Label for the account portion of the Signal Login on the credential card. -->
<string name="SignalLoginCard__account">Account</string>