Add About section to settings (#63)

This commit is contained in:
Patrick Honkonen
2024-04-26 14:13:38 -04:00
committed by GitHub
parent 1eb4a9770e
commit dd4a0a502d
6 changed files with 390 additions and 14 deletions

View File

@@ -61,7 +61,7 @@ fun NavGraphBuilder.itemListingGraph(
settingsGraph(
navController = navController,
onNavigateToExport = navigateToExport,
onNavigateToTutorial = navigateToTutorial,
onNavigateToTutorial = navigateToTutorial
)
}
}

View File

@@ -0,0 +1,88 @@
package com.bitwarden.authenticator.ui.platform.components.row
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.tooling.preview.Preview
import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.ui.platform.base.util.mirrorIfRtl
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.authenticator.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
/**
* Represents a row of text that can be clicked on and contains an external link.
* A confirmation dialog will always be displayed before [onConfirmClick] is invoked.
*
* @param text The label for the row as a [String].
* @param onConfirmClick The callback when the confirm button of the dialog is clicked.
* @param modifier The modifier to be applied to the layout.
* @param withDivider Indicates if a divider should be drawn on the bottom of the row, defaults
* to `true`.
* @param dialogTitle The title of the dialog displayed when the user clicks this item.
* @param dialogMessage The message of the dialog displayed when the user clicks this item.
* @param dialogConfirmButtonText The text on the confirm button of the dialog displayed when the
* user clicks this item.
* @param dialogDismissButtonText The text on the dismiss button of the dialog displayed when the
* user clicks this item.
*/
@Composable
fun BitwardenExternalLinkRow(
text: String,
onConfirmClick: () -> Unit,
modifier: Modifier = Modifier,
withDivider: Boolean = true,
dialogTitle: String,
dialogMessage: String,
dialogConfirmButtonText: String = stringResource(id = R.string.continue_text),
dialogDismissButtonText: String = stringResource(id = R.string.cancel),
) {
var shouldShowDialog by rememberSaveable { mutableStateOf(false) }
BitwardenTextRow(
text = text,
onClick = { shouldShowDialog = true },
modifier = modifier,
withDivider = withDivider,
) {
Icon(
modifier = Modifier.mirrorIfRtl(),
painter = rememberVectorPainter(id = R.drawable.ic_external_link),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
)
}
if (shouldShowDialog) {
BitwardenTwoButtonDialog(
title = dialogTitle,
message = dialogMessage,
confirmButtonText = dialogConfirmButtonText,
dismissButtonText = dialogDismissButtonText,
onConfirmClick = {
shouldShowDialog = false
onConfirmClick()
},
onDismissClick = { shouldShowDialog = false },
onDismissRequest = { shouldShowDialog = false },
)
}
}
@Preview
@Composable
private fun BitwardenExternalLinkRow_preview() {
AuthenticatorTheme {
BitwardenExternalLinkRow(
text = "Linked Text",
onConfirmClick = { },
dialogTitle = "",
dialogMessage = "",
)
}
}

View File

