diff --git a/Habitica/res/values/strings.xml b/Habitica/res/values/strings.xml
index 98ee7ac2a..6ce390d7f 100644
--- a/Habitica/res/values/strings.xml
+++ b/Habitica/res/values/strings.xml
@@ -752,6 +752,7 @@
Login names are now unique usernames that will be visible beside your display name and used for invitations, chat @mentions, and messaging.
If you’d like to learn more about this change, visit our wiki.
Usernames should conform to our Terms of Service and Community Guidelines. If you didn’t previously set a login name, your username was auto-generated.
+ You agree to our Terms of Service and have read our Privacy Policy.
Are you sure you want to confirm your current username?
Confirming your username will make it public for invitations, @mentions and messaging. You can change your username from settings at any time.
Cancel
@@ -1582,10 +1583,8 @@
Error getting credentials for authentication.
Received invalid credentials.
Unknown error during authentication.
- Start chatting!
- Remember to be friendly and follow the Community Guidelines.
-
-
+ 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.
+ Confirm new Password
- You
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/AccountPreferenceFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/AccountPreferenceFragment.kt
index ba327967d..bc5c6eb36 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/AccountPreferenceFragment.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/AccountPreferenceFragment.kt
@@ -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(R.id.old_password_edit_text)
- val passwordEditText = view?.findViewById(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(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) {
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/ChangePasswordBottomSheet.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/ChangePasswordBottomSheet.kt
new file mode 100644
index 000000000..95aa258e5
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/ChangePasswordBottomSheet.kt
@@ -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(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"
+ }
+}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/ChangePasswordScreen.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/ChangePasswordScreen.kt
new file mode 100644
index 000000000..2c8803f42
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/ChangePasswordScreen.kt
@@ -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 = {}
+ )
+ }
+}
+
+
+
+
+