Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -106,8 +106,16 @@ class TotpCodeManagerImpl @Inject constructor(
authenticatorSdkSource
.generateTotp(item.otpUri, dateTime)
.onSuccess { response ->
val nextCodeValue = authenticatorSdkSource
.generateTotp(
item.otpUri,
dateTime.plusSeconds(response.period.toLong()),
)
.getOrNull()
?.code
verificationCodeItem = VerificationCodeItem(
code = response.code,
nextCode = nextCodeValue,
periodSeconds = response.period.toInt(),
timeLeftSeconds = response.period.toInt() -
(time % response.period.toInt()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import com.bitwarden.authenticator.data.authenticator.repository.model.Authentic
* of verification items.
*
* @property code The verification code for the item.
* @property nextCode The verification code for the next time period.
* @property periodSeconds The time span where the code is valid in seconds.
* @property timeLeftSeconds The seconds remaining until a new code is required.
* @property issueTime The time the verification code was issued.
* @property id The cipher id of the item.
* @property username The username associated with the item.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was bugging me. Thank you for removing it. lol

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same πŸ˜†

*/
data class VerificationCodeItem(
val code: String,
val nextCode: String?,
val periodSeconds: Int,
val timeLeftSeconds: Int,
val issueTime: Long,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ interface SettingsDiskSource : FlightRecorderDiskSource {
*/
val isCrashLoggingEnabledFlow: Flow<Boolean?>

/**
* The current setting for showing the next TOTP code.
*/
var isShowNextCodeEnabled: Boolean?

/**
* Emits updates that track the [isShowNextCodeEnabled] value.
*/
val isShowNextCodeEnabledFlow: Flow<Boolean?>

/**
* Whether the user has previously dismissed the download Bitwarden action card.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ private const val ALERT_THRESHOLD_SECONDS_KEY = "alertThresholdSeconds"
private const val APP_TIMEOUT_IN_MINUTES_KEY = "appTimeoutInMinutes"
private const val FIRST_LAUNCH_KEY = "hasSeenWelcomeTutorial"
private const val CRASH_LOGGING_ENABLED_KEY = "crashLoggingEnabled"
private const val SHOW_NEXT_CODE_ENABLED_KEY = "showNextCodeEnabled"
private const val SCREEN_CAPTURE_ALLOW_KEY = "screenCaptureAllowed"
private const val HAS_USER_DISMISSED_DOWNLOAD_BITWARDEN_KEY =
"hasUserDismissedDownloadBitwardenCard"
Expand Down Expand Up @@ -53,6 +54,9 @@ class SettingsDiskSourceImpl(
private val mutableIsCrashLoggingEnabledFlow =
bufferedMutableSharedFlow<Boolean?>()

private val mutableIsShowNextCodeEnabledFlow =
bufferedMutableSharedFlow<Boolean?>()

private val mutableDefaultSaveOptionFlow =
bufferedMutableSharedFlow<DefaultSaveOption>()

Expand Down Expand Up @@ -157,6 +161,17 @@ class SettingsDiskSourceImpl(
get() = mutableIsCrashLoggingEnabledFlow
.onSubscription { emit(getBoolean(CRASH_LOGGING_ENABLED_KEY)) }

override var isShowNextCodeEnabled: Boolean?
get() = getBoolean(key = SHOW_NEXT_CODE_ENABLED_KEY)
set(value) {
putBoolean(key = SHOW_NEXT_CODE_ENABLED_KEY, value = value)
mutableIsShowNextCodeEnabledFlow.tryEmit(value)
}

override val isShowNextCodeEnabledFlow: Flow<Boolean?>
get() = mutableIsShowNextCodeEnabledFlow
.onSubscription { emit(getBoolean(SHOW_NEXT_CODE_ENABLED_KEY)) }

override var hasUserDismissedDownloadBitwardenCard: Boolean?
get() = getBoolean(HAS_USER_DISMISSED_DOWNLOAD_BITWARDEN_KEY)
set(value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ interface SettingsRepository : FlightRecorderManager {
*/
val isCrashLoggingEnabledFlow: Flow<Boolean>

/**
* Whether the next TOTP code preview is enabled.
*/
var isShowNextCodeEnabled: Boolean

/**
* Emits updates that track the [isShowNextCodeEnabled] value.
*/
val isShowNextCodeEnabledFlow: Flow<Boolean>

/**
* Whether the user has previously dismissed the download Bitwarden action card.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,22 @@ class SettingsRepositoryImpl(
initialValue = isCrashLoggingEnabled,
)

override var isShowNextCodeEnabled: Boolean
get() = settingsDiskSource.isShowNextCodeEnabled ?: false
set(value) {
settingsDiskSource.isShowNextCodeEnabled = value
}

override val isShowNextCodeEnabledFlow: Flow<Boolean>
get() = settingsDiskSource
.isShowNextCodeEnabledFlow
.map { it ?: isShowNextCodeEnabled }
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Eagerly,
initialValue = isShowNextCodeEnabled,
)

override var hasUserDismissedDownloadBitwardenCard: Boolean
get() = settingsDiskSource.hasUserDismissedDownloadBitwardenCard ?: false
set(value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class ItemListingViewModel @Inject constructor(
) : BaseViewModel<ItemListingState, ItemListingEvent, ItemListingAction>(
initialState = ItemListingState(
alertThresholdSeconds = settingsRepository.authenticatorAlertThresholdSeconds,
isShowNextCodeEnabled = settingsRepository.isShowNextCodeEnabled,
viewState = ItemListingState.ViewState.Loading,
dialog = null,
),
Expand All @@ -76,6 +77,12 @@ class ItemListingViewModel @Inject constructor(
.onEach(::sendAction)
.launchIn(viewModelScope)

settingsRepository
.isShowNextCodeEnabledFlow
.map { ItemListingAction.Internal.IsShowNextCodeEnabledReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)

combine(
flow = authenticatorRepository.getLocalVerificationCodesFlow(),
flow2 = authenticatorRepository.sharedCodesStateFlow,
Expand Down Expand Up @@ -244,6 +251,10 @@ class ItemListingViewModel @Inject constructor(
handleAlertThresholdSecondsReceive(internalAction)
}

is ItemListingAction.Internal.IsShowNextCodeEnabledReceive -> {
handleIsShowNextCodeEnabledReceive(internalAction)
}

is ItemListingAction.Internal.TotpCodeReceive -> {
handleTotpCodeReceive(internalAction)
}
Expand Down Expand Up @@ -450,6 +461,16 @@ class ItemListingViewModel @Inject constructor(
}
}

private fun handleIsShowNextCodeEnabledReceive(
action: ItemListingAction.Internal.IsShowNextCodeEnabledReceive,
) {
mutableStateFlow.update {
it.copy(
isShowNextCodeEnabled = action.isShowNextCodeEnabled,
)
}
}

private fun handleDialogDismiss() {
mutableStateFlow.update {
it.copy(dialog = null)
Expand Down Expand Up @@ -483,6 +504,7 @@ class ItemListingViewModel @Inject constructor(
val currentCodes = viewState?.sharedItems as? SharedCodesDisplayState.Codes
action.sharedCodesState.toSharedCodesDisplayState(
alertThresholdSeconds = state.alertThresholdSeconds,
isShowNextCodeEnabled = state.isShowNextCodeEnabled,
currentSections = currentCodes?.sections.orEmpty(),
)
}
Expand All @@ -504,6 +526,7 @@ class ItemListingViewModel @Inject constructor(
.map {
it.toDisplayItem(
alertThresholdSeconds = state.alertThresholdSeconds,
isShowNextCodeEnabled = state.isShowNextCodeEnabled,
sharedVerificationCodesState = authenticatorRepository
.sharedCodesStateFlow
.value,
Expand All @@ -517,6 +540,7 @@ class ItemListingViewModel @Inject constructor(
.map {
it.toDisplayItem(
alertThresholdSeconds = state.alertThresholdSeconds,
isShowNextCodeEnabled = state.isShowNextCodeEnabled,
sharedVerificationCodesState = authenticatorRepository
.sharedCodesStateFlow
.value,
Expand Down Expand Up @@ -712,6 +736,7 @@ const val ISSUER = "issuer"
@Parcelize
data class ItemListingState(
val alertThresholdSeconds: Int,
val isShowNextCodeEnabled: Boolean,
val viewState: ViewState,
val dialog: DialogState?,
) : Parcelable {
Expand Down Expand Up @@ -985,6 +1010,13 @@ sealed class ItemListingAction {
val thresholdSeconds: Int,
) : Internal()

/**
* Indicates the show next code enabled setting has been received.
*/
data class IsShowNextCodeEnabledReceive(
val isShowNextCodeEnabled: Boolean,
) : Internal()

/**
* Indicates a new TOTP code scan result has been received.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRe
import com.bitwarden.authenticator.data.authenticator.repository.model.DeleteItemResult
import com.bitwarden.authenticator.data.authenticator.repository.model.SharedVerificationCodesState
import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManager
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticator.ui.authenticator.feature.util.toDisplayItem
import com.bitwarden.authenticator.ui.authenticator.feature.util.toSharedCodesDisplayState
import com.bitwarden.authenticator.ui.platform.components.listitem.model.SharedCodesDisplayState
Expand Down Expand Up @@ -51,10 +52,12 @@ class ItemSearchViewModel @Inject constructor(
private val clipboardManager: BitwardenClipboardManager,
private val authenticatorRepository: AuthenticatorRepository,
private val authenticatorBridgeManager: AuthenticatorBridgeManager,
private val settingsRepository: SettingsRepository,
) : BaseViewModel<ItemSearchState, ItemSearchEvent, ItemSearchAction>(
initialState = savedStateHandle[KEY_STATE]
?: ItemSearchState(
searchTerm = "",
isShowNextCodeEnabled = settingsRepository.isShowNextCodeEnabled,
viewState = ItemSearchState.ViewState.Empty(message = null),
dialog = null,
),
Expand All @@ -66,6 +69,11 @@ class ItemSearchViewModel @Inject constructor(
.map(ItemSearchEvent::ShowSnackbar)
.onEach(::sendEvent)
.launchIn(viewModelScope)
settingsRepository
.isShowNextCodeEnabledFlow
.map { ItemSearchAction.Internal.IsShowNextCodeEnabledReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
combine(
authenticatorRepository.getLocalVerificationCodesFlow(),
authenticatorRepository.sharedCodesStateFlow,
Expand Down Expand Up @@ -164,6 +172,10 @@ class ItemSearchViewModel @Inject constructor(
}

is ItemSearchAction.Internal.DeleteItemReceive -> handleDeleteItemReceive(action)

is ItemSearchAction.Internal.IsShowNextCodeEnabledReceive -> {
handleIsShowNextCodeEnabledReceive(action)
}
}
}

Expand Down Expand Up @@ -212,6 +224,14 @@ class ItemSearchViewModel @Inject constructor(
}
}

private fun handleIsShowNextCodeEnabledReceive(
action: ItemSearchAction.Internal.IsShowNextCodeEnabledReceive,
) {
mutableStateFlow.update {
it.copy(isShowNextCodeEnabled = action.isShowNextCodeEnabled)
}
}

//region Utility Functions
private fun recalculateViewState() {
authenticatorRepository.getLocalVerificationCodesFlow()
Expand Down Expand Up @@ -291,7 +311,10 @@ class ItemSearchViewModel @Inject constructor(
is SharedVerificationCodesState.Success -> {
sharedData
.copy(items = sharedData.items.filterAndOrganize(searchTerm = searchTerm))
.toSharedCodesDisplayState(alertThresholdSeconds = 7)
.toSharedCodesDisplayState(
alertThresholdSeconds = 7,
isShowNextCodeEnabled = state.isShowNextCodeEnabled,
)
}
}

Expand All @@ -308,6 +331,7 @@ class ItemSearchViewModel @Inject constructor(
.map {
it.toDisplayItem(
alertThresholdSeconds = 7,
isShowNextCodeEnabled = state.isShowNextCodeEnabled,
sharedVerificationCodesState = authenticatorRepository
.sharedCodesStateFlow
.value,
Expand All @@ -329,6 +353,7 @@ class ItemSearchViewModel @Inject constructor(
@Parcelize
data class ItemSearchState(
val searchTerm: String,
val isShowNextCodeEnabled: Boolean,
val viewState: ViewState,
val dialog: DialogState?,
) : Parcelable {
Expand Down Expand Up @@ -457,6 +482,13 @@ sealed class ItemSearchAction {
data class DeleteItemReceive(
val result: DeleteItemResult,
) : Internal()

/**
* Indicates the show next code enabled setting has been received.
*/
data class IsShowNextCodeEnabledReceive(
val isShowNextCodeEnabled: Boolean,
) : Internal()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import kotlinx.collections.immutable.toImmutableList
*/
fun SharedVerificationCodesState.Success.toSharedCodesDisplayState(
alertThresholdSeconds: Int,
isShowNextCodeEnabled: Boolean,
currentSections: List<SharedCodesDisplayState.SharedCodesAccountSection> = emptyList(),
): SharedCodesDisplayState.Codes {
val codesMap =
Expand All @@ -25,6 +26,7 @@ fun SharedVerificationCodesState.Success.toSharedCodesDisplayState(
codesMap[it.source]?.add(
it.toDisplayItem(
alertThresholdSeconds = alertThresholdSeconds,
isShowNextCodeEnabled = isShowNextCodeEnabled,
// Always map based on Error state, because shared codes will never
// show "Copy to Bitwarden vault" action.
sharedVerificationCodesState = SharedVerificationCodesState.Error,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import com.bitwarden.authenticator.data.authenticator.repository.model.Authentic
import com.bitwarden.authenticator.data.authenticator.repository.model.SharedVerificationCodesState
import com.bitwarden.authenticator.ui.platform.components.listitem.model.VerificationCodeDisplayItem

private const val NEXT_CODE_THRESHOLD_SECONDS = 10

/**
* Converts [VerificationCodeItem] to a [VerificationCodeDisplayItem].
*/
fun VerificationCodeItem.toDisplayItem(
alertThresholdSeconds: Int,
isShowNextCodeEnabled: Boolean,
sharedVerificationCodesState: SharedVerificationCodesState,
showOverflow: Boolean,
): VerificationCodeDisplayItem = VerificationCodeDisplayItem(
Expand All @@ -25,6 +28,9 @@ fun VerificationCodeItem.toDisplayItem(
periodSeconds = periodSeconds,
alertThresholdSeconds = alertThresholdSeconds,
authCode = code,
nextAuthCode = nextCode?.takeIf {
isShowNextCodeEnabled && timeLeftSeconds < NEXT_CODE_THRESHOLD_SECONDS
},
showOverflow = showOverflow,
favorite = (source as? AuthenticatorItem.Source.Local)?.isFavorite ?: false,
showMoveToBitwarden = when (source) {
Expand Down
Loading
Loading