Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import com.bitwarden.network.service.CiphersService
import com.bitwarden.network.service.FolderService
import com.bitwarden.network.service.SendsService
import com.bitwarden.network.service.SyncService
import com.bitwarden.ui.platform.feature.cardscanner.manager.CardScanManager
import com.bitwarden.ui.platform.feature.cardscanner.manager.CardScanManagerImpl
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.manager.KdfManager
Expand Down Expand Up @@ -60,6 +62,10 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
object VaultManagerModule {

@Provides
@Singleton
fun provideCardScanManager(): CardScanManager = CardScanManagerImpl()

@Provides
@Singleton
fun provideVaultMigrationManager(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ import com.bitwarden.cxf.validator.CredentialExchangeRequestValidator
import com.bitwarden.cxf.validator.dsl.credentialExchangeRequestValidator
import com.bitwarden.ui.platform.composition.LocalExitManager
import com.bitwarden.ui.platform.composition.LocalIntentManager
import com.bitwarden.ui.platform.composition.LocalCardTextAnalyzer
import com.bitwarden.ui.platform.composition.LocalQrCodeAnalyzer
import com.bitwarden.ui.platform.feature.cardscanner.util.CardDataParser
import com.bitwarden.ui.platform.feature.cardscanner.util.CardDataParserImpl
import com.bitwarden.ui.platform.feature.cardscanner.util.CardTextAnalyzer
import com.bitwarden.ui.platform.feature.cardscanner.util.CardTextAnalyzerImpl
import com.bitwarden.ui.platform.feature.qrcodescan.util.QrCodeAnalyzer
import com.bitwarden.ui.platform.feature.qrcodescan.util.QrCodeAnalyzerImpl
import com.bitwarden.ui.platform.manager.IntentManager
Expand Down Expand Up @@ -84,6 +89,10 @@ fun LocalManagerProvider(
credentialExchangeRequestValidator: CredentialExchangeRequestValidator =
credentialExchangeRequestValidator(activity = activity),
authTabLaunchers: AuthTabLaunchers,
cardDataParser: CardDataParser = CardDataParserImpl(),
cardTextAnalyzer: CardTextAnalyzer = CardTextAnalyzerImpl(
cardDataParser = cardDataParser,
),
qrCodeAnalyzer: QrCodeAnalyzer = QrCodeAnalyzerImpl(),
content: @Composable () -> Unit,
) {
Expand All @@ -103,6 +112,7 @@ fun LocalManagerProvider(
LocalCredentialExchangeCompletionManager provides credentialExchangeCompletionManager,
LocalCredentialExchangeRequestValidator provides credentialExchangeRequestValidator,
LocalAuthTabLaunchers provides authTabLaunchers,
LocalCardTextAnalyzer provides cardTextAnalyzer,
LocalQrCodeAnalyzer provides qrCodeAnalyzer,
content = content,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.x8bit.bitwarden.ui.vault.feature.cardscanner

import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import kotlinx.serialization.Serializable

/**
* The type-safe route for the card scan screen.
*/
@Serializable
data object CardScanRoute

/**
* Add the card scan screen to the nav graph.
*/
fun NavGraphBuilder.cardScanDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions<CardScanRoute> {
CardScanScreen(
onNavigateBack = onNavigateBack,
)
}
}

/**
* Navigate to the card scan screen.
*/
fun NavController.navigateToCardScanScreen(
navOptions: NavOptions? = null,
) {
this.navigate(route = CardScanRoute, navOptions = navOptions)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package com.x8bit.bitwarden.ui.vault.feature.cardscanner

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.StatusBarsAppearanceAffect
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.camera.CameraPreview
import com.bitwarden.ui.platform.components.camera.CardScanOverlay
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.composition.LocalCardTextAnalyzer
import com.bitwarden.ui.platform.feature.cardscanner.util.CardTextAnalyzer
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.platform.theme.LocalBitwardenColorScheme
import com.bitwarden.ui.platform.theme.color.darkBitwardenColorScheme

/**
* The screen to scan credit cards for the application.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CardScanScreen(
onNavigateBack: () -> Unit,
viewModel: CardScanViewModel = hiltViewModel(),
cardTextAnalyzer: CardTextAnalyzer = LocalCardTextAnalyzer.current,
) {
cardTextAnalyzer.onCardScanned = { cardScanData ->
viewModel.trySendAction(
CardScanAction.CardScanReceive(cardScanData = cardScanData),
)
}

EventsEffect(viewModel = viewModel) { event ->
when (event) {
is CardScanEvent.NavigateBack -> onNavigateBack()
}
}

// This screen should always look like it's in dark mode
CompositionLocalProvider(
LocalBitwardenColorScheme provides darkBitwardenColorScheme,
) {

Check warning on line 63 in app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/cardscanner/CardScanScreen.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This lambda has 65 lines of code, which is greater than the 20 authorized. Split it into smaller functions.

See more on https://sonarcloud.io/project/issues?id=bitwarden_android&issues=AZ0p-7-tFd_qGgbzJEQf&open=AZ0p-7-tFd_qGgbzJEQf&pullRequest=6721
StatusBarsAppearanceAffect()
BitwardenScaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
BitwardenTopAppBar(
title = stringResource(id = BitwardenString.scan_card),
navigationIcon = rememberVectorPainter(
id = BitwardenDrawable.ic_close,
),
navigationIconContentDescription = stringResource(
id = BitwardenString.close,
),
onNavigationIconClick = {
viewModel.trySendAction(CardScanAction.CloseClick)
},
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(
state = rememberTopAppBarState(),
),
)
},
) {

Check warning on line 84 in app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/cardscanner/CardScanScreen.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This lambda has 43 lines of code, which is greater than the 20 authorized. Split it into smaller functions.

See more on https://sonarcloud.io/project/issues?id=bitwarden_android&issues=AZ0p-7-tFd_qGgbzJEQg&open=AZ0p-7-tFd_qGgbzJEQg&pullRequest=6721
CameraPreview(
cameraErrorReceive = {
viewModel.trySendAction(
CardScanAction.CameraSetupErrorReceive,
)
},
analyzer = cardTextAnalyzer,
modifier = Modifier.fillMaxSize(),
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize(),
) {

Check warning on line 97 in app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/cardscanner/CardScanScreen.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This lambda has 29 lines of code, which is greater than the 20 authorized. Split it into smaller functions.

See more on https://sonarcloud.io/project/issues?id=bitwarden_android&issues=AZ0p-7-tFd_qGgbzJEQh&open=AZ0p-7-tFd_qGgbzJEQh&pullRequest=6721
CardScanOverlay(
overlayWidth = 300.dp,
modifier = Modifier.weight(2f),
)

Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceAround,
modifier = Modifier
.weight(1f)
.fillMaxSize()
.background(
color = BitwardenTheme
.colorScheme
.background
.scrim,
)
.padding(horizontal = 16.dp),
) {
Text(
text = stringResource(
id = BitwardenString.scan_card_instruction,
),
textAlign = TextAlign.Center,
color = BitwardenTheme.colorScheme.text.primary,
style = BitwardenTheme.typography.bodyMedium,
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.x8bit.bitwarden.ui.vault.feature.cardscanner

import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.base.DeferredBackgroundEvent
import com.bitwarden.ui.platform.feature.cardscanner.util.CardScanData
import com.bitwarden.ui.platform.feature.cardscanner.manager.CardScanManager
import com.bitwarden.ui.platform.feature.cardscanner.util.CardScanResult
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize
import javax.inject.Inject

private const val KEY_STATE = "state"

/**
* Handles [CardScanAction] and launches [CardScanEvent] for the [CardScanScreen].
*/
@HiltViewModel
class CardScanViewModel @Inject constructor(
private val cardScanManager: CardScanManager,
savedStateHandle: SavedStateHandle,
) : BaseViewModel<CardScanState, CardScanEvent, CardScanAction>(
initialState = savedStateHandle[KEY_STATE]
?: CardScanState(hasHandledScan = false),
) {
override fun handleAction(action: CardScanAction) {
when (action) {
is CardScanAction.CloseClick -> handleCloseClick()
is CardScanAction.CameraSetupErrorReceive -> handleCameraErrorReceive()
is CardScanAction.CardScanReceive -> handleCardScanReceive(action)
}
}

private fun handleCloseClick() {
sendEvent(CardScanEvent.NavigateBack)
}

private fun handleCameraErrorReceive() {
cardScanManager.emitCardScanResult(CardScanResult.ScanError())
sendEvent(CardScanEvent.NavigateBack)
}

private fun handleCardScanReceive(action: CardScanAction.CardScanReceive) {
if (state.hasHandledScan) return
mutableStateFlow.update { it.copy(hasHandledScan = true) }
cardScanManager.emitCardScanResult(
CardScanResult.Success(cardScanData = action.cardScanData),
)
sendEvent(CardScanEvent.NavigateBack)
}
}

/**
* Models events for the [CardScanScreen].
*/
sealed class CardScanEvent {

/**
* Navigate back. Added [DeferredBackgroundEvent] as scan might fire before
* events are consumed.
*/
data object NavigateBack : CardScanEvent(), DeferredBackgroundEvent
}

/**
* Models actions for the [CardScanScreen].
*/
sealed class CardScanAction {

/**
* User clicked close.
*/
data object CloseClick : CardScanAction()

/**
* A card has been scanned with the detected fields.
*/
data class CardScanReceive(
val cardScanData: CardScanData,
) : CardScanAction()

/**
* The camera is unable to be set up.
*/
data object CameraSetupErrorReceive : CardScanAction()
}

/**
* Represents the state of the card scan screen.
*/
@Parcelize
data class CardScanState(
val hasHandledScan: Boolean,
) : Parcelable
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.bitwarden.cxf.importer.CredentialExchangeImporter
import com.bitwarden.cxf.manager.CredentialExchangeCompletionManager
import com.bitwarden.cxf.validator.CredentialExchangeRequestValidator
import com.bitwarden.ui.platform.base.BaseComposeTest
import com.bitwarden.ui.platform.feature.cardscanner.util.CardTextAnalyzer
import com.bitwarden.ui.platform.feature.qrcodescan.util.QrCodeAnalyzer
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.ui.platform.manager.IntentManager
Expand Down Expand Up @@ -49,6 +50,7 @@ abstract class BitwardenComposeTest : BaseComposeTest() {
credentialExchangeImporter: CredentialExchangeImporter = mockk(),
credentialExchangeCompletionManager: CredentialExchangeCompletionManager = mockk(),
credentialExchangeRequestValidator: CredentialExchangeRequestValidator = mockk(),
cardTextAnalyzer: CardTextAnalyzer = mockk(),
qrCodeAnalyzer: QrCodeAnalyzer = mockk(),
test: @Composable () -> Unit,
) {
Expand All @@ -69,6 +71,7 @@ abstract class BitwardenComposeTest : BaseComposeTest() {
credentialExchangeImporter = credentialExchangeImporter,
credentialExchangeCompletionManager = credentialExchangeCompletionManager,
credentialExchangeRequestValidator = credentialExchangeRequestValidator,
cardTextAnalyzer = cardTextAnalyzer,
qrCodeAnalyzer = qrCodeAnalyzer,
) {
BitwardenTheme(
Expand Down
Loading
Loading