From d2c329264cbb8186f4d98d135e6dde24e18ca0ed Mon Sep 17 00:00:00 2001 From: David Perez Date: Thu, 5 Jun 2025 14:20:12 -0500 Subject: [PATCH] PM-22397: Remove custom deletion date (#5311) --- .../button/BitwardenTextSelectionButton.kt | 1 + .../composition/LocalManagerProvider.kt | 10 +- .../manager/di/PlatformUiManagerModule.kt | 3 + .../manager/intent/IntentManagerImpl.kt | 2 +- .../send/addedit/AddEditSendContent.kt | 46 ++---- .../addedit/AddEditSendCustomDateChooser.kt | 88 ------------ .../addedit/AddEditSendDeletionDateChooser.kt | 132 ----------------- .../send/addedit/AddEditSendViewModel.kt | 6 +- .../AddEditSendCustomDateChooser.kt | 135 ++++++++++++++++++ .../AddEditSendDeletionDateChooser.kt | 85 +++++++++++ .../ui/platform/base/BitwardenComposeTest.kt | 5 + 11 files changed, 251 insertions(+), 262 deletions(-) delete mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendCustomDateChooser.kt delete mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendDeletionDateChooser.kt create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendCustomDateChooser.kt create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendDeletionDateChooser.kt diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/button/BitwardenTextSelectionButton.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/button/BitwardenTextSelectionButton.kt index a77c122f3e..facc023072 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/button/BitwardenTextSelectionButton.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/button/BitwardenTextSelectionButton.kt @@ -62,6 +62,7 @@ fun BitwardenTextSelectionButton( ) { Column( modifier = modifier + .defaultMinSize(minHeight = 60.dp) .clearAndSetSemantics { role = semanticRole contentDescription = supportingText diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/composition/LocalManagerProvider.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/composition/LocalManagerProvider.kt index 948ccf3566..bb9b5ae8f2 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/composition/LocalManagerProvider.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/composition/LocalManagerProvider.kt @@ -32,6 +32,7 @@ import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManagerImp import com.x8bit.bitwarden.ui.platform.manager.review.AppReviewManager import com.x8bit.bitwarden.ui.platform.manager.review.AppReviewManagerImpl import com.x8bit.bitwarden.ui.platform.model.FeatureFlagsState +import java.time.Clock /** * Helper [Composable] that wraps a [content] and provides manager classes via [CompositionLocal]. @@ -43,8 +44,9 @@ fun LocalManagerProvider( appResumeStateManager: AppResumeStateManager = AppResumeStateManagerImpl(), appReviewManager: AppReviewManager = AppReviewManagerImpl(activity = activity), biometricsManager: BiometricsManager = BiometricsManagerImpl(activity = activity), + clock: Clock = Clock.systemDefaultZone(), exitManager: ExitManager = ExitManagerImpl(activity = activity), - intentManager: IntentManager = IntentManagerImpl(context = activity), + intentManager: IntentManager = IntentManagerImpl(context = activity, clock = clock), credentialProviderCompletionManager: CredentialProviderCompletionManager = createCredentialProviderCompletionManager(activity = activity), keyChainManager: KeyChainManager = KeyChainManagerImpl(activity = activity), @@ -57,6 +59,7 @@ fun LocalManagerProvider( LocalAppResumeStateManager provides appResumeStateManager, LocalAppReviewManager provides appReviewManager, LocalBiometricsManager provides biometricsManager, + LocalClock provides clock, LocalExitManager provides exitManager, LocalCredentialProviderCompletionManager provides credentialProviderCompletionManager, LocalIntentManager provides intentManager, @@ -83,6 +86,11 @@ val LocalBiometricsManager: ProvidableCompositionLocal = comp error("CompositionLocal BiometricsManager not present") } +/** + * Provides access to the clock throughout the app. + */ +val LocalClock: ProvidableCompositionLocal = compositionLocalOf { Clock.systemDefaultZone() } + /** * Provides access to the exit manager throughout the app. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/di/PlatformUiManagerModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/di/PlatformUiManagerModule.kt index 60ccdea8f0..daf276e032 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/di/PlatformUiManagerModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/di/PlatformUiManagerModule.kt @@ -12,6 +12,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import java.time.Clock import javax.inject.Singleton /** @@ -25,9 +26,11 @@ class PlatformUiManagerModule { @Singleton fun provideIntentManager( @ApplicationContext context: Context, + clock: Clock, ): IntentManager = IntentManagerImpl( context = context, + clock = clock, ) @Provides diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManagerImpl.kt index 1754e2c878..e89d3c2709 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManagerImpl.kt @@ -85,7 +85,7 @@ const val EXTRA_KEY_UV_PERFORMED_DURING_UNLOCK: String = "uv_performed_during_un @OmitFromCoverage class IntentManagerImpl( private val context: Context, - private val clock: Clock = Clock.systemDefaultZone(), + private val clock: Clock, ) : IntentManager { override fun startActivity(intent: Intent) { try { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendContent.kt index 73deb4d15c..fcb2a71f76 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendContent.kt @@ -43,7 +43,6 @@ import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog -import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.header.BitwardenExpandingHeader @@ -51,6 +50,8 @@ import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.x8bit.bitwarden.ui.platform.components.stepper.BitwardenStepper import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager +import com.x8bit.bitwarden.ui.tools.feature.send.addedit.components.AddEditSendCustomDateChooser +import com.x8bit.bitwarden.ui.tools.feature.send.addedit.components.AddEditSendDeletionDateChooser import com.x8bit.bitwarden.ui.tools.feature.send.addedit.handlers.AddEditSendHandlers /** @@ -138,48 +139,23 @@ fun AddEditSendContent( if (isAddMode) { AddEditSendDeletionDateChooser( + onDateSelect = addSendHandlers.onDeletionDateChange, + isEnabled = !policyDisablesSend, modifier = Modifier .testTag("SendDeletionOptionsPicker") .fillMaxWidth() .standardHorizontalMargin(), - dateFormatPattern = state.common.dateFormatPattern, - timeFormatPattern = state.common.timeFormatPattern, - currentZonedDateTime = state.common.deletionDate, - onDateSelect = addSendHandlers.onDeletionDateChange, - isEnabled = !policyDisablesSend, ) } else { - Column( + AddEditSendCustomDateChooser( + originalSelection = state.common.deletionDate, + isEnabled = !policyDisablesSend, + onDateSelect = addSendHandlers.onDeletionDateChange, modifier = Modifier + .testTag("SendCustomDeletionDatePicker") .fillMaxWidth() - .standardHorizontalMargin() - .defaultMinSize(minHeight = 60.dp) - .cardStyle(cardStyle = CardStyle.Full, paddingVertical = 0.dp), - ) { - AddEditSendCustomDateChooser( - modifier = Modifier - .testTag("SendCustomDeletionDatePicker") - .fillMaxWidth(), - dateLabel = stringResource(id = R.string.deletion_date), - timeLabel = stringResource(id = R.string.deletion_time), - dateFormatPattern = state.common.dateFormatPattern, - timeFormatPattern = state.common.timeFormatPattern, - currentZonedDateTime = state.common.deletionDate, - isEnabled = !policyDisablesSend, - onDateSelect = { addSendHandlers.onDeletionDateChange(requireNotNull(it)) }, - ) - BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp)) - Spacer(modifier = Modifier.height(height = 12.dp)) - Text( - text = stringResource(id = R.string.deletion_date_info), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(height = 12.dp)) - } + .standardHorizontalMargin(), + ) } AddEditSendOptions( diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendCustomDateChooser.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendCustomDateChooser.kt deleted file mode 100644 index f1212b6b67..0000000000 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendCustomDateChooser.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.x8bit.bitwarden.ui.tools.feature.send.addedit - -import androidx.compose.foundation.layout.Row -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableLongStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenDateSelectButton -import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTimeSelectButton -import com.x8bit.bitwarden.ui.platform.util.orNow -import java.time.ZonedDateTime -import java.time.temporal.ChronoUnit -import kotlin.time.Duration.Companion.hours -import kotlin.time.Duration.Companion.minutes - -/** - * Displays a UI for selecting a customizable date and time. - * - * @param dateLabel The display label for the date selection field. - * @param timeLabel The display label for the time selection field. - * @param currentZonedDateTime The currently selected time, `null` when no time is selected yet. - * @param dateFormatPattern The pattern to use when displaying the date. - * @param timeFormatPattern The pattern for displaying the time. - * @param onDateSelect The callback for being notified of updates to the selected date and time. - * This will only be `null` when there is no selected time. - * @param isEnabled Whether the button is enabled. - * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. - */ -@Composable -fun AddEditSendCustomDateChooser( - dateLabel: String, - timeLabel: String, - currentZonedDateTime: ZonedDateTime?, - dateFormatPattern: String, - timeFormatPattern: String, - onDateSelect: (ZonedDateTime?) -> Unit, - isEnabled: Boolean, - modifier: Modifier = Modifier, -) { - // This tracks the date component (year, month, and day) and ignores lower level - // components. - var date: ZonedDateTime? by remember { mutableStateOf(currentZonedDateTime) } - // This tracks just the time component (hours and minutes) and ignores the higher level - // components. 0 representing midnight and counting up from there. - var timeMillis: Long by remember { - mutableLongStateOf( - currentZonedDateTime.orNow().let { - it.hour.hours.inWholeMilliseconds + it.minute.minutes.inWholeMilliseconds - }, - ) - } - val derivedDateTimeMillis: ZonedDateTime? by remember { - derivedStateOf { date?.plus(timeMillis, ChronoUnit.MILLIS) } - } - - Row( - modifier = modifier, - ) { - BitwardenDateSelectButton( - modifier = Modifier.weight(1f), - label = dateLabel, - formatPattern = dateFormatPattern, - currentZonedDateTime = currentZonedDateTime, - isEnabled = isEnabled, - onDateSelect = { - date = it - onDateSelect(derivedDateTimeMillis) - }, - cardStyle = null, - ) - BitwardenTimeSelectButton( - modifier = Modifier.weight(1f), - label = timeLabel, - formatPattern = timeFormatPattern, - currentZonedDateTime = currentZonedDateTime, - isEnabled = isEnabled, - onTimeSelect = { hour, minute -> - timeMillis = hour.hours.inWholeMilliseconds + minute.minutes.inWholeMilliseconds - onDateSelect(derivedDateTimeMillis) - }, - cardStyle = null, - ) - } -} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendDeletionDateChooser.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendDeletionDateChooser.kt deleted file mode 100644 index bbe751e45c..0000000000 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendDeletionDateChooser.kt +++ /dev/null @@ -1,132 +0,0 @@ -package com.x8bit.bitwarden.ui.tools.feature.send.addedit - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.bitwarden.ui.platform.base.util.cardStyle -import com.bitwarden.ui.platform.components.model.CardStyle -import com.bitwarden.ui.platform.theme.BitwardenTheme -import com.bitwarden.ui.util.Text -import com.bitwarden.ui.util.asText -import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider -import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton -import kotlinx.collections.immutable.toImmutableList -import java.time.ZonedDateTime -import java.time.temporal.ChronoUnit -import kotlin.time.Duration.Companion.days -import kotlin.time.Duration.Companion.hours - -/** - * Displays UX for choosing deletion date of a send. - */ -@Suppress("LongMethod") -@Composable -fun AddEditSendDeletionDateChooser( - currentZonedDateTime: ZonedDateTime, - dateFormatPattern: String, - timeFormatPattern: String, - onDateSelect: (ZonedDateTime) -> Unit, - isEnabled: Boolean, - modifier: Modifier = Modifier, -) { - val defaultOption = DeletionOptions.SEVEN_DAYS - val options = DeletionOptions.entries.associateWith { it.text() } - var selectedOption: DeletionOptions by rememberSaveable { mutableStateOf(defaultOption) } - Column( - modifier = modifier - .defaultMinSize(minHeight = 60.dp) - .cardStyle(cardStyle = CardStyle.Full, paddingVertical = 0.dp), - ) { - BitwardenMultiSelectButton( - label = stringResource(id = R.string.deletion_date), - isEnabled = isEnabled, - options = options.values.toImmutableList(), - selectedOption = selectedOption.text(), - onOptionSelected = { selected -> - selectedOption = options.entries.first { it.value == selected }.key - if (selectedOption != DeletionOptions.CUSTOM) { - onDateSelect( - // Add the appropriate milliseconds offset based on the selected option - ZonedDateTime.now().plus(selectedOption.offsetMillis, ChronoUnit.MILLIS), - ) - } - }, - insets = PaddingValues(top = 6.dp, bottom = 4.dp), - cardStyle = null, - ) - AnimatedVisibility(visible = selectedOption == DeletionOptions.CUSTOM) { - Column { - BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp)) - AddEditSendCustomDateChooser( - dateLabel = stringResource(id = R.string.deletion_date), - timeLabel = stringResource(id = R.string.deletion_time), - currentZonedDateTime = currentZonedDateTime, - dateFormatPattern = dateFormatPattern, - timeFormatPattern = timeFormatPattern, - onDateSelect = { onDateSelect(requireNotNull(it)) }, - isEnabled = isEnabled, - ) - } - } - BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp)) - Spacer(modifier = Modifier.height(height = 12.dp)) - Text( - text = stringResource(id = R.string.deletion_date_info), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(height = 12.dp)) - } -} - -private enum class DeletionOptions( - val text: Text, - val offsetMillis: Long, -) { - ONE_HOUR( - text = R.string.one_hour.asText(), - offsetMillis = 1.hours.inWholeMilliseconds, - ), - ONE_DAY( - text = R.string.one_day.asText(), - offsetMillis = 1.days.inWholeMilliseconds, - ), - TWO_DAYS( - text = R.string.two_days.asText(), - offsetMillis = 2.days.inWholeMilliseconds, - ), - THREE_DAYS( - text = R.string.three_days.asText(), - offsetMillis = 3.days.inWholeMilliseconds, - ), - SEVEN_DAYS( - text = R.string.seven_days.asText(), - offsetMillis = 7.days.inWholeMilliseconds, - ), - THIRTY_DAYS( - text = R.string.thirty_days.asText(), - offsetMillis = 30.days.inWholeMilliseconds, - ), - CUSTOM( - text = R.string.custom.asText(), - offsetMillis = -1L, - ), -} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendViewModel.kt index 890cb53ad6..8278b9917b 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendViewModel.kt @@ -759,11 +759,7 @@ data class AddEditSendState( val expirationDate: ZonedDateTime?, val sendUrl: String?, val hasPassword: Boolean, - ) : Parcelable { - val dateFormatPattern: String get() = "M/d/yyyy" - - val timeFormatPattern: String get() = "hh:mm a" - } + ) : Parcelable /** * Models what type the user is trying to send. diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendCustomDateChooser.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendCustomDateChooser.kt new file mode 100644 index 0000000000..a3f6ebd33a --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendCustomDateChooser.kt @@ -0,0 +1,135 @@ +package com.x8bit.bitwarden.ui.tools.feature.send.addedit.components + +import android.os.Parcelable +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.bitwarden.ui.platform.components.model.CardStyle +import com.bitwarden.ui.util.Text +import com.bitwarden.ui.util.asText +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton +import com.x8bit.bitwarden.ui.platform.composition.LocalClock +import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.parcelize.Parcelize +import java.time.Clock +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours + +/** + * Displays a UI for selecting a customizable date and time. + * + * @param originalSelection The originally selected time value, this cannot be changed after being + * set. + * @param onDateSelect The callback for being notified of updates to the selected date and time. + * @param isEnabled Whether the button is enabled. + * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. + * @param clock The clock used for formatting and timezone purposes. + */ +@Composable +fun AddEditSendCustomDateChooser( + originalSelection: ZonedDateTime, + onDateSelect: (ZonedDateTime) -> Unit, + isEnabled: Boolean, + modifier: Modifier = Modifier, + clock: Clock = LocalClock.current, +) { + val originalSelectionOption: CustomDeletionOption.Current = rememberSaveable { + CustomDeletionOption.Current(time = originalSelection) + } + val options = persistentMapOf( + originalSelectionOption to originalSelectionOption.getText(clock = clock)(), + CustomDeletionOption.OneHour to CustomDeletionOption.OneHour.getText(clock = clock)(), + CustomDeletionOption.OneDay to CustomDeletionOption.OneDay.getText(clock = clock)(), + CustomDeletionOption.TwoDays to CustomDeletionOption.TwoDays.getText(clock = clock)(), + CustomDeletionOption.ThreeDays to CustomDeletionOption.ThreeDays.getText(clock = clock)(), + CustomDeletionOption.SevenDays to CustomDeletionOption.SevenDays.getText(clock = clock)(), + CustomDeletionOption.ThirtyDays to CustomDeletionOption.ThirtyDays.getText(clock = clock)(), + ) + var currentSelectionOption: CustomDeletionOption by rememberSaveable(originalSelectionOption) { + mutableStateOf(value = originalSelectionOption) + } + BitwardenMultiSelectButton( + label = stringResource(id = R.string.deletion_date), + isEnabled = isEnabled, + options = options.values.toImmutableList(), + selectedOption = currentSelectionOption.getText(clock = clock).invoke(), + onOptionSelected = { selected -> + currentSelectionOption = options.entries.first { it.value == selected }.key + onDateSelect( + (currentSelectionOption as? CustomDeletionOption.Current) + ?.time + ?: ZonedDateTime + .now(clock) + .plus(currentSelectionOption.offsetMillis, ChronoUnit.MILLIS), + ) + }, + supportingText = stringResource(id = R.string.deletion_date_info), + insets = PaddingValues(top = 6.dp, bottom = 4.dp), + cardStyle = CardStyle.Full, + modifier = modifier, + ) +} + +@Parcelize +private sealed class CustomDeletionOption : Parcelable { + abstract val offsetMillis: Long + abstract fun getText(clock: Clock): Text + + @Parcelize + data class Current( + val time: ZonedDateTime, + ) : CustomDeletionOption() { + override val offsetMillis: Long get() = 0L + + override fun getText( + clock: Clock, + ): Text = time.toFormattedPattern(pattern = "d MMM, yyyy, h:mma", clock = clock).asText() + } + + @Parcelize + data object OneHour : CustomDeletionOption() { + override val offsetMillis: Long get() = 1.hours.inWholeMilliseconds + override fun getText(clock: Clock): Text = R.string.one_hour.asText() + } + + @Parcelize + data object OneDay : CustomDeletionOption() { + override val offsetMillis: Long get() = 1.days.inWholeMilliseconds + override fun getText(clock: Clock): Text = R.string.one_day.asText() + } + + @Parcelize + data object TwoDays : CustomDeletionOption() { + override val offsetMillis: Long get() = 2.days.inWholeMilliseconds + override fun getText(clock: Clock): Text = R.string.two_days.asText() + } + + @Parcelize + data object ThreeDays : CustomDeletionOption() { + override val offsetMillis: Long get() = 3.days.inWholeMilliseconds + override fun getText(clock: Clock): Text = R.string.three_days.asText() + } + + @Parcelize + data object SevenDays : CustomDeletionOption() { + override val offsetMillis: Long get() = 7.days.inWholeMilliseconds + override fun getText(clock: Clock): Text = R.string.seven_days.asText() + } + + @Parcelize + data object ThirtyDays : CustomDeletionOption() { + override val offsetMillis: Long get() = 30.days.inWholeMilliseconds + override fun getText(clock: Clock): Text = R.string.thirty_days.asText() + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendDeletionDateChooser.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendDeletionDateChooser.kt new file mode 100644 index 0000000000..5291e28c0f --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendDeletionDateChooser.kt @@ -0,0 +1,85 @@ +package com.x8bit.bitwarden.ui.tools.feature.send.addedit.components + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.bitwarden.ui.platform.components.model.CardStyle +import com.bitwarden.ui.util.Text +import com.bitwarden.ui.util.asText +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton +import com.x8bit.bitwarden.ui.platform.composition.LocalClock +import kotlinx.collections.immutable.toImmutableList +import java.time.Clock +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours + +/** + * Displays UX for choosing deletion date of a send. + */ +@Composable +fun AddEditSendDeletionDateChooser( + onDateSelect: (ZonedDateTime) -> Unit, + isEnabled: Boolean, + modifier: Modifier = Modifier, + clock: Clock = LocalClock.current, +) { + val options = DeletionOption.entries.associateWith { it.text() } + var selectedOption: DeletionOption by rememberSaveable { + mutableStateOf(value = DeletionOption.SEVEN_DAYS) + } + BitwardenMultiSelectButton( + label = stringResource(id = R.string.deletion_date), + isEnabled = isEnabled, + options = options.values.toImmutableList(), + selectedOption = selectedOption.text(), + onOptionSelected = { selected -> + selectedOption = options.entries.first { it.value == selected }.key + onDateSelect( + ZonedDateTime.now(clock).plus(selectedOption.offsetMillis, ChronoUnit.MILLIS), + ) + }, + supportingText = stringResource(id = R.string.deletion_date_info), + insets = PaddingValues(top = 6.dp, bottom = 4.dp), + cardStyle = CardStyle.Full, + modifier = modifier, + ) +} + +private enum class DeletionOption( + val text: Text, + val offsetMillis: Long, +) { + ONE_HOUR( + text = R.string.one_hour.asText(), + offsetMillis = 1.hours.inWholeMilliseconds, + ), + ONE_DAY( + text = R.string.one_day.asText(), + offsetMillis = 1.days.inWholeMilliseconds, + ), + TWO_DAYS( + text = R.string.two_days.asText(), + offsetMillis = 2.days.inWholeMilliseconds, + ), + THREE_DAYS( + text = R.string.three_days.asText(), + offsetMillis = 3.days.inWholeMilliseconds, + ), + SEVEN_DAYS( + text = R.string.seven_days.asText(), + offsetMillis = 7.days.inWholeMilliseconds, + ), + THIRTY_DAYS( + text = R.string.thirty_days.asText(), + offsetMillis = 30.days.inWholeMilliseconds, + ), +} diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt index fe9e970e1d..e3537e7751 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt @@ -16,6 +16,9 @@ import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager import com.x8bit.bitwarden.ui.platform.manager.review.AppReviewManager import com.x8bit.bitwarden.ui.platform.model.FeatureFlagsState import io.mockk.mockk +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset abstract class BitwardenComposeTest : BaseComposeTest() { @@ -32,6 +35,7 @@ abstract class BitwardenComposeTest : BaseComposeTest() { appResumeStateManager: AppResumeStateManager = mockk(), appReviewManager: AppReviewManager = mockk(), biometricsManager: BiometricsManager = mockk(), + clock: Clock = Clock.fixed(Instant.parse("2023-10-27T12:00:00Z"), ZoneOffset.UTC), exitManager: ExitManager = mockk(), intentManager: IntentManager = mockk(), credentialProviderCompletionManager: CredentialProviderCompletionManager = mockk(), @@ -46,6 +50,7 @@ abstract class BitwardenComposeTest : BaseComposeTest() { appResumeStateManager = appResumeStateManager, appReviewManager = appReviewManager, biometricsManager = biometricsManager, + clock = clock, exitManager = exitManager, intentManager = intentManager, credentialProviderCompletionManager = credentialProviderCompletionManager,