@@ -1,7 +1,13 @@
package com.bitwarden.authenticator.ui.platform.feature.settings
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -9,9 +15,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
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
@@ -20,18 +29,24 @@ 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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.ui.platform.base.util.EventsEffect
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.mirrorIfRtl
import com.bitwarden.authenticator.ui.platform.components.appbar.BitwardenMediumTopAppBar
@@ -40,13 +55,18 @@ import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicD
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenSelectionDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenSelectionRow
import com.bitwarden.authenticator.ui.platform.components.header.BitwardenListHeaderText
import com.bitwarden.authenticator.ui.platform.components.row.BitwardenExternalLinkRow
import com.bitwarden.authenticator.ui.platform.components.row.BitwardenTextRow
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.authenticator.ui.platform.components.toggle.BitwardenWideSwitch
import com.bitwarden.authenticator.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppLanguage
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.authenticator.ui.platform.manager.biometrics.BiometricsManager
import com.bitwarden.authenticator.ui.platform.manager.intent.IntentManager
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
import com.bitwarden.authenticator.ui.platform.theme.LocalBiometricsManager
import com.bitwarden.authenticator.ui.platform.theme.LocalIntentManager
import com.bitwarden.authenticator.ui.platform.util.displayLabel
/**
@@ -57,6 +77,7 @@ import com.bitwarden.authenticator.ui.platform.util.displayLabel
fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(),
biometricsManager: BiometricsManager = LocalBiometricsManager.current,
intentManager: IntentManager = LocalIntentManager.current,
onNavigateToTutorial: () -> Unit,
onNavigateToExport: () -> Unit,
) {
@@ -68,17 +89,26 @@ fun SettingsScreen(
when (event) {
SettingsEvent.NavigateToTutorial -> onNavigateToTutorial()
SettingsEvent.NavigateToExport -> onNavigateToExport()
SettingsEvent.NavigateToHelpCenter -> {
intentManager.launchUri("https://bitwarden.com/help".toUri())
}
SettingsEvent.NavigateToPrivacyPolicy -> {
intentManager.launchUri("https://bitwarden.com/privacy".toUri())
}
}
}
BitwardenScaffold(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenMediumTopAppBar(
title = stringResource(id = R.string.settings),
scrollBehavior = scrollBehavior
)
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
) { innerPadding ->
Column(
modifier = Modifier
@@ -125,8 +155,37 @@ fun SettingsScreen(
{
viewModel.trySendAction(SettingsAction.HelpClick.ShowTutorialClick)
}
},
onHelpCenterClick = remember(viewModel) {
{
viewModel.trySendAction(SettingsAction.HelpClick.HelpCenterClick)
}
}
)
Spacer(modifier = Modifier.height(16.dp))
AboutSettings(
state = state,
onPrivacyPolicyClick = remember(viewModel) {
{ viewModel.trySendAction(SettingsAction.AboutClick.PrivacyPolicyClick) }
},
onVersionClick = remember(viewModel) {
{ viewModel.trySendAction(SettingsAction.AboutClick.VersionClick) }
}
)
Box(
modifier = Modifier
.defaultMinSize(minHeight = 56.dp)
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
Text(
modifier = Modifier.padding(end = 16.dp),
text = state.copyrightInfo.invoke(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
}
}
@@ -365,6 +424,7 @@ private fun ThemeSelectionRow(
private fun HelpSettings(
modifier: Modifier = Modifier,
onTutorialClick: () -> Unit,
onHelpCenterClick: () -> Unit,
) {
BitwardenListHeaderText(
modifier = Modifier.padding(horizontal = 16.dp),
@@ -376,6 +436,102 @@ private fun HelpSettings(
modifier = modifier,
withDivider = true,
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenExternalLinkRow(
text = stringResource(id = R.string.bitwarden_help_center),
onConfirmClick = onHelpCenterClick,
dialogTitle = stringResource(id = R.string.continue_to_help_center),
dialogMessage = stringResource(
id = R.string.learn_more_about_how_to_use_bitwarden_on_the_help_center,
),
)
}
//endregion Help settings
//region About settings
@Composable
private fun AboutSettings(
state: SettingsState,
onPrivacyPolicyClick: () -> Unit,
onVersionClick: () -> Unit,
) {
BitwardenListHeaderText(
modifier = Modifier.padding(horizontal = 16.dp),
label = stringResource(id = R.string.about)
)
BitwardenExternalLinkRow(
text = stringResource(id = R.string.privacy_policy),
onConfirmClick = onPrivacyPolicyClick,
dialogTitle = stringResource(id = R.string.continue_to_privacy_policy),
dialogMessage = stringResource(
id = R.string.privacy_policy_description_long,
),
)
CopyRow(
text = state.version,
onClick = onVersionClick,
)
}
@Composable
private fun CopyRow(
text: Text,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val resources = LocalContext.current.resources
Box(
contentAlignment = Alignment.BottomCenter,
modifier = modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary),
onClick = onClick,
)
.semantics(mergeDescendants = true) {
contentDescription = text.toString(resources)
},
) {
Row(
modifier = Modifier
.defaultMinSize(minHeight = 56.dp)
.padding(start = 16.dp, end = 24.dp, top = 8.dp, bottom = 8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.padding(end = 16.dp)
.weight(1f),
text = text(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
Icon(
painter = rememberVectorPainter(id = R.drawable.ic_copy),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
)
}
HorizontalDivider(
modifier = Modifier.padding(start = 16.dp),
thickness = 1.dp,
color = MaterialTheme.colorScheme.outlineVariant,
)
}
}
//endregion About settings
@Preview
@Composable
private fun CopyRow_preview() {
AuthenticatorTheme {
CopyRow(
text = "Copyable Text".asText(),
onClick = { },
)
}
}

View File

@@ -5,18 +5,23 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.authenticator.BuildConfig
import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManager
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticator.data.platform.repository.model.BiometricsKeyResult
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.concat
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppLanguage
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import java.time.Clock
import java.time.Year
import javax.inject.Inject
private const val KEY_STATE = "state"
@@ -27,17 +32,17 @@ private const val KEY_STATE = "state"
@HiltViewModel
class SettingsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
val settingsRepository: SettingsRepository,
clock: Clock,
private val settingsRepository: SettingsRepository,
private val clipboardManager: BitwardenClipboardManager,
) : BaseViewModel<SettingsState, SettingsEvent, SettingsAction>(
initialState = savedStateHandle[KEY_STATE]
?: SettingsState(
appearance = SettingsState.Appearance(
language = settingsRepository.appLanguage,
theme = settingsRepository.appTheme,
),
isUnlockWithBiometricsEnabled = settingsRepository.isUnlockWithBiometricsEnabled,
dialog = null,
),
?: createInitialState(
clock,
settingsRepository.appLanguage,
settingsRepository.appTheme,
settingsRepository.isUnlockWithBiometricsEnabled
)
) {
override fun handleAction(action: SettingsAction) {
when (action) {
@@ -57,6 +62,10 @@ class SettingsViewModel @Inject constructor(
handleHelpClick(action)
}
is SettingsAction.AboutClick -> {
handleAboutClick(action)
}
is SettingsAction.Internal.BiometricsKeyResultReceive -> {
handleBiometricsKeyResultReceive(action)
}
@@ -162,12 +171,63 @@ class SettingsViewModel @Inject constructor(
private fun handleHelpClick(action: SettingsAction.HelpClick) {
when (action) {
SettingsAction.HelpClick.ShowTutorialClick -> handleShowTutorialCLick()
SettingsAction.HelpClick.HelpCenterClick -> handleHelpCenterClick()
}
}
private fun handleShowTutorialCLick() {
sendEvent(SettingsEvent.NavigateToTutorial)
}
private fun handleHelpCenterClick() {
sendEvent(SettingsEvent.NavigateToHelpCenter)
}
private fun handleAboutClick(action: SettingsAction.AboutClick) {
when (action) {
SettingsAction.AboutClick.PrivacyPolicyClick -> {
handlePrivacyPolicyClick()
}
SettingsAction.AboutClick.VersionClick -> {
handleVersionClick()
}
}
}
private fun handlePrivacyPolicyClick() {
sendEvent(SettingsEvent.NavigateToPrivacyPolicy)
}
private fun handleVersionClick() {
clipboardManager.setText(
text = state.copyrightInfo.concat("\n\n".asText()).concat(state.version),
)
}
companion object {
fun createInitialState(
clock: Clock,
appLanguage: AppLanguage,
appTheme: AppTheme,
unlockWithBiometricsEnabled: Boolean,
): SettingsState {
val currentYear = Year.now(clock)
val copyrightInfo = "© Bitwarden Inc. 2015-$currentYear".asText()
return SettingsState(
appearance = SettingsState.Appearance(
language = appLanguage,
theme = appTheme,
),
isUnlockWithBiometricsEnabled = unlockWithBiometricsEnabled,
version = R.string.version
.asText()
.concat(": ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})".asText()),
copyrightInfo = copyrightInfo,
dialog = null,
)
}
}
}
/**
@@ -178,6 +238,8 @@ data class SettingsState(
val appearance: Appearance,
val isUnlockWithBiometricsEnabled: Boolean,
val dialog: Dialog?,
val version: Text,
val copyrightInfo: Text,
) : Parcelable {
@Parcelize
@@ -202,9 +264,26 @@ data class SettingsState(
* Models events for the settings screen.
*/
sealed class SettingsEvent {
/**
* Navigate to the Tutorial screen.
*/
data object NavigateToTutorial : SettingsEvent()
/**
* Navigate to the Export screen.
*/
data object NavigateToExport : SettingsEvent()
/**
* Navigate to the Help Center web page.
*/
data object NavigateToHelpCenter : SettingsEvent()
/**
* Navigate to the privacy policy web page.
*/
data object NavigateToPrivacyPolicy : SettingsEvent()
}
/**
@@ -214,7 +293,14 @@ sealed class SettingsAction(
val dialog: Dialog? = null,
) {
/**
* Represents dialogs that may be displayed by the Settings screen.
*/
sealed class Dialog {
/**
*
*/
data class Loading(
val message: Text,
) : Dialog()
@@ -244,6 +330,11 @@ sealed class SettingsAction(
* Indicates the user clicked launch tutorial.
*/
data object ShowTutorialClick : HelpClick()
/**
* Indicates teh user clicked About.
*/
data object HelpCenterClick : HelpClick()
}
/**
@@ -265,10 +356,30 @@ sealed class SettingsAction(
) : AppearanceChange()
}
/**
* Models actions for the About section of settings.
*/
sealed class AboutClick : SettingsAction() {
/**
* Indicates the user clicked privacy policy.
*/
data object PrivacyPolicyClick : AboutClick()
/**
* Indicates the user clicked version.
*/
data object VersionClick : AboutClick()
}
/**
* Models actions that the Settings screen itself may send.
*/
sealed class Internal {
class BiometricsKeyResultReceive(val result: BiometricsKeyResult) : SettingsAction() {
}
/**
* Indicates the biometrics key validation results has been received.
*/
data class BiometricsKeyResultReceive(val result: BiometricsKeyResult) : SettingsAction()
}
}

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#1B1B1F"
android:pathData="M17.366,2.08L20.811,2.206L20.812,2.206C21.106,2.218 21.384,2.346 21.586,2.562C21.788,2.777 21.897,3.063 21.889,3.357L21.798,6.76C21.784,6.924 21.708,7.078 21.585,7.187C21.461,7.296 21.3,7.354 21.134,7.347C20.968,7.341 20.811,7.271 20.694,7.152C20.577,7.034 20.51,6.875 20.507,6.71L20.563,4.589C20.559,4.57 20.55,4.552 20.537,4.537C20.524,4.523 20.508,4.511 20.49,4.504C20.471,4.497 20.452,4.495 20.433,4.497C20.413,4.5 20.395,4.507 20.379,4.518L13.545,11.325C13.422,11.437 13.261,11.497 13.094,11.493C12.927,11.489 12.768,11.42 12.649,11.302C12.531,11.184 12.462,11.025 12.457,10.857C12.452,10.69 12.512,10.529 12.624,10.406L19.411,3.653C19.511,3.454 19.257,3.44 19.257,3.44L17.329,3.37C17.164,3.355 17.01,3.277 16.899,3.153C16.789,3.028 16.73,2.867 16.734,2.701C16.739,2.535 16.808,2.378 16.925,2.263C17.042,2.147 17.201,2.082 17.366,2.08Z" />
<path
android:fillColor="#1B1B1F"
android:pathData="M12,3.25H5.125C4.089,3.25 3.25,4.089 3.25,5.125V18.875C3.25,19.91 4.089,20.75 5.125,20.75H18.875C19.91,20.75 20.75,19.91 20.75,18.875V12C20.75,11.655 20.47,11.375 20.125,11.375C19.78,11.375 19.5,11.655 19.5,12V18.875C19.5,19.22 19.22,19.5 18.875,19.5H5.125C4.78,19.5 4.5,19.22 4.5,18.875V5.125C4.5,4.78 4.78,4.5 5.125,4.5H12C12.345,4.5 12.625,4.22 12.625,3.875C12.625,3.53 12.345,3.25 12,3.25Z" />
</vector>

View File

@@ -94,4 +94,13 @@
<string name="security">Security</string>
<string name="use_biometrics_to_unlock">Use biometrics to unlock</string>
<string name="too_many_failed_biometric_attempts">Too many failed biometrics attempts.</string>
<string name="about">About</string>
<string name="version">Version</string>
<string name="continue_text">Continue</string>
<string name="bitwarden_help_center">Bitwarden Help Center</string>
<string name="continue_to_help_center">Continue to Help Center?</string>
<string name="learn_more_about_how_to_use_bitwarden_on_the_help_center">Learn more about how to use Bitwarden Authenticator on the Help Center.</string>
<string name="privacy_policy">Privacy policy</string>
<string name="continue_to_privacy_policy">Continue to privacy policy?</string>
<string name="privacy_policy_description_long">Check out our privacy policy on bitwarden.com</string>
</resources>