mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-04-14 19:56:32 +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="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="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_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>
|
||||
|
|
@ -1582,10 +1583,8 @@
|
|||
<string name="auth_get_credentials_error">Error getting credentials for authentication.</string>
|
||||
<string name="auth_invalid_credentials">Received invalid credentials.</string>
|
||||
<string name="auth_unknown_error">Unknown error during authentication.</string>
|
||||
<string name="chat_empty_state_title">Start chatting!</string>
|
||||
<string name="chat_empty_state_description">Remember to be friendly and follow the Community Guidelines.</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="confirm_new_password">Confirm new Password</string>
|
||||
|
||||
<plurals name="you_x_others">
|
||||
<item quantity="zero">You</item>
|
||||
|
|
|
|||
|
|
@ -271,46 +271,21 @@ class AccountPreferenceFragment :
|
|||
}
|
||||
|
||||
private fun showChangePasswordDialog() {
|
||||
val inflater = context?.layoutInflater
|
||||
val view = inflater?.inflate(R.layout.dialog_edittext_change_pw, null)
|
||||
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, _ ->
|
||||
ChangePasswordBottomSheet{ oldPassword, newPassword ->
|
||||
lifecycleScope.launchCatching {
|
||||
KeyboardUtil.dismissKeyboard(activity)
|
||||
passwordEditText?.showErrorIfNecessary()
|
||||
passwordRepeatEditText?.showErrorIfNecessary()
|
||||
if (passwordEditText?.isValid != true || passwordRepeatEditText?.isValid != true) return@addButton
|
||||
lifecycleScope.launchCatching {
|
||||
val response = userRepository.updatePassword(
|
||||
oldPasswordEditText?.text ?: "",
|
||||
passwordEditText.text ?: "",
|
||||
passwordRepeatEditText.text ?: "",
|
||||
oldPassword,
|
||||
newPassword,
|
||||
newPassword,
|
||||
)
|
||||
response?.apiToken?.let {
|
||||
viewModel.saveTokens(it, user?.id ?: "")
|
||||
}
|
||||
(activity as? SnackbarActivity)?.showSnackbar(
|
||||
content = context.getString(R.string.password_changed),
|
||||
displayType = HabiticaSnackbar.SnackbarDisplayType.SUCCESS,
|
||||
)
|
||||
}
|
||||
d.dismiss()
|
||||
}
|
||||
dialog.addCancelButton()
|
||||
dialog.setAdditionalContentView(view)
|
||||
dialog.setAdditionalContentSidePadding(12)
|
||||
dialog.show()
|
||||
}
|
||||
}.show(childFragmentManager, ChangePasswordBottomSheet.TAG)
|
||||
}
|
||||
|
||||
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