chore(device-migration): Update text and UI for QR code login.

This commit is contained in:
Mateusz Armatys
2025-05-13 15:48:40 +02:00
parent 6ad313a7ea
commit fbe86914cc
20 changed files with 164 additions and 103 deletions
@@ -54,7 +54,7 @@
<string name="auth_help_option_other">Other sign-in issues</string>
<string name="auth_help_other">Still need help? Contact us directly.</string>
<string name="auth_help_option_customer_support">Customer support</string>
<string name="auth_help_option_sign_in_with_qr_code">Sign in with a QR code</string>
<string name="auth_help_option_sign_in_with_qr_code">Sign in with QR code</string>
<string name="auth_2fa_title">Two Factor Authentication</string>
<string name="auth_2fa_subtitle">Your account has several 2FA mechanisms configured. Please pick how you want to confirm your identity.</string>
<string name="auth_2fa_tab_one_time_code">One-time code</string>
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fff242ea424e6781a7bf0cf9856cee85e3b70c9f59045ae542610335840bdd2d
size 32024
oid sha256:d36c312a81032f7ff11d385167c8f4bc707ef94c44674bb13bbd4b541723fab7
size 31809
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:993fdeda3f3945dec7aa5784c6247b340ff0698d67354125240d68c59495ae27
size 32920
oid sha256:ef39810f02477b86a784f985342ea2bffca04aa5a59a2301aa3839bd97482ec6
size 32667
@@ -20,17 +20,19 @@ package me.proton.core.devicemigration.presentation.intro
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
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.fillMaxHeight
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.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
@@ -51,10 +53,12 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
@@ -78,6 +82,8 @@ import me.proton.core.devicemigration.presentation.qr.rememberQrScanLauncher
import me.proton.core.domain.entity.Product
import me.proton.core.domain.entity.displayName
private val MAX_CONTENT_WIDTH = 520.dp
@Composable
internal fun SignInIntroScreen(
modifier: Modifier = Modifier,
@@ -141,7 +147,9 @@ internal fun SignInIntroScreen(
},
) { padding ->
Box(
modifier = Modifier.padding(padding)
modifier = Modifier
.padding(padding)
.fillMaxWidth()
) {
when (state) {
is SignInIntroState.MissingCameraPermission -> SingInIntroMissingCameraPermission(
@@ -149,7 +157,10 @@ internal fun SignInIntroScreen(
navigateToAppSettings = navigateToAppSettings,
onCameraPermissionGranted = onCameraPermissionGranted,
modifier = Modifier
.fillMaxSize()
.padding(ProtonDimens.DefaultSpacing)
.widthIn(max = MAX_CONTENT_WIDTH)
.fillMaxHeight()
.align(Alignment.Center)
.verticalScroll(rememberScrollState())
)
@@ -162,7 +173,10 @@ internal fun SignInIntroScreen(
isInteractionDisabled = state.shouldDisableInteraction(),
onStart = onStart,
modifier = Modifier
.fillMaxSize()
.padding(ProtonDimens.MediumSpacing)
.widthIn(max = MAX_CONTENT_WIDTH)
.fillMaxHeight()
.align(Alignment.Center)
.verticalScroll(rememberScrollState())
)
}
@@ -183,7 +197,6 @@ private fun SignInIntroEvents(
onBiometricAuthResult(SignInIntroAction.OnBiometricAuthResult(result))
}
val biometricsTitle = stringResource(R.string.intro_origin_biometrics_title)
val biometricsSubtitle = stringResource(R.string.intro_origin_biometrics_subtitle)
val biometricsCancelButton = stringResource(R.string.presentation_alert_cancel)
val retryLabel = stringResource(R.string.presentation_retry)
@@ -211,7 +224,7 @@ private fun SignInIntroEvents(
is SignInIntroEvent.LaunchBiometricsCheck -> biometricsLauncher.launch(
title = biometricsTitle,
subtitle = biometricsSubtitle,
subtitle = null,
cancelButton = biometricsCancelButton,
authenticatorsResolver = event.resolver
)
@@ -230,8 +243,23 @@ private fun SignInIntroContent(
onStart: () -> Unit,
modifier: Modifier = Modifier
) {
val hints = remember {
arrayOf(
R.string.intro_origin_sign_in_hint_1,
R.string.intro_origin_sign_in_hint_2,
R.string.intro_origin_sign_in_hint_3,
R.string.intro_origin_sign_in_hint_4
)
}
val tips = remember {
arrayOf(
R.string.intro_origin_sign_in_tip_1,
R.string.intro_origin_sign_in_tip_2,
R.string.intro_origin_sign_in_tip_3
)
}
Column(
modifier = modifier.padding(ProtonDimens.MediumSpacing)
modifier = modifier
) {
Image(
painter = painterResource(R.drawable.edm_intro_qr_scan_icon),
@@ -245,37 +273,21 @@ private fun SignInIntroContent(
color = LocalColors.current.textNorm,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(top = ProtonDimens.LargerSpacing)
.padding(vertical = ProtonDimens.LargerSpacing)
)
Text(
text = stringResource(R.string.intro_origin_sign_in_hint_1),
textAlign = TextAlign.Center,
style = LocalTypography.current.body2Regular,
color = LocalColors.current.textWeak,
modifier = Modifier
.padding(top = ProtonDimens.DefaultSpacing)
)
Text(
text = annotatedStringResource(R.string.intro_origin_sign_in_hint_2),
textAlign = TextAlign.Center,
style = LocalTypography.current.body2Regular,
color = LocalColors.current.textWeak,
modifier = Modifier
.padding(top = ProtonDimens.DefaultSpacing)
)
hints.forEach { hintResId ->
Text(
text = annotatedStringResource(hintResId),
style = LocalTypography.current.body2Regular,
color = LocalColors.current.textNorm,
modifier = Modifier.padding(bottom = ProtonDimens.SmallSpacing)
)
}
Spacer(modifier = Modifier.weight(1.0f))
TipBox(
text = R.string.intro_origin_sign_in_tip_1,
modifier = Modifier.padding(top = ProtonDimens.DefaultSpacing)
)
TipBox(
text = R.string.intro_origin_sign_in_tip_2,
modifier = Modifier.padding(top = ProtonDimens.SmallSpacing)
)
TipsBox(tips)
ProtonSolidButton(
onClick = onStart,
@@ -307,7 +319,7 @@ private fun SingInIntroMissingCameraPermission(
}
Column(
modifier = modifier.padding(ProtonDimens.DefaultSpacing)
modifier = modifier
) {
Image(
painterResource(R.drawable.edm_missing_camera_permission),
@@ -391,36 +403,56 @@ private fun SignInIntroVerifying(
@Composable
@Suppress("LongParameterList")
private fun TipBox(
@StringRes text: Int,
private fun TipsBox(
tips: Array<Int>,
modifier: Modifier = Modifier,
@DrawableRes icon: Int = R.drawable.ic_proton_lightbulb,
bgColor: Color = LocalColors.current.backgroundSecondary,
textColor: Color = LocalColors.current.textWeak,
textStyle: TextStyle = LocalTypography.current.body2Regular
textStyle: TextStyle = LocalTypography.current.overlineRegular
) {
Row(
Column(
modifier = modifier
.background(bgColor, RoundedCornerShape(ProtonDimens.ExtraLargeCornerRadius))
.padding(ProtonDimens.DefaultSpacing),
) {
Icon(
painter = painterResource(icon),
contentDescription = null,
tint = textColor,
)
Text(
text = stringResource(text),
modifier = Modifier.padding(start = ProtonDimens.SmallSpacing),
color = textColor,
style = textStyle
)
Row(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = ProtonDimens.ExtraSmallSpacing)
) {
Icon(
painter = painterResource(icon),
contentDescription = null,
tint = textColor,
modifier = Modifier.size(ProtonDimens.SmallIconSize)
)
Text(
text = stringResource(R.string.intro_origin_sign_in_tips),
modifier = Modifier
.padding(start = ProtonDimens.SmallSpacing)
.align(Alignment.CenterVertically),
color = textColor,
style = textStyle,
fontWeight = FontWeight.SemiBold
)
}
tips.forEach { tipRes ->
Text(
text = stringResource(tipRes),
modifier = Modifier.padding(top = ProtonDimens.SmallSpacing),
color = textColor,
style = textStyle
)
}
}
}
@Composable
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(device = Devices.PIXEL_FOLD)
private fun SignInIntroScreenPreview() {
ProtonTheme {
SignInIntroScreen(
@@ -433,6 +465,7 @@ private fun SignInIntroScreenPreview() {
@Composable
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(device = Devices.PIXEL_FOLD)
private fun SignInIntroNoCameraPermissionPreview() {
ProtonTheme {
SignInIntroScreen(
@@ -446,6 +479,7 @@ private fun SignInIntroNoCameraPermissionPreview() {
@Composable
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(device = Devices.PIXEL_FOLD)
private fun SignInIntroVerifyingScreenPreview() {
ProtonTheme {
SignInIntroScreen(
@@ -77,7 +77,7 @@ public fun SignInToAnotherDeviceItem(
is SignInToAnotherDeviceState.Hidden -> Unit
is SignInToAnotherDeviceState.Visible -> {
content(
label = stringResource(R.string.target_sign_in_to_another_device),
label = stringResource(R.string.intro_origin_sign_in_title),
onClick = { launcher.launch(DeviceMigrationInput(state.userId)) }
)
}
@@ -154,6 +154,14 @@ private fun SignInContent(
val qrBoxSize = remember { qrBitmapSize + 2 * ProtonDimens.LargeSpacing }
val emptyBitmap = remember { Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) }
var qrBitmap: Bitmap by remember { mutableStateOf(emptyBitmap) }
val instructions = remember {
arrayOf(
R.string.target_sign_in_instruction_1,
R.string.target_sign_in_instruction_2,
R.string.target_sign_in_instruction_3,
R.string.target_sign_in_instruction_4
)
}
LaunchedEffect(state, qrBitmapSize) {
qrBitmap = when (state) {
@@ -209,13 +217,22 @@ private fun SignInContent(
Text(
text = stringResource(R.string.target_sign_in_scan_code),
modifier = Modifier.padding(top = ProtonDimens.MediumSpacing),
style = ProtonTheme.typography.body2Regular,
)
Text(
text = annotatedStringResource(R.string.target_sign_in_instructions),
modifier = Modifier.padding(top = ProtonDimens.MediumSpacing),
style = ProtonTheme.typography.body2Regular,
style = ProtonTheme.typography.body1Medium,
color = ProtonTheme.colors.textWeak
)
instructions.forEach { stringRes ->
Text(
text = annotatedStringResource(stringRes),
modifier = Modifier.padding(
top = ProtonDimens.SmallSpacing,
start = ProtonDimens.SmallSpacing,
end = ProtonDimens.SmallSpacing
),
style = ProtonTheme.typography.body2Regular,
color = ProtonTheme.colors.textWeak
)
}
}
}
@@ -23,15 +23,18 @@
<string name="edm_missing_camera_permission_body">To sign in with QR code, you\'ll need to let %1$s access your camera. You can do this in your device settings.</string>
<string name="edm_missing_camera_permission_footer">You\'re always in control: You can change this anytime in your device settings.</string>
<string name="edm_missing_camera_permission_settings">Settings</string>
<string name="intro_origin_sign_in_title">Sign in to another device</string>
<string name="intro_origin_sign_in_subtitle">Sign in with your QR code</string>
<string name="intro_origin_sign_in_hint_1">Use this device to scan the QR code displayed on the device you want to sign in to. Both devices will stay active.</string>
<string name="intro_origin_sign_in_hint_2">To access the QR code, tap the <b>Sign in with QR code</b> button on the sign-in screen.</string>
<string name="intro_origin_sign_in_tip_1">Watch out for messages that contain a QR code. Never scan a QR code from a website or a person you dont trust.</string>
<string name="intro_origin_sign_in_tip_2">Proton will never ask you to scan a QR code that doesnt come from the domain proton.me.</string>
<string name="intro_origin_sign_in_title">Sign in on another device</string>
<string name="intro_origin_sign_in_subtitle">How to sign in with QR code</string>
<string name="intro_origin_sign_in_hint_1">1. Open any Proton app on the device you want to sign in to</string>
<string name="intro_origin_sign_in_hint_2">2. On the sign in screen, select <b>Forgot password</b> or <b>Trouble signing in?</b></string>
<string name="intro_origin_sign_in_hint_3">3. Select <b>Sign in with QR code</b>. A QR code will appear.</string>
<string name="intro_origin_sign_in_hint_4">4. On this device, press <b>Scan QR code</b> and scan the code.</string>
<string name="intro_origin_sign_in_tips">Security tips</string>
<string name="intro_origin_sign_in_tip_1">Be cautious of messages with QR codes. Only scan a code if you trust the source.</string>
<string name="intro_origin_sign_in_tip_2">Proton will never contact you unexpectedly asking you to sign in with a QR code.</string>
<string name="intro_origin_sign_in_tip_3">Proton will never ask you to scan a QR code thats not from <b>https://proton.me</b> or an official Proton app.</string>
<string name="intro_origin_sign_in_begin">Scan QR code</string>
<string name="intro_origin_biometrics_title">Verify that its you</string>
<string name="intro_origin_biometrics_subtitle">Unlock to sign in with this app.</string>
<string name="intro_origin_biometrics_title">Unlock your device to continue</string>
<string name="manual_code_input_title">Enter code manually</string>
<string name="manual_code_input_description">To access the code of the device you want to sign in to, tap the <b>Sign in with QR code</b> button in the help menu of the sign-in screen, and then tap <b>Enter key manually</b>.</string>
<string name="manual_code_input_code_label">Code</string>
@@ -42,17 +45,19 @@
<string name="origin_success_sign_out_hint">If you want, you can sign out of this device.</string>
<string name="origin_success_sign_out">Sign out</string>
<string name="origin_success_ack">Got it</string>
<string name="qr_code_scan_hint">Place QR code within the square</string>
<string name="qr_code_scan_hint">Position QR code inside the square</string>
<string name="qr_code_scan_enter_manually">Enter code manually</string>
<string name="target_sign_in_title">Sign in with another device</string>
<string name="target_sign_in_scan_code">Scan this code with your phone camera to sign in instantly.</string>
<string name="target_sign_in_instructions">1. Open the Proton app on your phone\n2. Tap into <b>Settings</b>, then tap <b>Sign in to another device</b>\n3. Tap <b>Scan QR code</b></string>
<string name="target_sign_in_error_title">Something went wrong</string>
<string name="target_sign_in_retryable_error">We couldn\'t sign you in.\nPlease scan a new QR code to try again.</string>
<string name="target_sign_in_passphrase_error">We couldn\'t sign you in.\nPlease sign in with your password.</string>
<string name="target_sign_in_error_new_qr">New QR code</string>
<string name="target_sign_in_title">Sign in with QR code</string>
<string name="target_sign_in_scan_code">How to sign in using another device</string>
<string name="target_sign_in_instruction_1">1. Get another device thats signed in to your Proton Account</string>
<string name="target_sign_in_instruction_2">2. Using that device, open any Proton app and select <b>Settings</b></string>
<string name="target_sign_in_instruction_3">3. Select <b>Sign in on another device → Scan QR code</b></string>
<string name="target_sign_in_instruction_4">4. Scan the code to sign in</string>
<string name="target_sign_in_error_title">We couldnt sign you in</string>
<string name="target_sign_in_retryable_error">Try generating a new QR code.</string>
<string name="target_sign_in_passphrase_error">Please try another sign in method.</string>
<string name="target_sign_in_error_new_qr">Generate QR code</string>
<string name="target_sign_in_error_back_to_signin">Back to sign-in</string>
<string name="target_sign_in_to_another_device">Sign in to another device</string>
<string name="target_sign_in_qr_code_failure">We couldn\'t load the QR code. Tap to retry.</string>
<string name="target_sign_in_no_connection">It looks like youre offline. Please check your connection and try again.</string>
</resources>
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:60db404ac9ec71e9093cc4323d93473b00390e63b4800343af829b10b93e2405
size 69113
oid sha256:c95383bdf87d0bdd8c0462d9735a9b0e3315b3a5cab3f13773f96939e62245d2
size 72249
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:345e06c2d3d63eb0d4bc1234ef65efc234f0c143d8e254cf1e988cb2b94d8829
size 72989
oid sha256:7c14031b4eef6c79c4ecf0582e07b2bdc491b728d3d04d45332af6f2650e0417
size 77677
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8a92ec21988e11ccde69b21d5a8e13888f1bf7be270d203e91df2ff80f12ecff
size 58619
oid sha256:132b3881c5bdb7e4e28d5ec24e909864f53b11e57f735637a25f6e9b24c74432
size 58431
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c69723eb98c10619434db64bb36f6e9991cedce2d632e184d39417ed33ae82cf
size 59622
oid sha256:ca497e1a326ef3c7ed330bfc39e54a1f9ae4ebdf21f103fd7e3d87944b54a09f
size 59551
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9d4f9611c7adcce41feb35777c3e5558c7e9e0c42ce81bd95a634d8dee62f68c
size 12378
oid sha256:84089ec2463cfe91fab1f78c178227191fb94a4ae8902df4725aa53850aacd3a
size 12358
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6dc2009dbdabe5515512eb549fa82714369d7a90a9915eddb1d4a8de3c482d13
size 12728
oid sha256:6dc42c835db67e6b3d021ae17646016f7127cfe1131558ead770498a3bcc9c83
size 12738
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2a17ff5455af5d620a25d194305d41a707d5a465a0ae0c2982ef62f7f4d4545b
size 26808
oid sha256:37e1edc19d8fe9e8c4c9b1319bdb2f842e5da790b1af2322b30af78f4792b617
size 34757
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f9c30f93c35f9cd6ac01509d176795516c1b4a9aa41ba18d40a54972713c2d7c
size 30780
oid sha256:58dfe76eeaa471f4147d42d0beef831f9b2fd7986d2e4bda1b55924dc966fb18
size 36694
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a28c31a8302ea057605b594c846282d22ee69b758c87c3a8d63db1b9980bd71c
size 26942
oid sha256:c314fae7a76e94d75d4d64729564c754d9b6f465a73d8bb6baf3ccb8290a11fa
size 34870
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3bbd1fd1b54b45b1511029d20dfd976f3d8f0c021224ef0b0cb0b9a7f22a02ca
size 30902
oid sha256:d4b5b260bf306ea61c8c27e7440273b1efa2cb27d9b0e1243e5ba1e1c8e55239
size 36817
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e779ca17bc669ee752e7b2d27cf8f6ce170c2b655a6ae847abbdb1b235b54da1
size 19736
oid sha256:f4931606c2030b8a3782107d49c807406afb8c6e610dcf9baa83966240b0da14
size 20153
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7c8f5cc859ea4a14037344a004c2f90ec4d48b27c0582aed7bb208134010c8ae
size 20558
oid sha256:50461fd6001227384e882f767a7d9102f5de0614023d4dfa9f309920a91212c1
size 20857
@@ -24,6 +24,7 @@ import android.content.res.Resources
import android.icu.text.PluralRules
import android.os.Build
import android.text.SpannableString
import android.text.style.StyleSpan
import androidx.annotation.RequiresApi
import androidx.test.core.app.ApplicationProvider
import org.junit.Before
@@ -288,6 +289,10 @@ abstract class BaseStringResourcesTest(
val textSpans = SpannableString(text).getAllSpans()
if (referenceSpans.size != textSpans.size) {
// We ignore the difference, if we only have styling spans (the translations aren't updated yet).
if (referenceSpans.all { it is StyleSpan } && textSpans.all { it is StyleSpan }) {
return null
}
return "mismatched spans, expected a set of ${referenceSpans.contentToString()}" +
" but got ${textSpans.contentToString()}"
}