Skip to content

Commit 583a110

Browse files
committed
[PM-34127] feat: Integrate card scanner with VaultAddEdit
Wire the card scanner into the card cipher add/edit flow: - VaultAddEditViewModel: inject CardScanManager, handle scan results, populate card fields with brand-aware CVV validation - VaultAddEditScreen: pass scanner enabled state and click handler - VaultAddEditCardItems: conditionally render Scan Card button - VaultUnlockedNavigation: register card scan route
1 parent be951b7 commit 583a110

File tree

9 files changed

+414
-1
lines changed

9 files changed

+414
-1
lines changed

app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ import com.x8bit.bitwarden.ui.vault.feature.importlogins.navigateToImportLoginsS
5959
import com.x8bit.bitwarden.ui.vault.feature.item.navigateToVaultItem
6060
import com.x8bit.bitwarden.ui.vault.feature.item.vaultItemDestination
6161
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.vaultItemListingDestinationAsRoot
62+
import com.x8bit.bitwarden.ui.vault.feature.cardscanner.cardScanDestination
63+
import com.x8bit.bitwarden.ui.vault.feature.cardscanner.navigateToCardScanScreen
6264
import com.x8bit.bitwarden.ui.vault.feature.manualcodeentry.navigateToManualCodeEntryScreen
6365
import com.x8bit.bitwarden.ui.vault.feature.manualcodeentry.vaultManualCodeEntryDestination
6466
import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.navigateToVaultMoveToOrganization
@@ -169,6 +171,9 @@ fun NavGraphBuilder.vaultUnlockedGraph(
169171
onNavigateToQrCodeScanScreen = {
170172
navController.navigateToQrCodeScanScreen()
171173
},
174+
onNavigateToCardScanScreen = {
175+
navController.navigateToCardScanScreen()
176+
},
172177
onNavigateToManualCodeEntryScreen = {
173178
navController.navigateToManualCodeEntryScreen()
174179
},
@@ -208,6 +213,9 @@ fun NavGraphBuilder.vaultUnlockedGraph(
208213
)
209214
},
210215
)
216+
cardScanDestination(
217+
onNavigateBack = { navController.popBackStack() },
218+
)
211219
vaultQrCodeScanDestination(
212220
onNavigateToManualCodeEntryScreen = {
213221
navController.popBackStack()

app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditCardItems.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import androidx.compose.ui.text.input.KeyboardType
1717
import androidx.compose.ui.unit.dp
1818
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
1919
import com.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
20+
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
2021
import com.bitwarden.ui.platform.components.field.BitwardenPasswordField
2122
import com.bitwarden.ui.platform.components.field.BitwardenTextField
2223
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
@@ -34,6 +35,8 @@ import kotlinx.collections.immutable.toImmutableList
3435
@Suppress("LongMethod")
3536
fun LazyListScope.vaultAddEditCardItems(
3637
cardState: VaultAddEditState.ViewState.Content.ItemType.Card,
38+
isCardScannerEnabled: Boolean,
39+
onScanCardClick: () -> Unit,
3740
cardHandlers: VaultAddEditCardTypeHandlers,
3841
) {
3942
item {
@@ -47,6 +50,20 @@ fun LazyListScope.vaultAddEditCardItems(
4750
)
4851
}
4952

53+
if (isCardScannerEnabled) {
54+
item {
55+
Spacer(modifier = Modifier.height(8.dp))
56+
BitwardenOutlinedButton(
57+
label = stringResource(id = BitwardenString.scan_card),
58+
onClick = onScanCardClick,
59+
modifier = Modifier
60+
.testTag("ScanCardButton")
61+
.fillMaxWidth()
62+
.standardHorizontalMargin(),
63+
)
64+
}
65+
}
66+
5067
item {
5168
Spacer(modifier = Modifier.height(8.dp))
5269
BitwardenTextField(

app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditItemContent.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ fun CoachMarkScope<AddEditItemCoachMark>.VaultAddEditContent(
5151
identityItemTypeHandlers: VaultAddEditIdentityTypeHandlers,
5252
cardItemTypeHandlers: VaultAddEditCardTypeHandlers,
5353
sshKeyItemTypeHandlers: VaultAddEditSshKeyTypeHandlers,
54+
isCardScannerEnabled: Boolean,
5455
modifier: Modifier = Modifier,
5556
lazyListState: LazyListState,
5657
permissionsManager: PermissionsManager,
@@ -64,7 +65,10 @@ fun CoachMarkScope<AddEditItemCoachMark>.VaultAddEditContent(
6465
onResult = { isGranted ->
6566
when (state.type) {
6667
is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> Unit
67-
is VaultAddEditState.ViewState.Content.ItemType.Card -> Unit
68+
is VaultAddEditState.ViewState.Content.ItemType.Card -> {
69+
cardItemTypeHandlers.onScanCardClick(isGranted)
70+
}
71+
6872
is VaultAddEditState.ViewState.Content.ItemType.Identity -> Unit
6973
is VaultAddEditState.ViewState.Content.ItemType.SshKey -> Unit
7074
is VaultAddEditState.ViewState.Content.ItemType.Login -> {
@@ -236,6 +240,14 @@ fun CoachMarkScope<AddEditItemCoachMark>.VaultAddEditContent(
236240
is VaultAddEditState.ViewState.Content.ItemType.Card -> {
237241
vaultAddEditCardItems(
238242
cardState = state.type,
243+
isCardScannerEnabled = isCardScannerEnabled,
244+
onScanCardClick = {
245+
if (permissionsManager.checkPermission(Manifest.permission.CAMERA)) {
246+
cardItemTypeHandlers.onScanCardClick(true)
247+
} else {
248+
launcher.launch(Manifest.permission.CAMERA)
249+
}
250+
},
239251
cardHandlers = cardItemTypeHandlers,
240252
)
241253
}

app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditNavigation.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ fun NavGraphBuilder.vaultAddEditDestination(
7373
onNavigateBack: () -> Unit,
7474
onNavigateToManualCodeEntryScreen: () -> Unit,
7575
onNavigateToQrCodeScanScreen: () -> Unit,
76+
onNavigateToCardScanScreen: () -> Unit,
7677
onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit,
7778
onNavigateToAttachments: (cipherId: String) -> Unit,
7879
onNavigateToMoveToOrganization: (cipherId: String, showOnlyCollections: Boolean) -> Unit,
@@ -82,6 +83,7 @@ fun NavGraphBuilder.vaultAddEditDestination(
8283
onNavigateBack = onNavigateBack,
8384
onNavigateToManualCodeEntryScreen = onNavigateToManualCodeEntryScreen,
8485
onNavigateToQrCodeScanScreen = onNavigateToQrCodeScanScreen,
86+
onNavigateToCardScanScreen = onNavigateToCardScanScreen,
8587
onNavigateToGeneratorModal = onNavigateToGeneratorModal,
8688
onNavigateToAttachments = onNavigateToAttachments,
8789
onNavigateToMoveToOrganization = onNavigateToMoveToOrganization,

app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ import kotlinx.coroutines.launch
9999
fun VaultAddEditScreen(
100100
onNavigateBack: () -> Unit,
101101
onNavigateToQrCodeScanScreen: () -> Unit,
102+
onNavigateToCardScanScreen: () -> Unit,
102103
viewModel: VaultAddEditViewModel = hiltViewModel(),
103104
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
104105
intentManager: IntentManager = LocalIntentManager.current,
@@ -129,6 +130,10 @@ fun VaultAddEditScreen(
129130
onNavigateToQrCodeScanScreen()
130131
}
131132

133+
is VaultAddEditEvent.NavigateToCardScan -> {
134+
onNavigateToCardScanScreen()
135+
}
136+
132137
is VaultAddEditEvent.NavigateToManualCodeEntry -> {
133138
onNavigateToManualCodeEntryScreen()
134139
}
@@ -393,6 +398,7 @@ fun VaultAddEditScreen(
393398
identityItemTypeHandlers = identityItemTypeHandlers,
394399
cardItemTypeHandlers = cardItemTypeHandlers,
395400
sshKeyItemTypeHandlers = sshKeyItemTypeHandlers,
401+
isCardScannerEnabled = state.isCardScannerEnabled,
396402
lazyListState = lazyListState,
397403
onPreviousCoachMark = {
398404
coroutineScope.launch {

app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.bitwarden.vault.CipherView
2525
import com.bitwarden.vault.DecryptCipherListResult
2626
import com.bitwarden.vault.FolderView
2727
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
28+
import com.bitwarden.ui.platform.feature.cardscanner.manager.CardScanManager
2829
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
2930
import com.x8bit.bitwarden.data.auth.repository.model.UserState
3031
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
@@ -55,13 +56,15 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
5556
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
5657
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
5758
import com.x8bit.bitwarden.data.vault.repository.model.ArchiveCipherResult
59+
import com.bitwarden.ui.platform.feature.cardscanner.util.CardScanResult
5860
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
5961
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
6062
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
6163
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
6264
import com.x8bit.bitwarden.data.vault.repository.model.UnarchiveCipherResult
6365
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
6466
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
67+
import com.x8bit.bitwarden.ui.vault.util.detectCardBrand
6568
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
6669
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
6770
import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay
@@ -125,6 +128,7 @@ class VaultAddEditViewModel @Inject constructor(
125128
private val toastManager: ToastManager,
126129
private val authRepository: AuthRepository,
127130
private val clipboardManager: BitwardenClipboardManager,
131+
private val cardScanManager: CardScanManager,
128132
private val policyManager: PolicyManager,
129133
private val vaultRepository: VaultRepository,
130134
private val bitwardenCredentialManager: BitwardenCredentialManager,
@@ -178,6 +182,7 @@ class VaultAddEditViewModel @Inject constructor(
178182

179183
VaultAddEditState(
180184
isArchiveEnabled = featureFlagManager.getFeatureFlag(FlagKey.ArchiveItems),
185+
isCardScannerEnabled = featureFlagManager.getFeatureFlag(FlagKey.CardScanner),
181186
vaultAddEditType = vaultAddEditType,
182187
cipherType = vaultCipherType,
183188
viewState = when (vaultAddEditType) {
@@ -279,6 +284,18 @@ class VaultAddEditViewModel @Inject constructor(
279284
.onEach(::sendAction)
280285
.launchIn(viewModelScope)
281286

287+
featureFlagManager
288+
.getFeatureFlagFlow(FlagKey.CardScanner)
289+
.map { VaultAddEditAction.Internal.CardScannerFlagUpdateReceive(it) }
290+
.onEach(::sendAction)
291+
.launchIn(viewModelScope)
292+
293+
cardScanManager
294+
.cardScanResultFlow
295+
.map { VaultAddEditAction.Internal.CardScanResultReceive(it) }
296+
.onEach(::sendAction)
297+
.launchIn(viewModelScope)
298+
282299
snackbarRelayManager
283300
.getSnackbarDataFlow(SnackbarRelay.CIPHER_MOVED_TO_ORGANIZATION)
284301
.map { VaultAddEditAction.Internal.SnackbarDataReceived(it) }
@@ -1542,6 +1559,24 @@ class VaultAddEditViewModel @Inject constructor(
15421559
is VaultAddEditAction.ItemType.CardType.SecurityCodeVisibilityChange -> {
15431560
handleSecurityCodeVisibilityChange(action)
15441561
}
1562+
1563+
is VaultAddEditAction.ItemType.CardType.ScanCardClick -> {
1564+
handleScanCardClick(action)
1565+
}
1566+
}
1567+
}
1568+
1569+
private fun handleScanCardClick(
1570+
action: VaultAddEditAction.ItemType.CardType.ScanCardClick,
1571+
) {
1572+
if (!state.isCardScannerEnabled) return
1573+
if (action.isGranted) {
1574+
sendEvent(VaultAddEditEvent.NavigateToCardScan)
1575+
} else {
1576+
toastManager.show(
1577+
messageId = BitwardenString
1578+
.enable_camera_permission_to_use_the_scanner,
1579+
)
15451580
}
15461581
}
15471582

@@ -1653,6 +1688,14 @@ class VaultAddEditViewModel @Inject constructor(
16531688
handleArchiveItemsFlagUpdateReceive(action)
16541689
}
16551690

1691+
is VaultAddEditAction.Internal.CardScannerFlagUpdateReceive -> {
1692+
handleCardScannerFlagUpdateReceive(action)
1693+
}
1694+
1695+
is VaultAddEditAction.Internal.CardScanResultReceive -> {
1696+
handleCardScanResultReceive(action)
1697+
}
1698+
16561699
is VaultAddEditAction.Internal.DeleteCipherReceive -> handleDeleteCipherReceive(action)
16571700
is VaultAddEditAction.Internal.TotpCodeReceive -> handleVaultTotpCodeReceive(action)
16581701
is VaultAddEditAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
@@ -1870,6 +1913,41 @@ class VaultAddEditViewModel @Inject constructor(
18701913
mutableStateFlow.update { it.copy(isArchiveEnabled = action.isEnabled) }
18711914
}
18721915

1916+
private fun handleCardScannerFlagUpdateReceive(
1917+
action: VaultAddEditAction.Internal.CardScannerFlagUpdateReceive,
1918+
) {
1919+
mutableStateFlow.update { it.copy(isCardScannerEnabled = action.isEnabled) }
1920+
}
1921+
1922+
private fun handleCardScanResultReceive(
1923+
action: VaultAddEditAction.Internal.CardScanResultReceive,
1924+
) {
1925+
when (val result = action.cardScanResult) {
1926+
is CardScanResult.Success -> {
1927+
val data = result.cardScanData
1928+
updateCardContent { cardType ->
1929+
cardType.copy(
1930+
number = data.number ?: cardType.number,
1931+
cardHolderName = data.cardholderName
1932+
?: cardType.cardHolderName,
1933+
expirationYear = data.expirationYear
1934+
?: cardType.expirationYear,
1935+
expirationMonth = data.expirationMonth
1936+
?.toExpirationMonth()
1937+
?: cardType.expirationMonth,
1938+
securityCode = data.securityCode
1939+
?: cardType.securityCode,
1940+
brand = data.number
1941+
?.detectCardBrand()
1942+
?: cardType.brand,
1943+
)
1944+
}
1945+
}
1946+
1947+
is CardScanResult.ScanError -> Unit
1948+
}
1949+
}
1950+
18731951
private fun handleDeleteCipherReceive(action: VaultAddEditAction.Internal.DeleteCipherReceive) {
18741952
when (val result = action.result) {
18751953
is DeleteCipherResult.Error -> {
@@ -2404,6 +2482,24 @@ class VaultAddEditViewModel @Inject constructor(
24042482
//endregion Utility Functions
24052483
}
24062484

2485+
@Suppress("MagicNumber")
2486+
private fun String.toExpirationMonth(): VaultCardExpirationMonth =
2487+
when (this.toIntOrNull()) {
2488+
1 -> VaultCardExpirationMonth.JANUARY
2489+
2 -> VaultCardExpirationMonth.FEBRUARY
2490+
3 -> VaultCardExpirationMonth.MARCH
2491+
4 -> VaultCardExpirationMonth.APRIL
2492+
5 -> VaultCardExpirationMonth.MAY
2493+
6 -> VaultCardExpirationMonth.JUNE
2494+
7 -> VaultCardExpirationMonth.JULY
2495+
8 -> VaultCardExpirationMonth.AUGUST
2496+
9 -> VaultCardExpirationMonth.SEPTEMBER
2497+
10 -> VaultCardExpirationMonth.OCTOBER
2498+
11 -> VaultCardExpirationMonth.NOVEMBER
2499+
12 -> VaultCardExpirationMonth.DECEMBER
2500+
else -> VaultCardExpirationMonth.SELECT
2501+
}
2502+
24072503
/**
24082504
* Represents the state for adding an item to the vault.
24092505
*
@@ -2428,6 +2524,7 @@ data class VaultAddEditState(
24282524
val defaultUriMatchType: UriMatchType,
24292525
private val shouldShowCoachMarkTour: Boolean,
24302526
private val isArchiveEnabled: Boolean,
2527+
val isCardScannerEnabled: Boolean,
24312528
) : Parcelable {
24322529

24332530
/**
@@ -3098,6 +3195,11 @@ sealed class VaultAddEditEvent {
30983195
*/
30993196
data object NavigateToQrCodeScan : VaultAddEditEvent()
31003197

3198+
/**
3199+
* Navigate to the card scan screen.
3200+
*/
3201+
data object NavigateToCardScan : VaultAddEditEvent()
3202+
31013203
/**
31023204
* Navigate to the manual code entry screen.
31033205
*/
@@ -3698,6 +3800,13 @@ sealed class VaultAddEditAction {
36983800
* @property isVisible The new code visibility state.
36993801
*/
37003802
data class SecurityCodeVisibilityChange(val isVisible: Boolean) : CardType()
3803+
3804+
/**
3805+
* Fired when the scan card button is clicked.
3806+
*
3807+
* @property isGranted Whether camera permission was granted.
3808+
*/
3809+
data class ScanCardClick(val isGranted: Boolean) : CardType()
37013810
}
37023811

37033812
/**
@@ -3841,5 +3950,19 @@ sealed class VaultAddEditAction {
38413950
data class ArchiveItemsFlagUpdateReceive(
38423951
val isEnabled: Boolean,
38433952
) : Internal()
3953+
3954+
/**
3955+
* Indicates that the Card Scanner flag has been updated.
3956+
*/
3957+
data class CardScannerFlagUpdateReceive(
3958+
val isEnabled: Boolean,
3959+
) : Internal()
3960+
3961+
/**
3962+
* Indicates that a card scan result has been received.
3963+
*/
3964+
data class CardScanResultReceive(
3965+
val cardScanResult: CardScanResult,
3966+
) : Internal()
38443967
}
38453968
}

0 commit comments

Comments
 (0)