diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/qrcodescan/QrCodeScanScreen.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/qrcodescan/QrCodeScanScreen.kt
index f940e04465..dadba77dfc 100644
--- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/qrcodescan/QrCodeScanScreen.kt
+++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/qrcodescan/QrCodeScanScreen.kt
@@ -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
+ },
+ )
+ },
)
}
diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/theme/SpanStyles.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/theme/SpanStyles.kt
deleted file mode 100644
index 9f172558dd..0000000000
--- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/theme/SpanStyles.kt
+++ /dev/null
@@ -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,
-)
diff --git a/authenticator/src/main/res/values/strings.xml b/authenticator/src/main/res/values/strings.xml
index 0d7ff97153..96b4f58586 100644
--- a/authenticator/src/main/res/values/strings.xml
+++ b/authenticator/src/main/res/values/strings.xml
@@ -18,7 +18,7 @@
Scan a QR code
Scan QR code
Point your camera at the QR code.
- Cannot scan QR code.
+ Cannot scan QR code? Enter key manually
Enter key manually
Cannot add authenticator key?
No thanks
diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/qrcodescan/QrCodeScanScreenTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/qrcodescan/QrCodeScanScreenTest.kt
index cf164e0bc2..16166315ea 100644
--- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/qrcodescan/QrCodeScanScreenTest.kt
+++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/qrcodescan/QrCodeScanScreenTest.kt
@@ -28,7 +28,7 @@ class QrCodeScanScreenTest : AuthenticatorComposeTest() {
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val mutableEventFlow = bufferedMutableSharedFlow()
- 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 {