Skip to content

Commit be951b7

Browse files
committed
[PM-34126] feat: Add card scan screen
Add the card scanner screen with camera integration: - CardScanManager: shared result channel between scanner and consumer - CardScanViewModel: state management for scan screen - CardScanScreen: composable with camera preview and overlay - CardScanNavigation: route and navigation helper - CardScanner feature flag gated behind card-scanner-mobile
1 parent 2900099 commit be951b7

File tree

16 files changed

+586
-189
lines changed

16 files changed

+586
-189
lines changed

app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import com.bitwarden.network.service.CiphersService
1010
import com.bitwarden.network.service.FolderService
1111
import com.bitwarden.network.service.SendsService
1212
import com.bitwarden.network.service.SyncService
13+
import com.bitwarden.ui.platform.feature.cardscanner.manager.CardScanManager
14+
import com.bitwarden.ui.platform.feature.cardscanner.manager.CardScanManagerImpl
1315
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
1416
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
1517
import com.x8bit.bitwarden.data.auth.manager.KdfManager
@@ -60,6 +62,10 @@ import javax.inject.Singleton
6062
@InstallIn(SingletonComponent::class)
6163
object VaultManagerModule {
6264

65+
@Provides
66+
@Singleton
67+
fun provideCardScanManager(): CardScanManager = CardScanManagerImpl()
68+
6369
@Provides
6470
@Singleton
6571
fun provideVaultMigrationManager(

app/src/main/kotlin/com/x8bit/bitwarden/data/vault/util/CardNumberUtils.kt

Lines changed: 0 additions & 94 deletions
This file was deleted.

app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/composition/LocalManagerProvider.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@ import com.bitwarden.cxf.validator.CredentialExchangeRequestValidator
2424
import com.bitwarden.cxf.validator.dsl.credentialExchangeRequestValidator
2525
import com.bitwarden.ui.platform.composition.LocalExitManager
2626
import com.bitwarden.ui.platform.composition.LocalIntentManager
27+
import com.bitwarden.ui.platform.composition.LocalCardTextAnalyzer
2728
import com.bitwarden.ui.platform.composition.LocalQrCodeAnalyzer
29+
import com.bitwarden.ui.platform.feature.cardscanner.util.CardDataParser
30+
import com.bitwarden.ui.platform.feature.cardscanner.util.CardDataParserImpl
31+
import com.bitwarden.ui.platform.feature.cardscanner.util.CardTextAnalyzer
32+
import com.bitwarden.ui.platform.feature.cardscanner.util.CardTextAnalyzerImpl
2833
import com.bitwarden.ui.platform.feature.qrcodescan.util.QrCodeAnalyzer
2934
import com.bitwarden.ui.platform.feature.qrcodescan.util.QrCodeAnalyzerImpl
3035
import com.bitwarden.ui.platform.manager.IntentManager
@@ -84,6 +89,10 @@ fun LocalManagerProvider(
8489
credentialExchangeRequestValidator: CredentialExchangeRequestValidator =
8590
credentialExchangeRequestValidator(activity = activity),
8691
authTabLaunchers: AuthTabLaunchers,
92+
cardDataParser: CardDataParser = CardDataParserImpl(),
93+
cardTextAnalyzer: CardTextAnalyzer = CardTextAnalyzerImpl(
94+
cardDataParser = cardDataParser,
95+
),
8796
qrCodeAnalyzer: QrCodeAnalyzer = QrCodeAnalyzerImpl(),
8897
content: @Composable () -> Unit,
8998
) {
@@ -103,6 +112,7 @@ fun LocalManagerProvider(
103112
LocalCredentialExchangeCompletionManager provides credentialExchangeCompletionManager,
104113
LocalCredentialExchangeRequestValidator provides credentialExchangeRequestValidator,
105114
LocalAuthTabLaunchers provides authTabLaunchers,
115+
LocalCardTextAnalyzer provides cardTextAnalyzer,
106116
LocalQrCodeAnalyzer provides qrCodeAnalyzer,
107117
content = content,
108118
)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.x8bit.bitwarden.ui.vault.feature.cardscanner
2+
3+
import androidx.navigation.NavController
4+
import androidx.navigation.NavGraphBuilder
5+
import androidx.navigation.NavOptions
6+
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
7+
import kotlinx.serialization.Serializable
8+
9+
/**
10+
* The type-safe route for the card scan screen.
11+
*/
12+
@Serializable
13+
data object CardScanRoute
14+
15+
/**
16+
* Add the card scan screen to the nav graph.
17+
*/
18+
fun NavGraphBuilder.cardScanDestination(
19+
onNavigateBack: () -> Unit,
20+
) {
21+
composableWithSlideTransitions<CardScanRoute> {
22+
CardScanScreen(
23+
onNavigateBack = onNavigateBack,
24+
)
25+
}
26+
}
27+
28+
/**
29+
* Navigate to the card scan screen.
30+
*/
31+
fun NavController.navigateToCardScanScreen(
32+
navOptions: NavOptions? = null,
33+
) {
34+
this.navigate(route = CardScanRoute, navOptions = navOptions)
35+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package com.x8bit.bitwarden.ui.vault.feature.cardscanner
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.Spacer
7+
import androidx.compose.foundation.layout.fillMaxSize
8+
import androidx.compose.foundation.layout.navigationBarsPadding
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.material3.ExperimentalMaterial3Api
11+
import androidx.compose.material3.Text
12+
import androidx.compose.material3.TopAppBarDefaults
13+
import androidx.compose.material3.rememberTopAppBarState
14+
import androidx.compose.runtime.Composable
15+
import androidx.compose.runtime.CompositionLocalProvider
16+
import androidx.compose.ui.Alignment
17+
import androidx.compose.ui.Modifier
18+
import androidx.compose.ui.res.stringResource
19+
import androidx.compose.ui.text.style.TextAlign
20+
import androidx.compose.ui.unit.dp
21+
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
22+
import com.bitwarden.ui.platform.base.util.EventsEffect
23+
import com.bitwarden.ui.platform.base.util.StatusBarsAppearanceAffect
24+
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
25+
import com.bitwarden.ui.platform.components.camera.CameraPreview
26+
import com.bitwarden.ui.platform.components.camera.CardScanOverlay
27+
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
28+
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
29+
import com.bitwarden.ui.platform.composition.LocalCardTextAnalyzer
30+
import com.bitwarden.ui.platform.feature.cardscanner.util.CardTextAnalyzer
31+
import com.bitwarden.ui.platform.resource.BitwardenDrawable
32+
import com.bitwarden.ui.platform.resource.BitwardenString
33+
import com.bitwarden.ui.platform.theme.BitwardenTheme
34+
import com.bitwarden.ui.platform.theme.LocalBitwardenColorScheme
35+
import com.bitwarden.ui.platform.theme.color.darkBitwardenColorScheme
36+
37+
/**
38+
* The screen to scan credit cards for the application.
39+
*/
40+
@Suppress("LongMethod")
41+
@OptIn(ExperimentalMaterial3Api::class)
42+
@Composable
43+
fun CardScanScreen(
44+
onNavigateBack: () -> Unit,
45+
viewModel: CardScanViewModel = hiltViewModel(),
46+
cardTextAnalyzer: CardTextAnalyzer = LocalCardTextAnalyzer.current,
47+
) {
48+
cardTextAnalyzer.onCardScanned = { cardScanData ->
49+
viewModel.trySendAction(
50+
CardScanAction.CardScanReceive(cardScanData = cardScanData),
51+
)
52+
}
53+
54+
EventsEffect(viewModel = viewModel) { event ->
55+
when (event) {
56+
is CardScanEvent.NavigateBack -> onNavigateBack()
57+
}
58+
}
59+
60+
// This screen should always look like it's in dark mode
61+
CompositionLocalProvider(
62+
LocalBitwardenColorScheme provides darkBitwardenColorScheme,
63+
) {
64+
StatusBarsAppearanceAffect()
65+
BitwardenScaffold(
66+
modifier = Modifier.fillMaxSize(),
67+
topBar = {
68+
BitwardenTopAppBar(
69+
title = stringResource(id = BitwardenString.scan_card),
70+
navigationIcon = rememberVectorPainter(
71+
id = BitwardenDrawable.ic_close,
72+
),
73+
navigationIconContentDescription = stringResource(
74+
id = BitwardenString.close,
75+
),
76+
onNavigationIconClick = {
77+
viewModel.trySendAction(CardScanAction.CloseClick)
78+
},
79+
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(
80+
state = rememberTopAppBarState(),
81+
),
82+
)
83+
},
84+
) {
85+
CameraPreview(
86+
cameraErrorReceive = {
87+
viewModel.trySendAction(
88+
CardScanAction.CameraSetupErrorReceive,
89+
)
90+
},
91+
analyzer = cardTextAnalyzer,
92+
modifier = Modifier.fillMaxSize(),
93+
)
94+
Column(
95+
horizontalAlignment = Alignment.CenterHorizontally,
96+
modifier = Modifier.fillMaxSize(),
97+
) {
98+
CardScanOverlay(
99+
overlayWidth = 300.dp,
100+
modifier = Modifier.weight(2f),
101+
)
102+
103+
Column(
104+
horizontalAlignment = Alignment.CenterHorizontally,
105+
verticalArrangement = Arrangement.SpaceAround,
106+
modifier = Modifier
107+
.weight(1f)
108+
.fillMaxSize()
109+
.background(
110+
color = BitwardenTheme
111+
.colorScheme
112+
.background
113+
.scrim,
114+
)
115+
.padding(horizontal = 16.dp),
116+
) {
117+
Text(
118+
text = stringResource(
119+
id = BitwardenString.scan_card_instruction,
120+
),
121+
textAlign = TextAlign.Center,
122+
color = BitwardenTheme.colorScheme.text.primary,
123+
style = BitwardenTheme.typography.bodyMedium,
124+
modifier = Modifier.padding(horizontal = 16.dp),
125+
)
126+
Spacer(modifier = Modifier.navigationBarsPadding())
127+
}
128+
}
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)