mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-07-13 17:51:57 +00:00
Implement change password bottom sheet
Replaces the old change password dialog with a bottom sheet using compose.
This commit is contained in:
parent
ab8015b487
commit
028a9ecc12
4 changed files with 347 additions and 35 deletions
|
|
@ -752,6 +752,7 @@
|
||||||
<string name="usernamePromptBody">Login names are now unique usernames that will be visible beside your display name and used for invitations, chat @mentions, and messaging.</string>
|
<string name="usernamePromptBody">Login names are now unique usernames that will be visible beside your display name and used for invitations, chat @mentions, and messaging.</string>
|
||||||
<string name="usernamePromptWiki">If you’d like to learn more about this change, <a href="https://habitica.wikia.com/wiki/Player_Names">visit our wiki.</a></string>
|
<string name="usernamePromptWiki">If you’d like to learn more about this change, <a href="https://habitica.wikia.com/wiki/Player_Names">visit our wiki.</a></string>
|
||||||
<string name="usernamePromptDisclaimer">Usernames should conform to our <a href="https://habitica.com/static/terms">Terms of Service</a> and <a href="https://habitica.com/static/community-guidelines">Community Guidelines</a>. If you didn’t previously set a login name, your username was auto-generated.</string>
|
<string name="usernamePromptDisclaimer">Usernames should conform to our <a href="https://habitica.com/static/terms">Terms of Service</a> and <a href="https://habitica.com/static/community-guidelines">Community Guidelines</a>. If you didn’t previously set a login name, your username was auto-generated.</string>
|
||||||
|
<string name="register_tos_confirm">You agree to our <a href="https://habitica.com/static/terms">Terms of Service</a> and have read our <a href="https://habitica.com/static/privacy">Privacy Policy</a>.</string>
|
||||||
<string name="confirm_username_title">Are you sure you want to confirm your current username?</string>
|
<string name="confirm_username_title">Are you sure you want to confirm your current username?</string>
|
||||||
<string name="confirm_username_description">Confirming your username will make it public for invitations, @mentions and messaging. You can change your username from settings at any time.</string>
|
<string name="confirm_username_description">Confirming your username will make it public for invitations, @mentions and messaging. You can change your username from settings at any time.</string>
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
|
|
@ -1582,10 +1583,8 @@
|
||||||
<string name="auth_get_credentials_error">Error getting credentials for authentication.</string>
|
<string name="auth_get_credentials_error">Error getting credentials for authentication.</string>
|
||||||
<string name="auth_invalid_credentials">Received invalid credentials.</string>
|
<string name="auth_invalid_credentials">Received invalid credentials.</string>
|
||||||
<string name="auth_unknown_error">Unknown error during authentication.</string>
|
<string name="auth_unknown_error">Unknown error during authentication.</string>
|
||||||
<string name="chat_empty_state_title">Start chatting!</string>
|
<string name="password_change_info">Passwords must be 8 characters or more. Changing your password will log you out of any other devices and third-party tools you may use.</string>
|
||||||
<string name="chat_empty_state_description">Remember to be friendly and follow the Community Guidelines.</string>
|
<string name="confirm_new_password">Confirm new Password</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<plurals name="you_x_others">
|
<plurals name="you_x_others">
|
||||||
<item quantity="zero">You</item>
|
<item quantity="zero">You</item>
|
||||||
|
|
|
||||||
|
|
@ -271,46 +271,21 @@ class AccountPreferenceFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showChangePasswordDialog() {
|
private fun showChangePasswordDialog() {
|
||||||
val inflater = context?.layoutInflater
|
ChangePasswordBottomSheet{ oldPassword, newPassword ->
|
||||||
val view = inflater?.inflate(R.layout.dialog_edittext_change_pw, null)
|
lifecycleScope.launchCatching {
|
||||||
val oldPasswordEditText =
|
|
||||||
view?.findViewById<ValidatingEditText>(R.id.old_password_edit_text)
|
|
||||||
val passwordEditText = view?.findViewById<ValidatingEditText>(R.id.new_password_edit_text)
|
|
||||||
passwordEditText?.validator = { (it?.length ?: 0) >= 8 }
|
|
||||||
passwordEditText?.errorText = getString(R.string.password_too_short, 8)
|
|
||||||
val passwordRepeatEditText =
|
|
||||||
view?.findViewById<ValidatingEditText>(R.id.new_password_repeat_edit_text)
|
|
||||||
passwordRepeatEditText?.validator = { it == passwordEditText?.text }
|
|
||||||
passwordRepeatEditText?.errorText = getString(R.string.password_not_matching)
|
|
||||||
context?.let { context ->
|
|
||||||
val dialog = HabiticaAlertDialog(context)
|
|
||||||
dialog.setTitle(R.string.change_password)
|
|
||||||
dialog.addButton(R.string.change, true, false, false) { d, _ ->
|
|
||||||
KeyboardUtil.dismissKeyboard(activity)
|
KeyboardUtil.dismissKeyboard(activity)
|
||||||
passwordEditText?.showErrorIfNecessary()
|
|
||||||
passwordRepeatEditText?.showErrorIfNecessary()
|
|
||||||
if (passwordEditText?.isValid != true || passwordRepeatEditText?.isValid != true) return@addButton
|
|
||||||
lifecycleScope.launchCatching {
|
lifecycleScope.launchCatching {
|
||||||
val response = userRepository.updatePassword(
|
val response = userRepository.updatePassword(
|
||||||
oldPasswordEditText?.text ?: "",
|
oldPassword,
|
||||||
passwordEditText.text ?: "",
|
newPassword,
|
||||||
passwordRepeatEditText.text ?: "",
|
newPassword,
|
||||||
)
|
)
|
||||||
response?.apiToken?.let {
|
response?.apiToken?.let {
|
||||||
viewModel.saveTokens(it, user?.id ?: "")
|
viewModel.saveTokens(it, user?.id ?: "")
|
||||||
}
|
}
|
||||||
(activity as? SnackbarActivity)?.showSnackbar(
|
|
||||||
content = context.getString(R.string.password_changed),
|
|
||||||
displayType = HabiticaSnackbar.SnackbarDisplayType.SUCCESS,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
d.dismiss()
|
|
||||||
}
|
}
|
||||||
dialog.addCancelButton()
|
}.show(childFragmentManager, ChangePasswordBottomSheet.TAG)
|
||||||
dialog.setAdditionalContentView(view)
|
|
||||||
dialog.setAdditionalContentSidePadding(12)
|
|
||||||
dialog.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showAddPasswordDialog(showEmail: Boolean) {
|
private fun showAddPasswordDialog(showEmail: Boolean) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package com.habitrpg.android.habitica.ui.fragments.preferences
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.ComposeView
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import com.habitrpg.android.habitica.ui.views.ChangePasswordScreen
|
||||||
|
import com.habitrpg.common.habitica.theme.HabiticaTheme
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import com.habitrpg.android.habitica.R
|
||||||
|
|
||||||
|
class ChangePasswordBottomSheet(val onForgotPassword: () -> Unit = {}, val onPasswordChanged: (oldPassword: String, newPassword: String) -> Unit = { _, _ -> }) : BottomSheetDialogFragment() {
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
dialog?.let { dlg ->
|
||||||
|
val bottomSheet = dlg.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
|
||||||
|
bottomSheet?.layoutParams?.height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
val behavior = com.google.android.material.bottomsheet.BottomSheetBehavior.from(bottomSheet!!)
|
||||||
|
behavior.state = com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
behavior.isDraggable = false
|
||||||
|
behavior.skipCollapsed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return ComposeView(requireContext()).apply {
|
||||||
|
setContent {
|
||||||
|
HabiticaTheme {
|
||||||
|
var visible by remember { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(Unit) { visible = true }
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = fadeIn()
|
||||||
|
) {
|
||||||
|
ChangePasswordScreen(
|
||||||
|
onBack = { dismiss() },
|
||||||
|
onSave = { oldPassword, newPassword ->
|
||||||
|
onPasswordChanged(oldPassword, newPassword)
|
||||||
|
dismiss()
|
||||||
|
},
|
||||||
|
onForgotPassword = {
|
||||||
|
onForgotPassword()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "ChangePasswordFragment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,270 @@
|
||||||
|
package com.habitrpg.android.habitica.ui.views
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.habitrpg.android.habitica.R
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import com.habitrpg.android.habitica.ui.theme.colors
|
||||||
|
import com.habitrpg.common.habitica.theme.HabiticaTheme
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.colorResource
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChangePasswordScreen(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onSave: (oldPassword: String, newPassword: String) -> Unit,
|
||||||
|
onForgotPassword: () -> Unit
|
||||||
|
) {
|
||||||
|
val colors = HabiticaTheme.colors
|
||||||
|
val backgroundColor = colors.windowBackground
|
||||||
|
val fieldColor = colors.contentBackground
|
||||||
|
val labelColor = colors.textSecondary
|
||||||
|
val buttonColor = colors.tintedUiMain
|
||||||
|
val textColor = colors.textPrimary
|
||||||
|
|
||||||
|
var oldPassword by remember { mutableStateOf("") }
|
||||||
|
var newPassword by remember { mutableStateOf("") }
|
||||||
|
var confirmPassword by remember { mutableStateOf("") }
|
||||||
|
var attemptedSave by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val passwordValid = newPassword.length >= 8
|
||||||
|
val passwordsMatch = newPassword == confirmPassword && newPassword.isNotEmpty()
|
||||||
|
val canSave = passwordValid && passwordsMatch && oldPassword.isNotBlank()
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
color = backgroundColor
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 4.dp, bottom = 0.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
IconButton(onClick = onBack, modifier = Modifier.size(40.dp)) {
|
||||||
|
Icon(
|
||||||
|
painterResource(id = R.drawable.arrow_back),
|
||||||
|
contentDescription = stringResource(R.string.action_back),
|
||||||
|
tint = textColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.change_password),
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 28.sp,
|
||||||
|
color = textColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Start)
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.password_change_info),
|
||||||
|
color = labelColor,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(top = 6.dp, bottom = 22.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
PasswordField(
|
||||||
|
label = stringResource(R.string.old_password),
|
||||||
|
value = oldPassword,
|
||||||
|
onValueChange = { oldPassword = it },
|
||||||
|
fieldColor = fieldColor,
|
||||||
|
labelColor = labelColor,
|
||||||
|
textColor = textColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(14.dp))
|
||||||
|
PasswordField(
|
||||||
|
label = stringResource(R.string.new_password),
|
||||||
|
value = newPassword,
|
||||||
|
onValueChange = { newPassword = it },
|
||||||
|
fieldColor = fieldColor,
|
||||||
|
labelColor = labelColor,
|
||||||
|
textColor = textColor,
|
||||||
|
isError = attemptedSave && !passwordValid,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp)
|
||||||
|
)
|
||||||
|
if (attemptedSave && !passwordValid) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.password_too_short),
|
||||||
|
color = Color.Red,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
modifier = Modifier.padding(start = 8.dp, top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(14.dp))
|
||||||
|
PasswordField(
|
||||||
|
label = stringResource(R.string.confirm_new_password),
|
||||||
|
value = confirmPassword,
|
||||||
|
onValueChange = { confirmPassword = it },
|
||||||
|
fieldColor = fieldColor,
|
||||||
|
labelColor = labelColor,
|
||||||
|
textColor = textColor,
|
||||||
|
isError = attemptedSave && !passwordsMatch,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp)
|
||||||
|
)
|
||||||
|
if (attemptedSave && !passwordsMatch) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.password_not_matching),
|
||||||
|
color = Color.Red,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
modifier = Modifier.padding(start = 8.dp, top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
attemptedSave = true
|
||||||
|
if (canSave) onSave(oldPassword, newPassword)
|
||||||
|
},
|
||||||
|
enabled = canSave,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = buttonColor,
|
||||||
|
disabledContainerColor = buttonColor.copy(alpha = 0.3f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(52.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.change_password),
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 17.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = onForgotPassword,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.forgot_pw_btn),
|
||||||
|
color = buttonColor,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PasswordField(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
fieldColor: Color,
|
||||||
|
labelColor: Color,
|
||||||
|
textColor: Color,
|
||||||
|
isError: Boolean = false,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val dividerColor = if (value.isNotBlank()) colorResource(id = R.color.purple400_purple500) else colorResource(id = R.color.gray_400)
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
label = {
|
||||||
|
Text(label, color = labelColor, fontSize = 17.sp)
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
isError = isError,
|
||||||
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedContainerColor = fieldColor,
|
||||||
|
focusedContainerColor = fieldColor,
|
||||||
|
unfocusedBorderColor = Color.Transparent,
|
||||||
|
focusedBorderColor = Color.Transparent,
|
||||||
|
cursorColor = Color(0xFF9C8DF6),
|
||||||
|
unfocusedTextColor = textColor,
|
||||||
|
focusedTextColor = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
HorizontalDivider(
|
||||||
|
color = dividerColor,
|
||||||
|
thickness = 1.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Preview(showBackground = true, widthDp = 327, heightDp = 704, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
fun ChangePasswordScreenPreview() {
|
||||||
|
HabiticaTheme {
|
||||||
|
ChangePasswordScreen(
|
||||||
|
onBack = {},
|
||||||
|
onSave = { _, _ -> },
|
||||||
|
onForgotPassword = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in a new issue