mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 10:48:47 +03:00
BIT-1114 Add code manual entry (#523)
This commit is contained in:
parent
0ee25a3dd5
commit
e863559c12
16 changed files with 747 additions and 37 deletions
|
@ -18,6 +18,8 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.navigateToVaultAddEdit
|
|||
import com.x8bit.bitwarden.ui.vault.feature.addedit.vaultAddEditDestination
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.navigateToVaultItem
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.vaultItemDestination
|
||||
import com.x8bit.bitwarden.ui.vault.feature.manualcodeentry.navigateToManualCodeEntryScreen
|
||||
import com.x8bit.bitwarden.ui.vault.feature.manualcodeentry.vaultManualCodeEntryDestination
|
||||
import com.x8bit.bitwarden.ui.vault.feature.qrcodescan.navigateToQrCodeScanScreen
|
||||
import com.x8bit.bitwarden.ui.vault.feature.qrcodescan.vaultQrCodeScanDestination
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
|
||||
|
@ -59,6 +61,9 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
|||
onNavigateToQrCodeScanScreen = {
|
||||
navController.navigateToQrCodeScanScreen()
|
||||
},
|
||||
onNavigateToManualCodeEntryScreen = {
|
||||
navController.navigateToManualCodeEntryScreen()
|
||||
},
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
vaultItemDestination(
|
||||
|
@ -67,7 +72,22 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
|||
navController.navigateToVaultAddEdit(VaultAddEditType.EditItem(it))
|
||||
},
|
||||
)
|
||||
vaultQrCodeScanDestination(onNavigateBack = { navController.popBackStack() })
|
||||
vaultQrCodeScanDestination(
|
||||
onNavigateToManualCodeEntryScreen = {
|
||||
navController.popBackStack()
|
||||
navController.navigateToManualCodeEntryScreen()
|
||||
},
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
|
||||
vaultManualCodeEntryDestination(
|
||||
onNavigateToQrCodeScreen = {
|
||||
navController.popBackStack()
|
||||
navController.navigateToQrCodeScanScreen()
|
||||
},
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
|
||||
addSendDestination(onNavigateBack = { navController.popBackStack() })
|
||||
passwordHistoryDestination(onNavigateBack = { navController.popBackStack() })
|
||||
foldersDestination(onNavigateBack = { navController.popBackStack() })
|
||||
|
|
|
@ -40,8 +40,9 @@ data class VaultAddEditArgs(
|
|||
* Add the vault add & edit screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.vaultAddEditDestination(
|
||||
onNavigateToQrCodeScanScreen: () -> Unit,
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToManualCodeEntryScreen: () -> Unit,
|
||||
onNavigateToQrCodeScanScreen: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions(
|
||||
route = ADD_EDIT_ITEM_ROUTE,
|
||||
|
@ -49,7 +50,11 @@ fun NavGraphBuilder.vaultAddEditDestination(
|
|||
navArgument(ADD_EDIT_ITEM_TYPE) { type = NavType.StringType },
|
||||
),
|
||||
) {
|
||||
VaultAddEditScreen(onNavigateBack, onNavigateToQrCodeScanScreen)
|
||||
VaultAddEditScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToManualCodeEntryScreen = onNavigateToManualCodeEntryScreen,
|
||||
onNavigateToQrCodeScanScreen = onNavigateToQrCodeScanScreen,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ fun VaultAddEditScreen(
|
|||
viewModel: VaultAddEditViewModel = hiltViewModel(),
|
||||
permissionsManager: PermissionsManager =
|
||||
PermissionsManagerImpl(LocalContext.current as Activity),
|
||||
onNavigateToManualCodeEntryScreen: () -> Unit,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
|
@ -60,6 +61,10 @@ fun VaultAddEditScreen(
|
|||
onNavigateToQrCodeScanScreen()
|
||||
}
|
||||
|
||||
is VaultAddEditEvent.NavigateToManualCodeEntry -> {
|
||||
onNavigateToManualCodeEntryScreen()
|
||||
}
|
||||
|
||||
is VaultAddEditEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.message(resources), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
|
|
@ -424,13 +424,7 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
if (action.isGranted) {
|
||||
sendEvent(event = VaultAddEditEvent.NavigateToQrCodeScan)
|
||||
} else {
|
||||
// TODO Add manual QR code entry (BIT-1114)
|
||||
sendEvent(
|
||||
event = VaultAddEditEvent.ShowToast(
|
||||
message =
|
||||
"Permission Not Granted, Manual QR Code Entry Not Implemented".asText(),
|
||||
),
|
||||
)
|
||||
sendEvent(event = VaultAddEditEvent.NavigateToManualCodeEntry)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1233,6 +1227,11 @@ sealed class VaultAddEditEvent {
|
|||
* Navigate to the QR code scan screen.
|
||||
*/
|
||||
data object NavigateToQrCodeScan : VaultAddEditEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the manual code entry screen.
|
||||
*/
|
||||
data object NavigateToManualCodeEntry : VaultAddEditEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.manualcodeentry
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
|
||||
private const val MANUAL_CODE_ENTRY_ROUTE: String = "manual_code_entry"
|
||||
|
||||
/**
|
||||
* Add the manual code entry screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.vaultManualCodeEntryDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToQrCodeScreen: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions(
|
||||
route = MANUAL_CODE_ENTRY_ROUTE,
|
||||
) {
|
||||
ManualCodeEntryScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToQrCodeScreen = onNavigateToQrCodeScreen,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the manual code entry screen.
|
||||
*/
|
||||
fun NavController.navigateToManualCodeEntryScreen(
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
this.navigate(MANUAL_CODE_ENTRY_ROUTE, navOptions)
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.manualcodeentry
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.PermissionsManager
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.PermissionsManagerImpl
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledTonalButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
|
||||
|
||||
/**
|
||||
* The screen to manually add a totp code.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ManualCodeEntryScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToQrCodeScreen: () -> Unit,
|
||||
viewModel: ManualCodeEntryViewModel = hiltViewModel(),
|
||||
intentHandler: IntentHandler = IntentHandler(LocalContext.current),
|
||||
permissionsManager: PermissionsManager =
|
||||
PermissionsManagerImpl(LocalContext.current as Activity),
|
||||
) {
|
||||
var shouldShowPermissionDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
val launcher = permissionsManager.getLauncher { isGranted ->
|
||||
if (isGranted) {
|
||||
viewModel.trySendAction(ManualCodeEntryAction.ScanQrCodeTextClick)
|
||||
} else {
|
||||
shouldShowPermissionDialog = true
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
is ManualCodeEntryEvent.NavigateToAppSettings -> {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intent.data = Uri.parse("package:" + context.packageName)
|
||||
|
||||
intentHandler.startActivity(intent = intent)
|
||||
}
|
||||
|
||||
is ManualCodeEntryEvent.ShowToast -> {
|
||||
Toast
|
||||
.makeText(context, event.message.invoke(context.resources), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
|
||||
is ManualCodeEntryEvent.NavigateToQrCodeScreen -> {
|
||||
onNavigateToQrCodeScreen.invoke()
|
||||
}
|
||||
|
||||
is ManualCodeEntryEvent.NavigateBack -> {
|
||||
onNavigateBack.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldShowPermissionDialog) {
|
||||
BitwardenTwoButtonDialog(
|
||||
message = stringResource(id = R.string.enable_camer_permission_to_use_the_scanner),
|
||||
confirmButtonText = stringResource(id = R.string.settings),
|
||||
dismissButtonText = stringResource(id = R.string.no_thanks),
|
||||
onConfirmClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ManualCodeEntryAction.SettingsClick) }
|
||||
},
|
||||
onDismissClick = { shouldShowPermissionDialog = false },
|
||||
onDismissRequest = { shouldShowPermissionDialog = false },
|
||||
title = null,
|
||||
)
|
||||
}
|
||||
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.authenticator_key_scanner),
|
||||
navigationIcon = painterResource(id = R.drawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ManualCodeEntryAction.CloseClick) }
|
||||
},
|
||||
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
Column(modifier = Modifier.padding(paddingValues)) {
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.enter_key_manually),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextField(
|
||||
singleLine = false,
|
||||
label = stringResource(id = R.string.authenticator_key_scanner),
|
||||
value = state.code,
|
||||
onValueChange = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
ManualCodeEntryAction.CodeTextChange(it),
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenFilledTonalButton(
|
||||
label = stringResource(id = R.string.add_totp),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ManualCodeEntryAction.CodeSubmit) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.once_the_key_is_successfully_entered),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
vertical = 16.dp,
|
||||
horizontal = 16.dp,
|
||||
),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.cannot_add_authenticator_key),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
vertical = 8.dp,
|
||||
horizontal = 16.dp,
|
||||
),
|
||||
)
|
||||
|
||||
ClickableText(
|
||||
text = stringResource(id = R.string.scan_qr_code).toAnnotatedString(),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp),
|
||||
onClick = remember(viewModel) {
|
||||
{
|
||||
if (permissionsManager.checkPermission(Manifest.permission.CAMERA)) {
|
||||
viewModel.trySendAction(ManualCodeEntryAction.ScanQrCodeTextClick)
|
||||
} else {
|
||||
launcher.launch(Manifest.permission.CAMERA)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.manualcodeentry
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
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"
|
||||
|
||||
/**
|
||||
* The ViewModel for handling user interactions in the manual code entry screen.
|
||||
*
|
||||
*/
|
||||
@HiltViewModel
|
||||
class ManualCodeEntryViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val vaultRepository: VaultRepository,
|
||||
) : BaseViewModel<ManualCodeEntryState, ManualCodeEntryEvent, ManualCodeEntryAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: ManualCodeEntryState(code = ""),
|
||||
) {
|
||||
override fun handleAction(action: ManualCodeEntryAction) {
|
||||
when (action) {
|
||||
is ManualCodeEntryAction.CloseClick -> handleCloseClick()
|
||||
is ManualCodeEntryAction.CodeTextChange -> handleCodeTextChange(action)
|
||||
is ManualCodeEntryAction.CodeSubmit -> handleCodeSubmit()
|
||||
is ManualCodeEntryAction.ScanQrCodeTextClick -> handleScanQrCodeTextClick()
|
||||
is ManualCodeEntryAction.SettingsClick -> handleSettingsClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCloseClick() {
|
||||
sendEvent(ManualCodeEntryEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleCodeTextChange(action: ManualCodeEntryAction.CodeTextChange) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(code = action.code)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCodeSubmit() {
|
||||
vaultRepository.emitTotpCode(state.code)
|
||||
sendEvent(ManualCodeEntryEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleScanQrCodeTextClick() {
|
||||
sendEvent(ManualCodeEntryEvent.NavigateToQrCodeScreen)
|
||||
}
|
||||
|
||||
private fun handleSettingsClick() {
|
||||
sendEvent(ManualCodeEntryEvent.NavigateToAppSettings)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models state of the manual entry screen.
|
||||
*/
|
||||
@Parcelize
|
||||
data class ManualCodeEntryState(
|
||||
val code: String,
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
* Models events for the [ManualCodeEntryScreen].
|
||||
*/
|
||||
sealed class ManualCodeEntryEvent {
|
||||
|
||||
/**
|
||||
* Navigate back.
|
||||
*/
|
||||
data object NavigateBack : ManualCodeEntryEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the Qr code screen.
|
||||
*/
|
||||
data object NavigateToQrCodeScreen : ManualCodeEntryEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the app settings.
|
||||
*/
|
||||
data object NavigateToAppSettings : ManualCodeEntryEvent()
|
||||
|
||||
/**
|
||||
* Show a toast with the given [message].
|
||||
*/
|
||||
data class ShowToast(val message: Text) : ManualCodeEntryEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Models actions for the [ManualCodeEntryScreen].
|
||||
*/
|
||||
sealed class ManualCodeEntryAction {
|
||||
|
||||
/**
|
||||
* User clicked close.
|
||||
*/
|
||||
data object CloseClick : ManualCodeEntryAction()
|
||||
|
||||
/**
|
||||
* The user has submitted a code.
|
||||
*/
|
||||
data object CodeSubmit : ManualCodeEntryAction()
|
||||
|
||||
/**
|
||||
* The user has changed the code text.
|
||||
*/
|
||||
data class CodeTextChange(val code: String) : ManualCodeEntryAction()
|
||||
|
||||
/**
|
||||
* The text to switch to QR code scanning is clicked.
|
||||
*/
|
||||
data object ScanQrCodeTextClick : ManualCodeEntryAction()
|
||||
|
||||
/**
|
||||
* The action for the user clicking the settings button.
|
||||
*/
|
||||
data object SettingsClick : ManualCodeEntryAction()
|
||||
}
|
|
@ -12,11 +12,15 @@ private const val QR_CODE_SCAN_ROUTE: String = "qr_code_scan"
|
|||
*/
|
||||
fun NavGraphBuilder.vaultQrCodeScanDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToManualCodeEntryScreen: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions(
|
||||
route = QR_CODE_SCAN_ROUTE,
|
||||
) {
|
||||
QrCodeScanScreen(onNavigateBack)
|
||||
QrCodeScanScreen(
|
||||
onNavigateToManualCodeEntryScreen = onNavigateToManualCodeEntryScreen,
|
||||
onNavigateBack = onNavigateBack,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@ fun QrCodeScanScreen(
|
|||
onNavigateBack: () -> Unit,
|
||||
viewModel: QrCodeScanViewModel = hiltViewModel(),
|
||||
qrCodeAnalyzer: QrCodeAnalyzer = QrCodeAnalyzerImpl(),
|
||||
onNavigateToManualCodeEntryScreen: () -> Unit,
|
||||
) {
|
||||
qrCodeAnalyzer.onQrCodeScanned = remember(viewModel) {
|
||||
{ viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(it)) }
|
||||
|
@ -100,6 +101,10 @@ fun QrCodeScanScreen(
|
|||
is QrCodeScanEvent.NavigateBack -> {
|
||||
onNavigateBack.invoke()
|
||||
}
|
||||
|
||||
is QrCodeScanEvent.NavigateToManualCodeEntry -> {
|
||||
onNavigateToManualCodeEntryScreen.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ package com.x8bit.bitwarden.ui.vault.feature.qrcodescan
|
|||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -21,8 +20,8 @@ class QrCodeScanViewModel @Inject constructor(
|
|||
when (action) {
|
||||
is QrCodeScanAction.CloseClick -> handleCloseClick()
|
||||
is QrCodeScanAction.ManualEntryTextClick -> handleManualEntryTextClick()
|
||||
is QrCodeScanAction.CameraSetupErrorReceive -> handleCameraErrorReceive()
|
||||
is QrCodeScanAction.QrCodeScanReceive -> handleQrCodeScanReceive(action)
|
||||
is QrCodeScanAction.CameraSetupErrorReceive -> handleCameraErrorReceive(action)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,11 +32,8 @@ class QrCodeScanViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleManualEntryTextClick() {
|
||||
// TODO: Implement Manual Entry Screen (BIT-1114)
|
||||
sendEvent(
|
||||
QrCodeScanEvent.ShowToast(
|
||||
message = "Not yet implemented.".asText(),
|
||||
),
|
||||
QrCodeScanEvent.NavigateToManualCodeEntry,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -46,14 +42,9 @@ class QrCodeScanViewModel @Inject constructor(
|
|||
sendEvent(QrCodeScanEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleCameraErrorReceive(
|
||||
action: QrCodeScanAction.CameraSetupErrorReceive,
|
||||
) {
|
||||
// TODO: Implement Manual Entry Screen (BIT-1114)
|
||||
private fun handleCameraErrorReceive() {
|
||||
sendEvent(
|
||||
QrCodeScanEvent.ShowToast(
|
||||
message = "Not yet implemented.".asText(),
|
||||
),
|
||||
QrCodeScanEvent.NavigateToManualCodeEntry,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -68,6 +59,11 @@ sealed class QrCodeScanEvent {
|
|||
*/
|
||||
data object NavigateBack : QrCodeScanEvent()
|
||||
|
||||
/**
|
||||
* Navigate to manual code entry screen.
|
||||
*/
|
||||
data object NavigateToManualCodeEntry : QrCodeScanEvent()
|
||||
|
||||
/**
|
||||
* Show a toast with the given [message].
|
||||
*/
|
||||
|
|
|
@ -54,6 +54,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
|
||||
private var onNavigateBackCalled = false
|
||||
private var onNavigateQrCodeScanScreenCalled = false
|
||||
private var onNavigateToManualCodeEntryScreenCalled = false
|
||||
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<VaultAddEditEvent>()
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE_LOGIN)
|
||||
|
@ -69,12 +70,15 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
fun setup() {
|
||||
composeTestRule.setContent {
|
||||
VaultAddEditScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
permissionsManager = fakePermissionManager,
|
||||
onNavigateToQrCodeScanScreen = {
|
||||
onNavigateQrCodeScanScreenCalled = true
|
||||
},
|
||||
onNavigateToManualCodeEntryScreen = {
|
||||
onNavigateToManualCodeEntryScreenCalled = true
|
||||
},
|
||||
viewModel = viewModel,
|
||||
permissionsManager = fakePermissionManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -92,6 +96,13 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
assertTrue(onNavigateQrCodeScanScreenCalled)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on NavigateToManualCodeEntry event should invoke NavigateToManualCodeEntry`() {
|
||||
mutableEventFlow.tryEmit(VaultAddEditEvent.NavigateToManualCodeEntry)
|
||||
assertTrue(onNavigateToManualCodeEntryScreenCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking close button should send CloseClick action`() {
|
||||
composeTestRule
|
||||
|
@ -1429,6 +1440,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
.onNodeWithTextAfterScroll(text = "Security code")
|
||||
.assertTextContains("123")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking New Custom Field button should allow creation of Linked type`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_LOGIN
|
||||
|
|
|
@ -558,7 +558,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `SetupTotpClick should emit ShowToast with permission not granted when isGranted is false`() =
|
||||
fun `SetupTotpClick should emit NavigateToManualCodeEntry when isGranted is false`() =
|
||||
runTest {
|
||||
val viewModel = createAddVaultItemViewModel()
|
||||
|
||||
|
@ -568,10 +568,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
isGranted = false,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
VaultAddEditEvent.ShowToast("Permission Not Granted, Manual QR Code Entry Not Implemented".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
assertEquals(VaultAddEditEvent.NavigateToManualCodeEntry, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.manualcodeentry
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||
import androidx.compose.ui.test.assertTextEquals
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onFirst
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.FakePermissionManager
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class ManualCodeEntryScreenTests : BaseComposeTest() {
|
||||
|
||||
private var onNavigateBackCalled = false
|
||||
private var onNavigateToScanQrCodeCalled = false
|
||||
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<ManualCodeEntryEvent>()
|
||||
private val mutableStateFlow =
|
||||
MutableStateFlow(ManualCodeEntryState(""))
|
||||
|
||||
private val fakePermissionManager: FakePermissionManager = FakePermissionManager()
|
||||
private val intentHandler = mockk<IntentHandler>(relaxed = true)
|
||||
|
||||
private val viewModel = mockk<ManualCodeEntryViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
composeTestRule.setContent {
|
||||
ManualCodeEntryScreen(
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
viewModel = viewModel,
|
||||
onNavigateToQrCodeScreen = {
|
||||
onNavigateToScanQrCodeCalled = true
|
||||
},
|
||||
permissionsManager = fakePermissionManager,
|
||||
intentHandler = intentHandler,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on NavigateBack event should invoke onNavigateBack`() {
|
||||
mutableEventFlow.tryEmit(ManualCodeEntryEvent.NavigateBack)
|
||||
assertTrue(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on NavigateToScanQrCode event should invoke NavigateToScanQrCode`() {
|
||||
mutableEventFlow.tryEmit(ManualCodeEntryEvent.NavigateToQrCodeScreen)
|
||||
assertTrue(onNavigateToScanQrCodeCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on NavigateToAppSettings event should invoke intent handler`() {
|
||||
mutableEventFlow.tryEmit(ManualCodeEntryEvent.NavigateToAppSettings)
|
||||
|
||||
val uri = Uri.parse(
|
||||
"package:" +
|
||||
ApplicationProvider
|
||||
.getApplicationContext<Application>()
|
||||
.packageName,
|
||||
)
|
||||
|
||||
val intentSlot = slot<Intent>()
|
||||
verify { intentHandler.startActivity(capture(intentSlot)) }
|
||||
|
||||
assertEquals(
|
||||
uri,
|
||||
intentSlot.captured.data,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `clicking on manual text should send ScanQrCodeTextClick if camera permission is granted`() {
|
||||
fakePermissionManager.checkPermissionResult = true
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Scan QR Code")
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(ManualCodeEntryAction.ScanQrCodeTextClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dialog should be dismissed on dismiss click in settings dialog`() {
|
||||
fakePermissionManager.checkPermissionResult = false
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Scan QR Code")
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Enable camera permission to use the scanner")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("No thanks")
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Enable camera permission to use the scanner")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsNotDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `settings dialog should call SettingsClick action on confirm click`() {
|
||||
fakePermissionManager.checkPermissionResult = false
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Scan QR Code")
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Enable camera permission to use the scanner")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Settings")
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(ManualCodeEntryAction.SettingsClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CodeTextChanged will be sent when text for code is updated`() {
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Authenticator key")
|
||||
.onFirst()
|
||||
.assertTextEquals("Authenticator key", "")
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Authenticator key")
|
||||
.onFirst()
|
||||
.performTextInput(text = "TestCode")
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
ManualCodeEntryAction.CodeTextChange("TestCode"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Authenticator key text should display the text provided by the state`() {
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Authenticator key")
|
||||
.onFirst()
|
||||
.assertTextEquals("Authenticator key", "")
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(code = "TestCode")
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Authenticator key")
|
||||
.onFirst()
|
||||
.assertTextEquals("Authenticator key", "TestCode")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking Add TOTP button should send CodeSubmit action`() {
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Add TOTP")
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
ManualCodeEntryAction.CodeSubmit,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.manualcodeentry
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ManualCodeEntryViewModelTests : BaseViewModelTest() {
|
||||
|
||||
private val totpTestCodeFlow: Flow<String> = bufferedMutableSharedFlow()
|
||||
private val vaultRepository: VaultRepository = mockk {
|
||||
every { totpCodeFlow } returns totpTestCodeFlow
|
||||
every { emitTotpCode(any()) } just runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CloseClick should emit NavigateBack`() = runTest {
|
||||
val viewModel = createViewModel(initialState = ManualCodeEntryState(""))
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(ManualCodeEntryAction.CloseClick)
|
||||
assertEquals(ManualCodeEntryEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CodeSubmit should emit new code and NavigateBack`() = runTest {
|
||||
val viewModel =
|
||||
createViewModel(initialState = ManualCodeEntryState("TestCode"))
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(ManualCodeEntryAction.CodeSubmit)
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCode("TestCode") }
|
||||
assertEquals(ManualCodeEntryEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CodeTextChange should update state with new value`() = runTest {
|
||||
val viewModel =
|
||||
createViewModel(initialState = ManualCodeEntryState("TestCode"))
|
||||
|
||||
val expectedState = ManualCodeEntryState("NewCode")
|
||||
|
||||
viewModel.actionChannel.trySend(ManualCodeEntryAction.CodeTextChange("NewCode"))
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SettingsClick should emit NavigateToAppSettings and update state`() = runTest {
|
||||
val viewModel = createViewModel(initialState = ManualCodeEntryState(""))
|
||||
|
||||
val expectedState = ManualCodeEntryState("")
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(ManualCodeEntryAction.SettingsClick)
|
||||
|
||||
assertEquals(ManualCodeEntryEvent.NavigateToAppSettings, awaitItem())
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ScanQrTextCodeClick should emit NavigateToQrCodeScreen`() = runTest {
|
||||
val viewModel = createViewModel(initialState = ManualCodeEntryState(""))
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(ManualCodeEntryAction.ScanQrCodeTextClick)
|
||||
|
||||
assertEquals(ManualCodeEntryEvent.NavigateToQrCodeScreen, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(initialState: ManualCodeEntryState): ManualCodeEntryViewModel =
|
||||
ManualCodeEntryViewModel(
|
||||
vaultRepository = vaultRepository,
|
||||
savedStateHandle = SavedStateHandle(
|
||||
initialState = mapOf("state" to initialState),
|
||||
),
|
||||
)
|
||||
}
|
|
@ -17,6 +17,7 @@ import org.robolectric.annotation.Config
|
|||
class QrCodeScanScreenTest : BaseComposeTest() {
|
||||
|
||||
private var onNavigateBackCalled = false
|
||||
private var onNavigateToManualCodeEntryScreenCalled = false
|
||||
|
||||
private val imageProxy: ImageProxy = mockk()
|
||||
private val qrCodeAnalyzer = FakeQrCodeAnalyzer()
|
||||
|
@ -31,9 +32,12 @@ class QrCodeScanScreenTest : BaseComposeTest() {
|
|||
fun setup() {
|
||||
composeTestRule.setContent {
|
||||
QrCodeScanScreen(
|
||||
qrCodeAnalyzer = qrCodeAnalyzer,
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
viewModel = viewModel,
|
||||
qrCodeAnalyzer = qrCodeAnalyzer,
|
||||
onNavigateToManualCodeEntryScreen = {
|
||||
onNavigateToManualCodeEntryScreenCalled = true
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +48,12 @@ class QrCodeScanScreenTest : BaseComposeTest() {
|
|||
assertTrue(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on NavigateToManualCodeEntry event should invoke onNavigateToManualCodeEntryScreen`() {
|
||||
mutableEventFlow.tryEmit(QrCodeScanEvent.NavigateToManualCodeEntry)
|
||||
assertTrue(onNavigateToManualCodeEntryScreenCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when unable to setup camera CameraErrorReceive will be sent`() = runTest {
|
||||
// Because the camera is not set up in the tests, this will always be triggered
|
||||
|
|
|
@ -4,7 +4,6 @@ import app.cash.turbine.test
|
|||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
|
@ -34,13 +33,13 @@ class QrCodeScanViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `CameraErrorReceive should emit ShowToast`() = runTest {
|
||||
fun `CameraErrorReceive should emit NavigateToManualCodeEntry`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(QrCodeScanAction.CameraSetupErrorReceive)
|
||||
assertEquals(
|
||||
QrCodeScanEvent.ShowToast("Not yet implemented.".asText()),
|
||||
QrCodeScanEvent.NavigateToManualCodeEntry,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
|
@ -53,7 +52,7 @@ class QrCodeScanViewModelTest : BaseViewModelTest() {
|
|||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(QrCodeScanAction.ManualEntryTextClick)
|
||||
assertEquals(
|
||||
QrCodeScanEvent.ShowToast("Not yet implemented.".asText()),
|
||||
QrCodeScanEvent.NavigateToManualCodeEntry,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue