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 {