From 025f66ca83562687efd2bd23246651aa0805c2d9 Mon Sep 17 00:00:00 2001 From: AllenTom Date: Fri, 6 Sep 2024 15:34:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=82=AE=E7=AE=B1=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E9=A1=B5=E9=9D=A2=E8=A1=A8=E5=8D=95=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增邮箱注册页面表单校验,包括: - 邮箱格式校验 - 密码格式校验(至少8位, 包含大小写字母和数字) - 确认密码校验 - 服务条款和推广信息勾选校验 新增错误提示,包括: - 邮箱格式错误 - 密码格式错误 - 确认密码不一致 - 未勾选服务条款 - 未勾选推广信息 优化 用户体验,包括: - 使用 CheckboxWithLabel 组件优化勾选框样式 - 使用字符串资源 - 调整页面布局 --- .../main/java/com/aiosman/riderpro/Const.kt | 17 +- .../aiosman/riderpro/data/AccountService.kt | 9 +- .../com/aiosman/riderpro/data/Exception.kt | 37 ++- .../riderpro/ui/composables/Checkbox.kt | 49 ++++ .../ui/composables/CheckboxWithLabel.kt | 40 +++ .../aiosman/riderpro/ui/login/emailsignup.kt | 233 ++++++++++-------- app/src/main/res/values-zh/strings.xml | 12 + app/src/main/res/values/strings.xml | 12 + 8 files changed, 303 insertions(+), 106 deletions(-) create mode 100644 app/src/main/java/com/aiosman/riderpro/ui/composables/Checkbox.kt create mode 100644 app/src/main/java/com/aiosman/riderpro/ui/composables/CheckboxWithLabel.kt diff --git a/app/src/main/java/com/aiosman/riderpro/Const.kt b/app/src/main/java/com/aiosman/riderpro/Const.kt index 76a3018..2a4cbdb 100644 --- a/app/src/main/java/com/aiosman/riderpro/Const.kt +++ b/app/src/main/java/com/aiosman/riderpro/Const.kt @@ -1,11 +1,24 @@ package com.aiosman.riderpro +import android.content.Context + object ConstVars { // api 地址 // const val BASE_SERVER = "http://192.168.31.190:8088" -// const val BASE_SERVER = "http://192.168.31.36:8088" - const val BASE_SERVER = "https://8.137.22.101:8088" + const val BASE_SERVER = "http://192.168.31.36:8088" + + // const val BASE_SERVER = "https://8.137.22.101:8088" const val MOMENT_LIKE_CHANNEL_ID = "moment_like" const val MOMENT_LIKE_CHANNEL_NAME = "Moment Like" +} + +enum class ErrorCode(val code: Int) { + USER_EXIST(10001) +} +fun Context.getErrorMessageCode(code: Int?): String { + return when (code) { + 10001 -> getString(R.string.error_10001_user_exist) + else -> getString(R.string.error_unknown) + } } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt b/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt index 78c272d..9abb524 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt @@ -365,7 +365,14 @@ class AccountServiceImpl : AccountService { } override suspend fun registerUserWithPassword(loginName: String, password: String) { - ApiClient.api.register(RegisterRequestBody(loginName, password)) + val resp = ApiClient.api.register(RegisterRequestBody(loginName, password)) + + if (!resp.isSuccessful) { + parseErrorResponse(resp.errorBody())?.let { + throw it.toServiceException() + } + throw ServiceException("Failed to register") + } } override suspend fun changeAccountPassword(oldPassword: String, newPassword: String) { diff --git a/app/src/main/java/com/aiosman/riderpro/data/Exception.kt b/app/src/main/java/com/aiosman/riderpro/data/Exception.kt index 7bdb711..300a43f 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/Exception.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/Exception.kt @@ -1,12 +1,43 @@ package com.aiosman.riderpro.data +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import okhttp3.ResponseBody + /** * 错误返回 */ class ServiceException( override val message: String, - val code: Int = 0, - val data: Any? = null + val code: Int? = 0, + val data: Any? = null, + val error: String? = null, + val name: String? = null, ) : Exception( message -) \ No newline at end of file +) + +data class ApiErrorResponse( + @SerializedName("code") + val code: Int?, + @SerializedName("error") + val error: String?, + @SerializedName("message") + val name: String?, +) { + fun toServiceException(): ServiceException { + return ServiceException( + message = error ?: name ?: "", + code = code, + error = error, + name = name + ) + } +} + +fun parseErrorResponse(errorBody: ResponseBody?): ApiErrorResponse? { + return errorBody?.let { + val gson = Gson() + gson.fromJson(it.charStream(), ApiErrorResponse::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/composables/Checkbox.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/Checkbox.kt new file mode 100644 index 0000000..a8de480 --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/Checkbox.kt @@ -0,0 +1,49 @@ +package com.aiosman.riderpro.ui.composables + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.aiosman.riderpro.ui.modifiers.noRippleClickable + + +@Composable +fun Checkbox( + size: Int = 24, + checked: Boolean = false, + onCheckedChange: (Boolean) -> Unit = {} +) { + val backgroundColor by animateColorAsState(if (checked) Color.Black else Color.Transparent) + val borderColor by animateColorAsState(if (checked) Color.Transparent else Color(0xffebebeb)) + val borderWidth by animateDpAsState(if (checked) 0.dp else 1.dp) + + Box( + modifier = Modifier + .size(size.dp) + .noRippleClickable { + onCheckedChange(!checked) + } + .background(color = backgroundColor) + .border(width = borderWidth, color = borderColor) + .padding(2.dp) + ) { + if (checked) { + Icon( + Icons.Default.Check, + contentDescription = "Checked", + tint = Color.White, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/composables/CheckboxWithLabel.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/CheckboxWithLabel.kt new file mode 100644 index 0000000..ad94523 --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/CheckboxWithLabel.kt @@ -0,0 +1,40 @@ +package com.aiosman.riderpro.ui.composables + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun CheckboxWithLabel( + checked: Boolean = false, + checkSize: Int = 16, + label: String = "", + fontSize: Int = 12, + error: Boolean = false, + onCheckedChange: (Boolean) -> Unit, +) { + Row( + ) { + Checkbox( + checked = checked, + onCheckedChange = { + onCheckedChange(it) + }, + size = checkSize + ) + Text( + text = label, + modifier = Modifier.padding(start = 8.dp), + fontSize = fontSize.sp, + style = TextStyle( + color = if (error) Color.Red else Color.Black + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/login/emailsignup.kt b/app/src/main/java/com/aiosman/riderpro/ui/login/emailsignup.kt index 497d869..ac603af 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/login/emailsignup.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/login/emailsignup.kt @@ -1,18 +1,22 @@ package com.aiosman.riderpro.ui.login import android.widget.Toast +import androidx.compose.foundation.background 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -20,30 +24,37 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.aiosman.riderpro.AppStore +import com.aiosman.riderpro.ErrorCode import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.Messaging import com.aiosman.riderpro.R import com.aiosman.riderpro.data.AccountService import com.aiosman.riderpro.data.ServiceException import com.aiosman.riderpro.data.AccountServiceImpl +import com.aiosman.riderpro.getErrorMessageCode import com.aiosman.riderpro.ui.NavigationRoute import com.aiosman.riderpro.ui.comment.NoticeScreenHeader import com.aiosman.riderpro.ui.composables.ActionButton +import com.aiosman.riderpro.ui.composables.Checkbox +import com.aiosman.riderpro.ui.composables.CheckboxWithLabel import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout +import com.aiosman.riderpro.ui.composables.StatusBarSpacer import com.aiosman.riderpro.ui.composables.TextInputField import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable fun EmailSignupScreen() { - var email by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } - var confirmPassword by remember { mutableStateOf("") } + var email by remember { mutableStateOf("takayamaaren@gmail.com") } + var password by remember { mutableStateOf("Dzh17217.") } + var confirmPassword by remember { mutableStateOf("Dzh17217.") } var rememberMe by remember { mutableStateOf(false) } var acceptTerms by remember { mutableStateOf(false) } var acceptPromotions by remember { mutableStateOf(false) } @@ -51,32 +62,62 @@ fun EmailSignupScreen() { val navController = LocalNavController.current val context = LocalContext.current val accountService: AccountService = AccountServiceImpl() + var emailError by remember { mutableStateOf(null) } + var passwordError by remember { mutableStateOf(null) } + var confirmPasswordError by remember { mutableStateOf(null) } + var termsError by remember { mutableStateOf(false) } + var promotionsError by remember { mutableStateOf(false) } + fun validateForm(): Boolean { - if (email.isEmpty()) { - Toast.makeText(context, "Email is required", Toast.LENGTH_SHORT).show() - return false + emailError = when { + // 非空 + email.isEmpty() -> context.getString(R.string.text_error_email_required) + // 邮箱格式 + !android.util.Patterns.EMAIL_ADDRESS.matcher(email) + .matches() -> context.getString(R.string.text_error_email_format) + + else -> null } - if (password.isEmpty()) { - Toast.makeText(context, "Password is required", Toast.LENGTH_SHORT).show() - return false + passwordError = when { + // 非空 + password.isEmpty() -> context.getString(R.string.text_error_password_required) + // 包含大写字母 + !password.matches(Regex(".*[A-Z].*")) -> context.getString(R.string.text_error_password_format) + // 至少8位 + password.length < 8 -> context.getString(R.string.text_error_password_format) + // 至少一个数字 + !password.matches(Regex(".*\\d.*")) -> context.getString(R.string.text_error_password_format) + // 包含小写字母 + !password.matches(Regex(".*[a-z].*")) -> context.getString(R.string.text_error_password_format) + else -> null } - if (confirmPassword.isEmpty()) { - Toast.makeText(context, "Confirm password is required", Toast.LENGTH_SHORT).show() - return false - } - if (password != confirmPassword) { - Toast.makeText(context, "Password does not match", Toast.LENGTH_SHORT).show() - return false + confirmPasswordError = when { + // 非空 + confirmPassword.isEmpty() -> context.getString(R.string.text_error_confirm_password_required) + // 与密码一致 + confirmPassword != password -> context.getString(R.string.text_error_confirm_password_mismatch) + else -> null } if (!acceptTerms) { - Toast.makeText(context, "You must accept terms", Toast.LENGTH_SHORT).show() + scope.launch(Dispatchers.Main) { + Toast.makeText(context, context.getString(R.string.error_not_accept_term), Toast.LENGTH_SHORT).show() + } + termsError = true return false + }else{ + termsError = false } if (!acceptPromotions) { - Toast.makeText(context, "You must accept promotions", Toast.LENGTH_SHORT).show() + scope.launch(Dispatchers.Main) { + Toast.makeText(context, context.getString(R.string.error_not_accept_recive_notice), Toast.LENGTH_SHORT).show() + } + promotionsError = true return false + }else{ + promotionsError = false } - return true + + return emailError == null && passwordError == null && confirmPasswordError == null } suspend fun registerUser() { @@ -86,8 +127,18 @@ fun EmailSignupScreen() { accountService.registerUserWithPassword(email, password) } catch (e: ServiceException) { scope.launch(Dispatchers.Main) { - Toast.makeText(context, "Failed to register", Toast.LENGTH_SHORT).show() + if (e.code == ErrorCode.USER_EXIST.code) { + emailError = context.getString(R.string.error_10001_user_exist) + return@launch + } + Toast.makeText(context, context.getErrorMessageCode(e.code), Toast.LENGTH_SHORT).show() } + return + } catch (e: Exception) { + scope.launch(Dispatchers.Main) { + Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() + } + return } // 获取 token val authResp = accountService.loginUserWithPassword(email, password) @@ -115,118 +166,98 @@ fun EmailSignupScreen() { popUpTo(NavigationRoute.Login.route) { inclusive = true } } } - - - } - StatusBarMaskLayout { - Column( - horizontalAlignment = Alignment.CenterHorizontally, + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxSize() + .background(Color.White) + ) { + StatusBarSpacer() + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, start = 16.dp, end = 16.dp) + ) { + NoticeScreenHeader(stringResource(R.string.sign_in_upper), moreIcon = false) + } + Spacer(modifier = Modifier.padding(32.dp)) + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 24.dp) ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, end = 16.dp) - ) { - NoticeScreenHeader("SIGNUP", moreIcon = false) - } - - Spacer(modifier = Modifier.padding(68.dp)) TextInputField( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), + .fillMaxWidth(), text = email, onValueChange = { email = it }, - label = "What's your email?", - hint = "Enter your email" + label = stringResource(R.string.login_email_label), + hint = stringResource(R.string.text_hint_email), + error = emailError ) - Spacer(modifier = Modifier.padding(16.dp)) + Spacer(modifier = Modifier.padding(8.dp)) TextInputField( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), + .fillMaxWidth(), text = password, onValueChange = { password = it }, password = true, - label = "What's your password?", - hint = "Enter your password" + label = stringResource(R.string.login_password_label), + hint = stringResource(R.string.text_hint_password), + error = passwordError ) - Spacer(modifier = Modifier.padding(16.dp)) + Spacer(modifier = Modifier.padding(8.dp)) TextInputField( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), + .fillMaxWidth(), text = confirmPassword, onValueChange = { confirmPassword = it }, password = true, - label = "Confirm password", - hint = "Enter your password" + label = stringResource(R.string.login_confirm_password_label), + hint = stringResource(R.string.text_hint_confirm_password), + error = confirmPasswordError ) - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(16.dp)) Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start, ) { - Row( - verticalAlignment = Alignment.CenterVertically + CheckboxWithLabel( + checked = rememberMe, + checkSize = 16, + fontSize = 12, + label = stringResource(R.string.remember_me), ) { - Checkbox( - checked = rememberMe, - onCheckedChange = { - rememberMe = it - }, - modifier = Modifier.padding(start = 16.dp), - colors = CheckboxDefaults.colors( - checkedColor = Color.Black - ), - ) - Text("Remember me", modifier = Modifier.padding(start = 4.dp)) + rememberMe = it } - Row( - verticalAlignment = Alignment.CenterVertically + Spacer(modifier = Modifier.height(16.dp)) + CheckboxWithLabel( + checked = acceptTerms, + checkSize = 16, + fontSize = 12, + label = stringResource(R.string.agree_terms_of_service), + error = termsError ) { - Checkbox( - checked = acceptTerms, - onCheckedChange = { - acceptTerms = it - }, - modifier = Modifier.padding(start = 16.dp), - colors = CheckboxDefaults.colors( - checkedColor = Color.Black - ), - ) - Text( - "Yes, I have read and agree to RiderPro’s Terms of Service.", - modifier = Modifier.padding(start = 4.dp), - fontSize = 12.sp - ) + acceptTerms = it } - Row( - verticalAlignment = Alignment.CenterVertically + Spacer(modifier = Modifier.height(16.dp)) + CheckboxWithLabel( + checked = acceptPromotions, + checkSize = 16, + fontSize = 12, + label = stringResource(R.string.agree_promotion), + error = promotionsError ) { - Checkbox( - checked = acceptPromotions, - onCheckedChange = { - acceptPromotions = it - }, - modifier = Modifier.padding(start = 16.dp), - colors = CheckboxDefaults.colors( - checkedColor = Color.Black - ), - ) - Text( - "Yes, Send me news and promotional content from RiderPro.", - modifier = Modifier.padding(start = 4.dp), - fontSize = 12.sp - ) + acceptPromotions = it } } @@ -235,7 +266,7 @@ fun EmailSignupScreen() { modifier = Modifier .width(345.dp) .height(48.dp), - text = "LET'S RIDE".uppercase(), + text = stringResource(R.string.lets_ride_upper), backgroundImage = R.mipmap.rider_pro_signup_red_bg ) { scope.launch(Dispatchers.IO) { @@ -243,5 +274,7 @@ fun EmailSignupScreen() { } } } + } + } \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 0f2bdc3..77876d2 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -33,4 +33,16 @@ 使用邮箱注册 使用 Google 账号登录 返回 + 再次输入密码 + 再次输入密码 + 我已阅读用户协议 + 我同意 Rider Pro 推送消息 + 邮箱格式错误 + 密码至少为 8 位,包含大写字母、小写字母、数字 + 密码和确认密码必须相同 + 请输入确认密码 + 用户已存在 + 服务端未知错误 + 为了为您提供更个性化的服务,请允许我们向您推送相关信息。 + "为了提供更好的服务,请您在注册前仔细阅读并同意《用户协议》。 " \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 42f69da..9ccd14a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,4 +32,16 @@ CONTINUE WITH EMAIL CONTINUE WITH GOOGLE BACK + Enter your password again + Confirm password + Yes, I have read and agree to RiderPro’s Terms of Service. + Yes, I would like to receive promotions and updates from RiderPro. + Invalid email + The password must be at least 8 characters long and contain a combination of uppercase letters, lowercase letters, and numbers. + Please ensure that the passwords entered twice are consistent. + Confirm password is required + User existed + Unknown error + To provide you with a more personalized experience, please allow us to send you relevant notifications. + To provide you with the best service, please read and agree to our User Agreement before registering. \ No newline at end of file