Skip to content

Commit

Permalink
BITAU-184 Allow user to save to Bitwarden when adding a code manually (
Browse files Browse the repository at this point in the history
  • Loading branch information
ahaisting-livefront authored Oct 30, 2024
1 parent bcc7e67 commit 5ac5e31
Show file tree
Hide file tree
Showing 6 changed files with 441 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.ui.platform.base.util.EventsEffect
import com.bitwarden.authenticator.ui.platform.base.util.toAnnotatedString
import com.bitwarden.authenticator.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.authenticator.ui.platform.components.button.BitwardenFilledTonalButton
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
Expand Down Expand Up @@ -201,17 +200,19 @@ fun ManualCodeEntryScreen(
)

Spacer(modifier = Modifier.height(16.dp))
BitwardenFilledTonalButton(
label = stringResource(id = R.string.add_code),
onClick = remember(viewModel) {
{ viewModel.trySendAction(ManualCodeEntryAction.CodeSubmit) }
SaveManualCodeButtons(
state = state.buttonState,
onSaveLocallyClick = remember(viewModel) {
{
viewModel.trySendAction(ManualCodeEntryAction.SaveLocallyClick)
}
},
onSaveToBitwardenClick = remember(viewModel) {
{
viewModel.trySendAction(ManualCodeEntryAction.SaveToBitwardenClick)
}
},
modifier = Modifier
.semantics { testTag = "AddCodeButton" }
.fillMaxWidth()
.padding(horizontal = 16.dp),
)

Text(
text = stringResource(id = R.string.once_the_key_is_successfully_entered),
style = MaterialTheme.typography.bodyMedium,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.Aut
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType
import com.bitwarden.authenticator.data.authenticator.manager.TotpCodeManager
import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
import com.bitwarden.authenticator.data.authenticator.repository.model.SharedVerificationCodesState
import com.bitwarden.authenticator.data.authenticator.repository.util.isSyncWithBitwardenEnabled
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticator.ui.platform.base.BaseViewModel
import com.bitwarden.authenticator.ui.platform.base.util.Text
import com.bitwarden.authenticator.ui.platform.base.util.asText
import com.bitwarden.authenticator.ui.platform.base.util.isBase32
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
Expand All @@ -26,24 +31,37 @@ private const val KEY_STATE = "state"
*
*/
@HiltViewModel
@Suppress("TooManyFunctions")
class ManualCodeEntryViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val authenticatorRepository: AuthenticatorRepository,
private val authenticatorBridgeManager: AuthenticatorBridgeManager,
settingsRepository: SettingsRepository,
) : BaseViewModel<ManualCodeEntryState, ManualCodeEntryEvent, ManualCodeEntryAction>(
initialState = savedStateHandle[KEY_STATE]
?: ManualCodeEntryState(code = "", issuer = "", dialog = null),
?: ManualCodeEntryState(
code = "",
issuer = "",
dialog = null,
buttonState = deriveButtonState(
sharedCodesState = authenticatorRepository.sharedCodesStateFlow.value,
defaultSaveOption = settingsRepository.defaultSaveOption,
),
),
) {
override fun handleAction(action: ManualCodeEntryAction) {
when (action) {
is ManualCodeEntryAction.CloseClick -> handleCloseClick()
is ManualCodeEntryAction.CodeTextChange -> handleCodeTextChange(action)
is ManualCodeEntryAction.IssuerTextChange -> handleIssuerTextChange(action)
is ManualCodeEntryAction.CodeSubmit -> handleCodeSubmit()
is ManualCodeEntryAction.ScanQrCodeTextClick -> handleScanQrCodeTextClick()
is ManualCodeEntryAction.SettingsClick -> handleSettingsClick()
ManualCodeEntryAction.DismissDialog -> {
handleDialogDismiss()
}

ManualCodeEntryAction.SaveLocallyClick -> handleSaveLocallyClick()
ManualCodeEntryAction.SaveToBitwardenClick -> handleSaveToBitwardenClick()
}
}

Expand All @@ -67,7 +85,11 @@ class ManualCodeEntryViewModel @Inject constructor(
}
}

private fun handleCodeSubmit() {
private fun handleSaveLocallyClick() = handleCodeSubmit(saveToBitwarden = false)

private fun handleSaveToBitwardenClick() = handleCodeSubmit(saveToBitwarden = true)

private fun handleCodeSubmit(saveToBitwarden: Boolean) {
val isSteamCode = state.code.startsWith(TotpCodeManager.STEAM_CODE_PREFIX)
val sanitizedCode = state.code
.replace(" ", "")
Expand All @@ -87,6 +109,38 @@ class ManualCodeEntryViewModel @Inject constructor(
return
}

if (saveToBitwarden) {
// Save to Bitwarden by kicking off save to Bitwarden flow:
saveValidCodeToBitwarden(sanitizedCode)
} else {
// Save locally by giving entity to AuthRepository and navigating back:
saveValidCodeLocally(sanitizedCode, isSteamCode)
}
}

private fun saveValidCodeToBitwarden(sanitizedCode: String) {
val didLaunchSaveToBitwarden = authenticatorBridgeManager
.startAddTotpLoginItemFlow(
totpUri = "otpauth://totp/?secret=$sanitizedCode&issuer=${state.issuer}",
)
if (!didLaunchSaveToBitwarden) {
mutableStateFlow.update {
it.copy(
dialog = ManualCodeEntryState.DialogState.Error(
title = R.string.something_went_wrong.asText(),
message = R.string.please_try_again.asText(),
),
)
}
} else {
sendEvent(ManualCodeEntryEvent.NavigateBack)
}
}

private fun saveValidCodeLocally(
sanitizedCode: String,
isSteamCode: Boolean,
) {
viewModelScope.launch {
authenticatorRepository.createItem(
AuthenticatorItemEntity(
Expand Down Expand Up @@ -133,6 +187,22 @@ class ManualCodeEntryViewModel @Inject constructor(
}
}

private fun deriveButtonState(
sharedCodesState: SharedVerificationCodesState,
defaultSaveOption: DefaultSaveOption,
): ManualCodeEntryState.ButtonState {
// If syncing with Bitwarden is not enabled, show local save only:
if (!sharedCodesState.isSyncWithBitwardenEnabled) {
return ManualCodeEntryState.ButtonState.LocalOnly
}
// Otherwise, show save options based on user's preferences:
return when (defaultSaveOption) {
DefaultSaveOption.NONE -> ManualCodeEntryState.ButtonState.SaveToBitwardenPrimary
DefaultSaveOption.BITWARDEN_APP -> ManualCodeEntryState.ButtonState.SaveToBitwardenPrimary
DefaultSaveOption.LOCAL -> ManualCodeEntryState.ButtonState.SaveLocallyPrimary
}
}

/**
* Models state of the manual entry screen.
*/
Expand All @@ -141,6 +211,7 @@ data class ManualCodeEntryState(
val code: String,
val issuer: String,
val dialog: DialogState?,
val buttonState: ButtonState,
) : Parcelable {

/**
Expand All @@ -166,6 +237,31 @@ data class ManualCodeEntryState(
val message: Text,
) : DialogState()
}

/**
* Models what variation of button states should be shown.
*/
@Parcelize
sealed class ButtonState : Parcelable {

/**
* Show only save locally option.
*/
@Parcelize
data object LocalOnly : ButtonState()

/**
* Show both save locally and save to Bitwarden, with Bitwarden being the primary option.
*/
@Parcelize
data object SaveToBitwardenPrimary : ButtonState()

/**
* Show both save locally and save to Bitwarden, with locally being the primary option.
*/
@Parcelize
data object SaveLocallyPrimary : ButtonState()
}
}

/**
Expand Down Expand Up @@ -205,9 +301,14 @@ sealed class ManualCodeEntryAction {
data object CloseClick : ManualCodeEntryAction()

/**
* The user has submitted a code.
* The user clicked the save locally button.
*/
data object SaveLocallyClick : ManualCodeEntryAction()

/**
* Th user clicked the save to Bitwarden button.
*/
data object CodeSubmit : ManualCodeEntryAction()
data object SaveToBitwardenClick : ManualCodeEntryAction()

/**
* The user has changed the code text.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.bitwarden.authenticator.ui.authenticator.feature.manualcodeentry

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.unit.dp
import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.ui.platform.components.button.BitwardenFilledButton
import com.bitwarden.authenticator.ui.platform.components.button.BitwardenFilledTonalButton
import com.bitwarden.authenticator.ui.platform.components.button.BitwardenOutlinedButton

/**
* Displays save buttons for saving a manually entered code.
*
* @param state State of the buttons to show.
* @param onSaveLocallyClick Callback invoked when the user clicks save locally.
* @param onSaveToBitwardenClick Callback invoked when the user clicks save to Bitwarden.
*/
@Composable
fun SaveManualCodeButtons(
state: ManualCodeEntryState.ButtonState,
onSaveLocallyClick: () -> Unit,
onSaveToBitwardenClick: () -> Unit,
) {

when (state) {
ManualCodeEntryState.ButtonState.LocalOnly -> {
BitwardenFilledTonalButton(
label = stringResource(id = R.string.add_code),
onClick = onSaveLocallyClick,
modifier = Modifier
.semantics { testTag = "AddCodeButton" }
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}

ManualCodeEntryState.ButtonState.SaveLocallyPrimary -> {
Column {
BitwardenFilledButton(
label = stringResource(id = R.string.add_code_locally),
onClick = onSaveLocallyClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
BitwardenOutlinedButton(
label = stringResource(R.string.add_code_to_bitwarden),
onClick = onSaveToBitwardenClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}

ManualCodeEntryState.ButtonState.SaveToBitwardenPrimary -> {
Column {
BitwardenFilledButton(
label = stringResource(id = R.string.add_code_to_bitwarden),
onClick = onSaveToBitwardenClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
BitwardenOutlinedButton(
label = stringResource(R.string.add_code_locally),
onClick = onSaveLocallyClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
}
}
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,6 @@
<string name="choose_save_location_message">Save this authenticator key here, or add it to a login in your Bitwarden app.</string>
<string name="save_option_as_default">Save option as default</string>
<string name="account_synced_from_bitwarden_app">Account synced from Bitwarden app</string>
<string name="add_code_to_bitwarden">Add code to Bitwarden</string>
<string name="add_code_locally">Add code locally</string>
</resources>
Loading

0 comments on commit 5ac5e31

Please sign in to comment.