mirror of
https://github.com/bitwarden/android.git
synced 2026-10-22 02:53:19 -05:00
BWA-160: Modernize QrCodeScanScreen (#5342)
This commit is contained in:
1 parent
76995a28ad
commit
2a8a16ab3f
4 files changed
+89
-130
No files matched your search
+81
-107
@@ -1,6 +1,5 @@
|
||||
package com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.widget.Toast
|
||||
@@ -21,7 +20,6 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -47,10 +45,7 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.CustomAccessibilityAction
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.LineBreak
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
@@ -65,8 +60,11 @@ import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogStat
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.authenticator.ui.platform.theme.LocalNonMaterialColors
|
||||
import com.bitwarden.authenticator.ui.platform.theme.clickableSpanStyle
|
||||
import com.bitwarden.authenticator.ui.platform.util.isPortrait
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.annotatedStringResource
|
||||
import com.bitwarden.ui.platform.base.util.spanStyleOf
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.util.asText
|
||||
import java.util.concurrent.Executors
|
||||
@@ -88,14 +86,11 @@ fun QrCodeScanScreen(
|
||||
qrCodeAnalyzer.onQrCodeScanned = remember(viewModel) {
|
||||
{ viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(it)) }
|
||||
}
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val orientation = LocalConfiguration.current.orientation
|
||||
val context = LocalContext.current
|
||||
|
||||
val onEnterCodeManuallyClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(QrCodeScanAction.ManualEntryTextClick) }
|
||||
}
|
||||
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
is QrCodeScanEvent.ShowToast -> {
|
||||
@@ -114,6 +109,19 @@ fun QrCodeScanScreen(
|
||||
}
|
||||
}
|
||||
|
||||
QrCodeScanDialogs(
|
||||
dialogState = state.dialog,
|
||||
onSaveHereClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(QrCodeScanAction.SaveLocallyClick(it)) }
|
||||
},
|
||||
onTakeMeToBitwardenClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(QrCodeScanAction.SaveToBitwardenClick(it)) }
|
||||
},
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(QrCodeScanAction.SaveToBitwardenErrorDismiss) }
|
||||
},
|
||||
)
|
||||
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
@@ -136,52 +144,46 @@ fun QrCodeScanScreen(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
)
|
||||
|
||||
when (orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> {
|
||||
LandscapeQRCodeContent(
|
||||
onEnterCodeManuallyClick = onEnterCodeManuallyClick,
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
)
|
||||
}
|
||||
if (LocalConfiguration.current.isPortrait) {
|
||||
PortraitQRCodeContent(
|
||||
onEnterCodeManuallyClick = onEnterCodeManuallyClick,
|
||||
modifier = Modifier.padding(paddingValues = innerPadding),
|
||||
)
|
||||
} else {
|
||||
LandscapeQRCodeContent(
|
||||
onEnterCodeManuallyClick = onEnterCodeManuallyClick,
|
||||
modifier = Modifier.padding(paddingValues = innerPadding),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
PortraitQRCodeContent(
|
||||
onEnterCodeManuallyClick = onEnterCodeManuallyClick,
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
)
|
||||
}
|
||||
@Composable
|
||||
private fun QrCodeScanDialogs(
|
||||
dialogState: QrCodeScanState.DialogState?,
|
||||
onSaveHereClick: (Boolean) -> Unit,
|
||||
onTakeMeToBitwardenClick: (Boolean) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
when (dialogState) {
|
||||
QrCodeScanState.DialogState.ChooseSaveLocation -> {
|
||||
ChooseSaveLocationDialog(
|
||||
onSaveHereClick = onSaveHereClick,
|
||||
onTakeMeToBitwardenClick = onTakeMeToBitwardenClick,
|
||||
)
|
||||
}
|
||||
|
||||
when (state.dialog) {
|
||||
QrCodeScanState.DialogState.ChooseSaveLocation -> {
|
||||
ChooseSaveLocationDialog(
|
||||
onSaveHereClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(QrCodeScanAction.SaveLocallyClick(it))
|
||||
}
|
||||
},
|
||||
onTakeMeToBitwardenClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(QrCodeScanAction.SaveToBitwardenClick(it))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
QrCodeScanState.DialogState.SaveToBitwardenError -> BitwardenBasicDialog(
|
||||
QrCodeScanState.DialogState.SaveToBitwardenError -> {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = R.string.something_went_wrong.asText(),
|
||||
message = R.string.please_try_again.asText(),
|
||||
),
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(QrCodeScanAction.SaveToBitwardenErrorDismiss)
|
||||
}
|
||||
},
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +208,7 @@ private fun PortraitQRCodeContent(
|
||||
.weight(1f)
|
||||
.fillMaxSize()
|
||||
.background(color = Color.Black.copy(alpha = .4f))
|
||||
.padding(horizontal = 16.dp)
|
||||
.standardHorizontalMargin()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Text(
|
||||
@@ -219,6 +221,7 @@ private fun PortraitQRCodeContent(
|
||||
|
||||
BottomClickableText(
|
||||
onEnterCodeManuallyClick = onEnterCodeManuallyClick,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
@@ -246,8 +249,7 @@ private fun LandscapeQRCodeContent(
|
||||
.weight(1f)
|
||||
.fillMaxSize()
|
||||
.background(color = Color.Black.copy(alpha = .4f))
|
||||
.padding(horizontal = 16.dp)
|
||||
.navigationBarsPadding()
|
||||
.standardHorizontalMargin()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Text(
|
||||
@@ -260,6 +262,7 @@ private fun LandscapeQRCodeContent(
|
||||
BottomClickableText(
|
||||
onEnterCodeManuallyClick = onEnterCodeManuallyClick,
|
||||
)
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,7 +303,7 @@ private fun CameraPreview(
|
||||
|
||||
val preview = Preview.Builder()
|
||||
.build()
|
||||
.apply { setSurfaceProvider(previewView.surfaceProvider) }
|
||||
.apply { surfaceProvider = previewView.surfaceProvider }
|
||||
|
||||
// Unbind from the camera provider when we leave the screen.
|
||||
DisposableEffect(Unit) {
|
||||
@@ -330,7 +333,7 @@ private fun CameraPreview(
|
||||
preview,
|
||||
imageAnalyzer,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
cameraErrorReceive()
|
||||
}
|
||||
}
|
||||
@@ -443,61 +446,32 @@ private fun BottomClickableText(
|
||||
onEnterCodeManuallyClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val cannotScanText = stringResource(id = R.string.cannot_scan_qr_code)
|
||||
val enterKeyText = stringResource(id = R.string.enter_key_manually)
|
||||
val clickableStyle = clickableSpanStyle()
|
||||
val manualTextColor = LocalNonMaterialColors.current.qrCodeClickableText
|
||||
|
||||
val customTitleLineBreak = LineBreak(
|
||||
strategy = LineBreak.Strategy.Balanced,
|
||||
strictness = LineBreak.Strictness.Strict,
|
||||
wordBreak = LineBreak.WordBreak.Phrase,
|
||||
)
|
||||
|
||||
val annotatedString = remember {
|
||||
buildAnnotatedString {
|
||||
withStyle(style = clickableStyle.copy(color = Color.White)) {
|
||||
pushStringAnnotation(
|
||||
tag = cannotScanText,
|
||||
annotation = cannotScanText,
|
||||
)
|
||||
append(cannotScanText)
|
||||
}
|
||||
|
||||
append(" ")
|
||||
|
||||
withStyle(style = clickableStyle.copy(color = manualTextColor)) {
|
||||
pushStringAnnotation(tag = enterKeyText, annotation = enterKeyText)
|
||||
append(enterKeyText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = annotatedString,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
textAlign = TextAlign.Center,
|
||||
lineBreak = customTitleLineBreak,
|
||||
),
|
||||
onClick = { offset ->
|
||||
annotatedString
|
||||
.getStringAnnotations(
|
||||
tag = enterKeyText,
|
||||
start = offset,
|
||||
end = offset,
|
||||
)
|
||||
.firstOrNull()
|
||||
?.let { onEnterCodeManuallyClick.invoke() }
|
||||
},
|
||||
modifier = modifier
|
||||
.semantics {
|
||||
CustomAccessibilityAction(
|
||||
label = enterKeyText,
|
||||
action = {
|
||||
onEnterCodeManuallyClick.invoke()
|
||||
true
|
||||
},
|
||||
)
|
||||
Text(
|
||||
text = annotatedStringResource(
|
||||
id = R.string.cannot_scan_qr_code_enter_key_manually,
|
||||
linkHighlightStyle = spanStyleOf(
|
||||
color = LocalNonMaterialColors.current.qrCodeClickableText,
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
),
|
||||
style = spanStyleOf(
|
||||
color = Color.White,
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
),
|
||||
onAnnotationClick = {
|
||||
when (it) {
|
||||
"enterKeyManually" -> onEnterCodeManuallyClick()
|
||||
}
|
||||
},
|
||||
),
|
||||
modifier = modifier.semantics {
|
||||
CustomAccessibilityAction(
|
||||
label = enterKeyText,
|
||||
action = {
|
||||
onEnterCodeManuallyClick()
|
||||
true
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.theme
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
|
||||
/**
|
||||
* Defines a span style for clickable span texts. Useful because spans require a
|
||||
* [SpanStyle] instead of the typical [TextStyle].
|
||||
*/
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun clickableSpanStyle(): SpanStyle = SpanStyle(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
|
||||
fontWeight = MaterialTheme.typography.bodyMedium.fontWeight,
|
||||
fontStyle = MaterialTheme.typography.bodyMedium.fontStyle,
|
||||
fontFamily = MaterialTheme.typography.bodyMedium.fontFamily,
|
||||
)
|
||||
@@ -18,7 +18,7 @@
|
||||
<string name="scan_a_qr_code">Scan a QR code</string>
|
||||
<string name="scan_qr_code"><annotation link="scanQrCode">Scan QR code</annotation></string>
|
||||
<string name="point_your_camera_at_the_qr_code">Point your camera at the QR code.</string>
|
||||
<string name="cannot_scan_qr_code">Cannot scan QR code.</string>
|
||||
<string name="cannot_scan_qr_code_enter_key_manually">Cannot scan QR code? <annotation link="enterKeyManually">Enter key manually</annotation></string>
|
||||
<string name="enter_key_manually">Enter key manually</string>
|
||||
<string name="cannot_add_authenticator_key">Cannot add authenticator key?</string>
|
||||
<string name="no_thanks">No thanks</string>
|
||||
|
||||
+7
-1
@@ -28,7 +28,7 @@ class QrCodeScanScreenTest : AuthenticatorComposeTest() {
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<QrCodeScanEvent>()
|
||||
|
||||
val viewModel: QrCodeScanViewModel = mockk {
|
||||
private val viewModel: QrCodeScanViewModel = mockk {
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
every { trySendAction(any()) } just runs
|
||||
@@ -54,6 +54,12 @@ class QrCodeScanScreenTest : AuthenticatorComposeTest() {
|
||||
assertTrue(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on NavigateToManualCodeEntry event receive should call navigate to manual code entry`() {
|
||||
mutableEventFlow.tryEmit(QrCodeScanEvent.NavigateToManualCodeEntry)
|
||||
assertTrue(onNavigateToManualCodeEntryScreenCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Save here click should send SaveLocallyClick action`() {
|
||||
mutableStateFlow.update {
|
||||
|
||||
Reference in new issue
Block a user