mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-07-08 23:14:17 +01:00
Adaptive welcome screen with compact, medium, and large layouts.
This commit is contained in:
committed by
Greyson Parrelli
parent
38baf17938
commit
d90e9919ae
@@ -6,6 +6,10 @@
|
||||
package org.signal.core.ui
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.window.core.layout.WindowSizeClass
|
||||
@@ -31,6 +35,16 @@ fun Resources.getWindowSizeClass(): WindowSizeClass {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberWindowBreakpoint(): WindowBreakpoint {
|
||||
val resources = LocalResources.current
|
||||
val configuration = LocalConfiguration.current
|
||||
|
||||
return remember(resources, configuration) {
|
||||
resources.getWindowBreakpoint()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the device's form factor (PHONE, FOLDABLE, or TABLET) based on the current
|
||||
* [Resources] and window size class.
|
||||
@@ -39,7 +53,7 @@ fun Resources.getWindowSizeClass(): WindowSizeClass {
|
||||
* - Returns [WindowBreakpoint.SMALL] if the width or height is compact.
|
||||
* - Returns [WindowBreakpoint.LARGE] if the height is at least the expanded lower bound.
|
||||
* - Returns [WindowBreakpoint.MEDIUM] if the width is at least the medium lower bound.
|
||||
* - Otherwise, falls back to aspect ratio heuristics: wider (≥ 1.6) is [WindowBreakpoint.LARGE], else [WindowBreakpoint.MEDIUM].
|
||||
* - Otherwise, falls back to aspect ratio heuristics: wider (≥ 1.5) is [WindowBreakpoint.LARGE], else [WindowBreakpoint.MEDIUM].
|
||||
*
|
||||
* @return the inferred [WindowBreakpoint] for the current device.
|
||||
*/
|
||||
@@ -58,7 +72,7 @@ fun Resources.getWindowBreakpoint(): WindowBreakpoint {
|
||||
val denominator = minOf(displayMetrics.widthPixels, displayMetrics.heightPixels)
|
||||
val aspectRatio = numerator.toFloat() / denominator
|
||||
|
||||
return if (aspectRatio >= 1.6f) {
|
||||
return if (aspectRatio >= 1.5f) {
|
||||
WindowBreakpoint.LARGE
|
||||
} else {
|
||||
WindowBreakpoint.MEDIUM
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.ui.compose
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.SubcomposeLayout
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
/**
|
||||
* Lays out [primary] and [secondary] side by side in a row. [primaryProportion] controls what
|
||||
* fraction of the total width [primary] receives (e.g. 0.5f = equal split).
|
||||
*/
|
||||
@Composable
|
||||
fun SideBySideLayout(
|
||||
primary: @Composable () -> Unit,
|
||||
secondary: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
primaryProportion: Float = 0.5f
|
||||
) {
|
||||
SubcomposeLayout(modifier = modifier) { constraints ->
|
||||
val primaryWidth = (constraints.maxWidth * primaryProportion).toInt()
|
||||
val secondaryWidth = constraints.maxWidth - primaryWidth
|
||||
|
||||
val primaryPlaceables = subcompose("primary", primary).map { m ->
|
||||
m.measure(constraints.copy(minWidth = primaryWidth, maxWidth = primaryWidth, minHeight = 0))
|
||||
}
|
||||
val primaryHeight = primaryPlaceables.maxOfOrNull { it.height } ?: 0
|
||||
|
||||
val secondaryMeasurables = subcompose("secondary", secondary)
|
||||
val secondaryMinHeight = secondaryMeasurables.maxOfOrNull { it.minIntrinsicHeight(secondaryWidth) } ?: 0
|
||||
val layoutHeight = maxOf(primaryHeight, secondaryMinHeight)
|
||||
|
||||
val secondaryPlaceables = secondaryMeasurables.map { m ->
|
||||
m.measure(constraints.copy(minWidth = secondaryWidth, maxWidth = secondaryWidth, minHeight = layoutHeight, maxHeight = layoutHeight))
|
||||
}
|
||||
|
||||
layout(constraints.maxWidth, layoutHeight) {
|
||||
primaryPlaceables.forEach { it.placeRelative(0, 0) }
|
||||
secondaryPlaceables.forEach { it.placeRelative(primaryWidth, 0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SideBySideLayoutPreview() {
|
||||
Previews.Preview {
|
||||
SideBySideLayout(
|
||||
primary = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = Color.Red)
|
||||
)
|
||||
},
|
||||
secondary = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = Color.Blue)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -100,10 +100,10 @@ annotation class TabletLandscapeDayPreview
|
||||
@TabletLandscapeDayPreview
|
||||
annotation class TabletDayPreviews
|
||||
|
||||
@Preview(name = "tablet portrait (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=1280dp,height=840dp,orientation=portrait")
|
||||
@Preview(name = "tablet portrait (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=840dp,height=1280dp,orientation=portrait")
|
||||
annotation class TabletPortraitNightPreview
|
||||
|
||||
@Preview(name = "tablet landscape (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=840dp,height=1280dp,orientation=landscape")
|
||||
@Preview(name = "tablet landscape (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=1280dp,height=840dp,orientation=landscape")
|
||||
annotation class TabletLandscapeNightPreview
|
||||
|
||||
@TabletPortraitNightPreview
|
||||
|
||||
@@ -249,6 +249,7 @@ private fun EntryProviderScope<NavKey>.navigationEntries(
|
||||
onEvent = { event ->
|
||||
when (event) {
|
||||
WelcomeScreenEvents.Continue -> parentEventEmitter.navigateTo(RegistrationRoute.Permissions(nextRoute = RegistrationRoute.PhoneNumberEntry))
|
||||
WelcomeScreenEvents.LinkDevice -> parentEventEmitter.navigateTo(RegistrationRoute.Permissions(nextRoute = RegistrationRoute.QuickRestoreQrScan)) // TODO - Replace this with the device-link QR code
|
||||
WelcomeScreenEvents.HasOldPhone -> parentEventEmitter.navigateTo(RegistrationRoute.Permissions(nextRoute = RegistrationRoute.QuickRestoreQrScan))
|
||||
WelcomeScreenEvents.DoesNotHaveOldPhone -> parentEventEmitter.navigateTo(RegistrationRoute.Permissions(nextRoute = RegistrationRoute.ChooseRestoreOptionBeforeRegistration))
|
||||
}
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.registration.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.SubcomposeLayout
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Previews
|
||||
|
||||
/**
|
||||
* Scaffold for registration flow screens.
|
||||
*/
|
||||
@Composable
|
||||
fun RegistrationScreen(
|
||||
content: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
footer: (@Composable () -> Unit)? = null
|
||||
) {
|
||||
SubcomposeLayout(modifier = modifier.imePadding()) { constraints ->
|
||||
val footerPlaceables = footer?.let {
|
||||
subcompose("footer", it).map { m -> m.measure(constraints.copy(minHeight = 0)) }
|
||||
} ?: emptyList()
|
||||
val footerHeight = footerPlaceables.maxOfOrNull { it.height } ?: 0
|
||||
|
||||
val contentHeight = (constraints.maxHeight - footerHeight).coerceAtLeast(0)
|
||||
val contentPlaceables = subcompose("content", content).map { m ->
|
||||
m.measure(constraints.copy(minHeight = contentHeight, maxHeight = contentHeight))
|
||||
}
|
||||
|
||||
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||
contentPlaceables.forEach { it.placeRelative(0, 0) }
|
||||
footerPlaceables.forEach { it.placeRelative(0, contentHeight) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RegistrationScreenPreview() {
|
||||
Previews.Preview {
|
||||
RegistrationScreen(
|
||||
content = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = Color.Red)
|
||||
)
|
||||
},
|
||||
footer = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(100.dp)
|
||||
.background(color = Color.Green)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
+287
-61
@@ -10,7 +10,9 @@ package org.signal.registration.screens.welcome
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -18,6 +20,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -41,18 +44,23 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.signal.core.ui.WindowBreakpoint
|
||||
import org.signal.core.ui.compose.AllDevicePreviews
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SideBySideLayout
|
||||
import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.core.ui.compose.dismissWithAnimation
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.ui.rememberWindowBreakpoint
|
||||
import org.signal.registration.R
|
||||
import org.signal.registration.screens.RegistrationScreen
|
||||
import org.signal.registration.test.TestTags
|
||||
|
||||
/**
|
||||
@@ -65,74 +73,37 @@ fun WelcomeScreen(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
val windowBreakpoint = rememberWindowBreakpoint()
|
||||
val onRestoreOrTransferClick = { showBottomSheet = true }
|
||||
val onTermsAndPrivacyClick: () -> Unit = {}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.testTag(TestTags.WELCOME_SCREEN),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.welcome),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.RegistrationActivity_take_privacy_with_you_be_yourself_in_every_message),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 32.dp)
|
||||
.testTag(TestTags.WELCOME_HEADLINE)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
||||
TextButton(
|
||||
onClick = { /* Terms & Privacy link */ },
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.RegistrationActivity_terms_and_privacy),
|
||||
textAlign = TextAlign.Center
|
||||
when (windowBreakpoint) {
|
||||
WindowBreakpoint.SMALL -> {
|
||||
CompactLayout(
|
||||
onEvent = onEvent,
|
||||
onRestoreOrTransferClick = onRestoreOrTransferClick,
|
||||
onTermsAndPrivacyClick = onTermsAndPrivacyClick,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = { onEvent(WelcomeScreenEvents.Continue) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp)
|
||||
.testTag(TestTags.WELCOME_GET_STARTED_BUTTON)
|
||||
) {
|
||||
Text(stringResource(R.string.RegistrationActivity_continue))
|
||||
WindowBreakpoint.MEDIUM -> {
|
||||
MediumLayout(
|
||||
onEvent = onEvent,
|
||||
onRestoreOrTransferClick = onRestoreOrTransferClick,
|
||||
onTermsAndPrivacyClick = onTermsAndPrivacyClick,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(17.dp))
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = { showBottomSheet = true },
|
||||
colors = ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = SignalTheme.colors.colorSurface2
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp)
|
||||
.testTag(TestTags.WELCOME_RESTORE_OR_TRANSFER_BUTTON)
|
||||
) {
|
||||
Text(stringResource(R.string.registration_activity__restore_or_transfer))
|
||||
WindowBreakpoint.LARGE -> {
|
||||
LargeLayout(
|
||||
onEvent = onEvent,
|
||||
onTermsAndPrivacyClick = onTermsAndPrivacyClick,
|
||||
onRestoreOrTransferClick = onRestoreOrTransferClick,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
|
||||
if (showBottomSheet) {
|
||||
@@ -146,6 +117,261 @@ fun WelcomeScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CompactLayout(
|
||||
onEvent: (WelcomeScreenEvents) -> Unit,
|
||||
onTermsAndPrivacyClick: () -> Unit,
|
||||
onRestoreOrTransferClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
RegistrationScreen(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.testTag(TestTags.WELCOME_SCREEN),
|
||||
content = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
HeroImage(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
)
|
||||
|
||||
Headline(
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 32.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
}
|
||||
},
|
||||
footer = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
TermsAndPrivacy(onTermsAndPrivacyClick = onTermsAndPrivacyClick)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
PrimaryDeviceCallToActionButtons(
|
||||
onEvent = onEvent,
|
||||
onRestoreOrTransferClick = onRestoreOrTransferClick
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MediumLayout(
|
||||
onEvent: (WelcomeScreenEvents) -> Unit,
|
||||
onTermsAndPrivacyClick: () -> Unit,
|
||||
onRestoreOrTransferClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
RegistrationScreen(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(bottom = 56.dp),
|
||||
content = {
|
||||
SideBySideLayout(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
primary = {
|
||||
HeroImage()
|
||||
},
|
||||
secondary = {
|
||||
Headline(
|
||||
modifier = Modifier.padding(top = 88.dp)
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
footer = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
TermsAndPrivacy(onTermsAndPrivacyClick = onTermsAndPrivacyClick)
|
||||
|
||||
PrimaryDeviceCallToActionButtons(
|
||||
onEvent = onEvent,
|
||||
onRestoreOrTransferClick = onRestoreOrTransferClick
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LargeLayout(
|
||||
onEvent: (WelcomeScreenEvents) -> Unit,
|
||||
onTermsAndPrivacyClick: () -> Unit,
|
||||
onRestoreOrTransferClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
RegistrationScreen(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
content = {
|
||||
SideBySideLayout(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
primary = {
|
||||
HeroImage(
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
)
|
||||
},
|
||||
secondary = {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = Modifier
|
||||
.widthIn(max = 380.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(top = 98.dp)
|
||||
) {
|
||||
Headline(
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
modifier = Modifier.padding(start = 10.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
TermsAndPrivacy(
|
||||
onTermsAndPrivacyClick = onTermsAndPrivacyClick,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
PrimaryDeviceCallToActionButtons(
|
||||
onEvent = onEvent,
|
||||
onRestoreOrTransferClick = onRestoreOrTransferClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HeroImage(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.welcome),
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Headline(
|
||||
modifier: Modifier = Modifier,
|
||||
style: TextStyle = MaterialTheme.typography.headlineMedium,
|
||||
textAlign: TextAlign = TextAlign.Start
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.RegistrationActivity_take_privacy_with_you_be_yourself_in_every_message),
|
||||
style = style,
|
||||
textAlign = textAlign,
|
||||
modifier = modifier
|
||||
.testTag(TestTags.WELCOME_HEADLINE)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TermsAndPrivacy(
|
||||
onTermsAndPrivacyClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
TextButton(
|
||||
onClick = onTermsAndPrivacyClick,
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
),
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.RegistrationActivity_terms_and_privacy),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.PrimaryDeviceCallToActionButtons(
|
||||
onEvent: (WelcomeScreenEvents) -> Unit,
|
||||
onRestoreOrTransferClick: () -> Unit
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = { onEvent(WelcomeScreenEvents.Continue) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp)
|
||||
.testTag(TestTags.WELCOME_GET_STARTED_BUTTON)
|
||||
) {
|
||||
Text(stringResource(R.string.RegistrationActivity_continue))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(17.dp))
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onRestoreOrTransferClick,
|
||||
colors = ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = SignalTheme.colors.colorSurface2
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp)
|
||||
.testTag(TestTags.WELCOME_RESTORE_OR_TRANSFER_BUTTON)
|
||||
) {
|
||||
Text(stringResource(R.string.registration_activity__restore_or_transfer))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.SecondaryDeviceCallToActionButtons(
|
||||
onEvent: (WelcomeScreenEvents) -> Unit
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = { onEvent(WelcomeScreenEvents.LinkDevice) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.WELCOME_LINK_DEVICE_BUTTON)
|
||||
) {
|
||||
Text(stringResource(R.string.WelcomeScreen__link_your_account))
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.WelcomeScreen__not_on_signal_yet)
|
||||
)
|
||||
|
||||
TextButton(
|
||||
onClick = { onEvent(WelcomeScreenEvents.Continue) },
|
||||
modifier = Modifier.testTag(TestTags.WELCOME_GET_STARTED_BUTTON)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.WelcomeScreen__create_account)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bottom sheet for restore or transfer options.
|
||||
*/
|
||||
|
||||
+1
@@ -9,6 +9,7 @@ import org.signal.registration.util.DebugLoggableModel
|
||||
|
||||
sealed class WelcomeScreenEvents : DebugLoggableModel() {
|
||||
data object Continue : WelcomeScreenEvents()
|
||||
data object LinkDevice : WelcomeScreenEvents()
|
||||
data object HasOldPhone : WelcomeScreenEvents()
|
||||
data object DoesNotHaveOldPhone : WelcomeScreenEvents()
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ object TestTags {
|
||||
const val WELCOME_SCREEN = "welcome_screen"
|
||||
const val WELCOME_HEADLINE = "welcome_headline"
|
||||
const val WELCOME_GET_STARTED_BUTTON = "welcome_get_started_button"
|
||||
const val WELCOME_LINK_DEVICE_BUTTON = "welcome_link_device_button"
|
||||
const val WELCOME_RESTORE_OR_TRANSFER_BUTTON = "welcome_restore_or_transfer_button"
|
||||
const val WELCOME_RESTORE_HAS_OLD_PHONE_BUTTON = "welcome_restore_has_old_phone_button"
|
||||
const val WELCOME_RESTORE_NO_OLD_PHONE_BUTTON = "welcome_restore_no_old_phone_button"
|
||||
|
||||
@@ -205,4 +205,10 @@
|
||||
<string name="LocalBackupRestoreScreen__restore_failed_description">An error occurred while restoring your backup. Please try again.</string>
|
||||
<!-- Button to retry restore after failure -->
|
||||
<string name="LocalBackupRestoreScreen__try_again">Try again</string>
|
||||
<!-- Button to link an existing Signal account to this device -->
|
||||
<string name="WelcomeScreen__link_your_account">Link your account</string>
|
||||
<!-- Prompt shown to users who don't have a Signal account yet -->
|
||||
<string name="WelcomeScreen__not_on_signal_yet">Not on Signal yet?</string>
|
||||
<!-- Button to create a new Signal account -->
|
||||
<string name="WelcomeScreen__create_account">Create account</string>
|
||||
</resources>
|
||||
|
||||
+23
@@ -56,6 +56,29 @@ class WelcomeScreenTest {
|
||||
assert(emittedEvent == WelcomeScreenEvents.Continue)
|
||||
}
|
||||
|
||||
@Config(qualifiers = "w1280dp-h800dp-xhdpi")
|
||||
@Test
|
||||
fun `when Link Your Account is clicked, LinkDevice event is emitted`() {
|
||||
// Given
|
||||
var emittedEvent: WelcomeScreenEvents? = null
|
||||
|
||||
composeTestRule.setContent {
|
||||
SignalTheme {
|
||||
WelcomeScreen(
|
||||
onEvent = { event ->
|
||||
emittedEvent = event
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// When
|
||||
composeTestRule.onNodeWithTag(TestTags.WELCOME_GET_STARTED_BUTTON).performClick()
|
||||
|
||||
// Then
|
||||
assert(emittedEvent == WelcomeScreenEvents.Continue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when Restore or transfer is clicked, bottom sheet is shown`() {
|
||||
// Given
|
||||
|
||||
Reference in New Issue
Block a user