From a055af5280e66fdf1e2e5d79156a4093d7138f1e Mon Sep 17 00:00:00 2001
From: Altynbek Nurtaza <altynbek.nurtaza@nu.edu.kz>
Date: Mon, 21 Feb 2022 14:43:07 +0000
Subject: [PATCH] Add reveal password to registration screen

---
 .cla-accepted                                 |   1 +
 .../compose/material/OutlinedTextFieldExt.kt  | 103 ++++++++++++++++++
 .../briar/desktop/login/LoginScreen.kt        |   6 +-
 .../briar/desktop/login/RegistrationScreen.kt |  10 +-
 4 files changed, 110 insertions(+), 10 deletions(-)

diff --git a/.cla-accepted b/.cla-accepted
index 722b0d023d..1669034493 100644
--- a/.cla-accepted
+++ b/.cla-accepted
@@ -4,3 +4,4 @@
 @akwizgran (Michael Rogers)
 @paul-lorenc (Paul Lorenc)
 @ialokim (Mikolai Gütschow)
+@altynbek.nurtaza (Altynbek Nurtaza)
diff --git a/src/main/kotlin/androidx/compose/material/OutlinedTextFieldExt.kt b/src/main/kotlin/androidx/compose/material/OutlinedTextFieldExt.kt
index e5ad6c096a..d2defb118f 100644
--- a/src/main/kotlin/androidx/compose/material/OutlinedTextFieldExt.kt
+++ b/src/main/kotlin/androidx/compose/material/OutlinedTextFieldExt.kt
@@ -27,6 +27,9 @@ import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.material.InitialFocusState.AFTER_FIRST_FOCUSSED
 import androidx.compose.material.InitialFocusState.AFTER_FOCUS_LOST_ONCE
 import androidx.compose.material.InitialFocusState.FROM_START
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
@@ -43,9 +46,11 @@ import androidx.compose.ui.input.key.key
 import androidx.compose.ui.input.key.onPreviewKeyEvent
 import androidx.compose.ui.input.key.type
 import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.PasswordVisualTransformation
 import androidx.compose.ui.text.input.VisualTransformation
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 
 /**
  * Material Design outlined text field with extended support for error and helper messages,
@@ -142,3 +147,101 @@ fun OutlinedTextField(
 }
 
 enum class InitialFocusState { FROM_START, AFTER_FIRST_FOCUSSED, AFTER_FOCUS_LOST_ONCE }
+
+/**
+ * Material Design outlined text field with support for error and helper messages,
+ * as well as for handling the Enter key. The difference with [OutlinedTextField] is
+ * that it shows the visibility icons instead of error icons in the error state.
+ * All parameters not specified here are the same as on the original [OutlinedTextField].
+ *
+ * @param visualTransformation Visual transformation is only applied when password is hidden
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun OutlinedPasswordTextField(
+    value: String,
+    onValueChange: (String) -> Unit,
+    modifier: Modifier = Modifier,
+    onEnter: () -> Unit = {},
+    enabled: Boolean = true,
+    readOnly: Boolean = false,
+    textStyle: TextStyle = LocalTextStyle.current,
+    label: @Composable (() -> Unit)? = null,
+    placeholder: @Composable (() -> Unit)? = null,
+    leadingIcon: @Composable (() -> Unit)? = null,
+    helperMessage: String? = null,
+    errorMessage: String? = null,
+    isError: Boolean = false,
+    showErrorWhen: InitialFocusState = FROM_START,
+    visualTransformation: VisualTransformation = PasswordVisualTransformation(),
+    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+    singleLine: Boolean = false,
+    maxLines: Int = Int.MAX_VALUE,
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    shape: Shape = MaterialTheme.shapes.small,
+    colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
+) {
+    var isPasswordVisible by remember { mutableStateOf(false) }
+
+    OutlinedTextField(
+        value = value,
+        onValueChange = onValueChange,
+        modifier = modifier,
+        onEnter = onEnter,
+        enabled = enabled,
+        readOnly = readOnly,
+        textStyle = textStyle,
+        label = label,
+        placeholder = placeholder,
+        leadingIcon = leadingIcon,
+        trailingIcon = {
+            ShowHidePasswordIcon(
+                isVisible = isPasswordVisible,
+                toggleIsVisible = {
+                    isPasswordVisible = !isPasswordVisible
+                },
+            )
+        },
+        errorIcon = {
+            ShowHidePasswordIcon(
+                isVisible = isPasswordVisible,
+                toggleIsVisible = {
+                    isPasswordVisible = !isPasswordVisible
+                },
+            )
+        },
+        helperMessage = helperMessage,
+        errorMessage = errorMessage,
+        isError = isError,
+        showErrorWhen = showErrorWhen,
+        visualTransformation = if (!isPasswordVisible) visualTransformation else VisualTransformation.None,
+        keyboardOptions = keyboardOptions,
+        singleLine = singleLine,
+        maxLines = maxLines,
+        interactionSource = interactionSource,
+        shape = shape,
+        colors = colors,
+    )
+}
+
+@Composable
+private fun ShowHidePasswordIcon(
+    isVisible: Boolean,
+    toggleIsVisible: () -> Unit,
+) {
+    IconButton(
+        onClick = toggleIsVisible
+    ) {
+        if (isVisible) {
+            Icon(
+                imageVector = Icons.Filled.VisibilityOff,
+                contentDescription = i18n("access.password.show"),
+            )
+        } else {
+            Icon(
+                imageVector = Icons.Filled.Visibility,
+                contentDescription = i18n("access.password.hide"),
+            )
+        }
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt
index f1413a50ad..abdb0fd68a 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt
@@ -26,7 +26,7 @@ import androidx.compose.material.ButtonType.DESTRUCTIVE
 import androidx.compose.material.ButtonType.NEUTRAL
 import androidx.compose.material.DialogButton
 import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.OutlinedPasswordTextField
 import androidx.compose.material.Text
 import androidx.compose.material.TextButton
 import androidx.compose.runtime.Composable
@@ -38,7 +38,6 @@ import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.text.input.PasswordVisualTransformation
 import androidx.compose.ui.unit.dp
 import org.briarproject.briar.desktop.login.LoginSubViewModel.State.COMPACTING
 import org.briarproject.briar.desktop.login.LoginSubViewModel.State.MIGRATING
@@ -110,14 +109,13 @@ fun LoginForm(
     val initialFocusRequester = remember { FocusRequester() }
     val passwordForgotten = remember { mutableStateOf(false) }
 
-    OutlinedTextField(
+    OutlinedPasswordTextField(
         value = password,
         onValueChange = setPassword,
         label = { Text(i18n("startup.field.password")) },
         singleLine = true,
         isError = passwordInvalidError,
         errorMessage = i18n("startup.error.password_wrong"),
-        visualTransformation = PasswordVisualTransformation(),
         keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Next),
         modifier = Modifier.fillMaxWidth().focusRequester(initialFocusRequester),
         onEnter = onEnter
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationScreen.kt
index d223cb9b17..0397637deb 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationScreen.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationScreen.kt
@@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.requiredHeight
 import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.material.InitialFocusState.AFTER_FIRST_FOCUSSED
 import androidx.compose.material.InitialFocusState.AFTER_FOCUS_LOST_ONCE
+import androidx.compose.material.OutlinedPasswordTextField
 import androidx.compose.material.OutlinedTextField
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
@@ -37,7 +38,6 @@ import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.text.input.PasswordVisualTransformation
 import androidx.compose.ui.unit.dp
 import org.briarproject.briar.desktop.login.RegistrationSubViewModel.State.CREATED
 import org.briarproject.briar.desktop.login.RegistrationSubViewModel.State.CREATING
@@ -140,7 +140,7 @@ fun PasswordForm(
         if (password.isNotEmpty())
             StrengthMeter(passwordStrength, Modifier.fillMaxWidth())
     }
-    OutlinedTextField(
+    OutlinedPasswordTextField(
         value = password,
         onValueChange = setPassword,
         label = { Text(i18n("startup.field.password")) },
@@ -148,12 +148,11 @@ fun PasswordForm(
         isError = passwordTooWeakError,
         showErrorWhen = AFTER_FOCUS_LOST_ONCE,
         errorMessage = i18n("startup.error.password_too_weak"),
-        visualTransformation = PasswordVisualTransformation(),
         keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Next),
         modifier = Modifier.fillMaxWidth().focusRequester(initialFocusRequester),
-        onEnter = { focusManager.moveFocus(FocusDirection.Next) }
+        onEnter = { focusManager.moveFocus(FocusDirection.Next) },
     )
-    OutlinedTextField(
+    OutlinedPasswordTextField(
         value = passwordConfirmation,
         onValueChange = setPasswordConfirmation,
         label = { Text(i18n("startup.field.password_confirmation")) },
@@ -161,7 +160,6 @@ fun PasswordForm(
         isError = passwordsDontMatchError,
         showErrorWhen = AFTER_FIRST_FOCUSSED,
         errorMessage = i18n("startup.error.passwords_not_match"),
-        visualTransformation = PasswordVisualTransformation(),
         keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
         modifier = Modifier.fillMaxWidth(),
         onEnter = onEnter,
-- 
GitLab