From 0d8255e763347b3e36539171a5a3f8eec58554f4 Mon Sep 17 00:00:00 2001 From: Pratyush Singh Date: Fri, 29 Dec 2023 00:08:16 +0530 Subject: [PATCH] refactor: registration screen to compose --- app/build.gradle | 3 + .../ui/fragments/RegistrationFragment.kt | 361 +++++++++--------- .../ui/registration/RegistrationScreen.kt | 266 +++++++++++++ .../main/res/layout/activity_registration.xml | 3 +- 4 files changed, 446 insertions(+), 187 deletions(-) create mode 100644 app/src/main/java/org/mifos/mobile/ui/registration/RegistrationScreen.kt diff --git a/app/build.gradle b/app/build.gradle index e276955ac0..e85cdf9da4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -97,6 +97,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'com.googlecode.libphonenumber:libphonenumber:7.0.4' implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.lifecycleExtensionsVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.lifecycleVersion" kapt "com.github.Raizlabs.DBFlow:dbflow-processor:$rootProject.dbflowVersion" @@ -116,6 +117,8 @@ dependencies { //Country Code picker implementation "com.hbb20:ccp:$rootProject.countryCodePicker" + implementation 'com.github.ParveshSandila:CountryCodeChooser:1.0' + implementation("com.github.jump-sdk:jetpack_compose_country_code_picker_emoji:2.2.6") //Square dependencies implementation("com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion") { diff --git a/app/src/main/java/org/mifos/mobile/ui/fragments/RegistrationFragment.kt b/app/src/main/java/org/mifos/mobile/ui/fragments/RegistrationFragment.kt index 5e700f1d5b..c40758cb35 100644 --- a/app/src/main/java/org/mifos/mobile/ui/fragments/RegistrationFragment.kt +++ b/app/src/main/java/org/mifos/mobile/ui/fragments/RegistrationFragment.kt @@ -1,25 +1,26 @@ package org.mifos.mobile.ui.fragments -import android.graphics.PorterDuff import android.os.Bundle -import android.text.Editable -import android.text.TextWatcher +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.RadioButton -import android.widget.TextView +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import com.hbb20.CountryCodePicker +import com.google.i18n.phonenumbers.NumberParseException +import com.google.i18n.phonenumbers.PhoneNumberUtil +import com.google.i18n.phonenumbers.Phonenumber import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import org.mifos.mobile.R -import org.mifos.mobile.databinding.FragmentRegistrationBinding +import org.mifos.mobile.core.ui.theme.MifosMobileTheme import org.mifos.mobile.ui.activities.base.BaseActivity import org.mifos.mobile.ui.fragments.base.BaseFragment +import org.mifos.mobile.ui.registration.RegistrationScreen import org.mifos.mobile.utils.Network import org.mifos.mobile.utils.PasswordStrength import org.mifos.mobile.utils.RegistrationUiState @@ -31,61 +32,56 @@ import org.mifos.mobile.viewModels.RegistrationViewModel */ @AndroidEntryPoint class RegistrationFragment : BaseFragment() { - private var _binding: FragmentRegistrationBinding? = null - private val binding get() = _binding!! + private lateinit var viewModel: RegistrationViewModel + private lateinit var accountNumberContent: String + private lateinit var usernameContent: String + private lateinit var firstNameContent: String + private lateinit var lastNameContent: String + private lateinit var phoneNumberContent: String + private lateinit var emailContent: String + private lateinit var passwordContent: String + private lateinit var authenticationModeContent: String + private lateinit var countryCodeContent: String + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { - _binding = FragmentRegistrationBinding.inflate(inflater, container, false) - val rootView = binding.root viewModel = ViewModelProvider(this)[RegistrationViewModel::class.java] - with(binding) { - etPassword.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged( - charSequence: CharSequence, i: Int, i1: Int, i2: Int - ) { - } - - override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) { - if (charSequence.isEmpty()) { - progressBar.visibility = View.GONE - passwordStrength.visibility = View.GONE - } else { - progressBar.visibility = View.VISIBLE - passwordStrength.visibility = View.VISIBLE - updatePasswordStrengthView(charSequence.toString()) - } + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + MifosMobileTheme { + RegistrationScreen( + register = { account, username, firstname, lastname, phoneNumber, email, password, authenticationMode, countryCode -> + accountNumberContent = account + usernameContent = username + firstNameContent = firstname + lastNameContent = lastname + phoneNumberContent = phoneNumber + emailContent = email + passwordContent = password + authenticationModeContent = authenticationMode + countryCodeContent = countryCode + registerClicked() + }, + progress = { updatePasswordStrengthView(it) } + ) } - - override fun afterTextChanged(editable: Editable) {} - }) + } } - return rootView } - private fun updatePasswordStrengthView(password: String) { - with(binding) { - if (TextView.VISIBLE != passwordStrength.visibility) return - if (password.isEmpty()) { - passwordStrength.text = "" - progressBar.progress = 0 - return - } - val str = PasswordStrength.calculateStrength(password) - passwordStrength.text = str.getText(context) - passwordStrength.setTextColor(str.color) - val mode = PorterDuff.Mode.SRC_IN - progressBar.progressDrawable?.setColorFilter(str.color, mode) - when (str.getText(context)) { - getString(R.string.password_strength_weak) -> progressBar.progress = 25 - getString(R.string.password_strength_medium) -> progressBar.progress = 50 - getString(R.string.password_strength_strong) -> progressBar.progress = 75 - else -> progressBar.progress = 100 - } + private fun updatePasswordStrengthView(password: String): Float { + val str = PasswordStrength.calculateStrength(password) + return when (str.getText(context)) { + getString(R.string.password_strength_weak) -> 0.25f + getString(R.string.password_strength_medium) -> 0.5f + getString(R.string.password_strength_strong) -> 0.75f + else -> 1f } } @@ -114,155 +110,136 @@ class RegistrationFragment : BaseFragment() { } } - binding.btnRegister.setOnClickListener { - registerClicked() - } } private fun registerClicked() { if (areFieldsValidated()) { - with(binding) { - val radioButton = rgVerificationMode.checkedRadioButtonId.let { - root.findViewById(it) - } - val accountNumber = etAccountNumber.text.toString() - val authenticationMode = radioButton?.text.toString() - val email = etEmail.text.toString() - val firstName = etFirstName.text.toString() - val lastName = etLastName.text.toString() - val mobileNumber = - countryCodePicker.selectedCountryCode.toString() + etPhoneNumber.text.toString() - if (etPassword.text.toString() != etConfirmPassword.text.toString()) { - Toaster.show(root, getString(R.string.error_password_not_match)) - return - } - val password = etPassword.text.toString() - val username = etUsername.text.toString().replace(" ", "") - - if (Network.isConnected(context)) { - viewModel.registerUser( - accountNumber, - authenticationMode, - email, - firstName, - lastName, - mobileNumber, - password, - username - ) - } else { - Toaster.show(root, getString(R.string.no_internet_connection)) - } + if (Network.isConnected(context)) { + viewModel.registerUser( + accountNumberContent, + authenticationModeContent, + emailContent, + firstNameContent, + lastNameContent, + phoneNumberContent, + passwordContent, + usernameContent + ) + } else { + Toaster.show(view, getString(R.string.no_internet_connection)) } + } } private fun areFieldsValidated(): Boolean { - val rootView = binding.root - with(binding) { - return when { - viewModel.isInputFieldBlank(etAccountNumber.text.toString()) -> { - Toaster.show( - rootView, - getString( - R.string.error_validation_blank, getString(R.string.account_number) - ), - ) - false - } - - viewModel.isInputFieldBlank(etUsername.text.toString()) -> { - Toaster.show( - rootView, - getString(R.string.error_validation_blank, getString(R.string.username)), - ) - false - } + return when { + viewModel.isInputFieldBlank(accountNumberContent) -> { + Toaster.show( + view, + getString( + R.string.error_validation_blank, getString(R.string.account_number) + ), + ) + false + } - viewModel.isInputLengthInadequate(etUsername.text.toString()) -> { - Toaster.show(rootView, getString(R.string.error_username_greater_than_six)) - false - } + viewModel.isInputFieldBlank(usernameContent) -> { + Toaster.show( + view, + getString(R.string.error_validation_blank, getString(R.string.username)), + ) + false + } - viewModel.inputHasSpaces(etUsername.text.toString()) -> { - Toaster.show( - rootView, - getString( - R.string.error_validation_cannot_contain_spaces, - getString(R.string.username), - getString(R.string.not_contain_username), - ), - ) - false - } + viewModel.isInputLengthInadequate(usernameContent) -> { + Toaster.show(view, getString(R.string.error_username_greater_than_six)) + false + } - viewModel.isInputFieldBlank(etFirstName.text.toString()) -> { - Toaster.show( - rootView, - getString(R.string.error_validation_blank, getString(R.string.first_name)), - ) - false - } + viewModel.inputHasSpaces(usernameContent) -> { + Toaster.show( + view, + getString( + R.string.error_validation_cannot_contain_spaces, + getString(R.string.username), + getString(R.string.not_contain_username), + ), + ) + false + } - viewModel.isInputFieldBlank(etLastName.text.toString()) -> { - Toaster.show( - rootView, - getString(R.string.error_validation_blank, getString(R.string.last_name)), - ) - false - } + viewModel.isInputFieldBlank(firstNameContent) -> { + Toaster.show( + view, + getString(R.string.error_validation_blank, getString(R.string.first_name)), + ) + false + } - viewModel.isInputFieldBlank(etEmail.text.toString()) -> { - Toaster.show( - rootView, - getString(R.string.error_validation_blank, getString(R.string.email)), - ) - false - } + viewModel.isInputFieldBlank(lastNameContent) -> { + Toaster.show( + view, + getString(R.string.error_validation_blank, getString(R.string.last_name)), + ) + false + } - viewModel.isInputFieldBlank(etPassword.text.toString()) -> { - Toaster.show( - rootView, - getString(R.string.error_validation_blank, getString(R.string.password)), - ) - false - } + viewModel.isInputFieldBlank(emailContent) -> { + Toaster.show( + view, + getString(R.string.error_validation_blank, getString(R.string.email)), + ) + false + } - viewModel.hasLeadingTrailingSpaces(etPassword.text.toString()) -> { - Toaster.show( - rootView, - getString( - R.string.error_validation_cannot_contain_leading_or_trailing_spaces, - getString(R.string.password), - ), - ) - false - } + viewModel.isInputFieldBlank(passwordContent) -> { + Toaster.show( + view, + getString(R.string.error_validation_blank, getString(R.string.password)), + ) + false + } - viewModel.isEmailInvalid(etEmail.text.toString()) -> { - Toaster.show(rootView, getString(R.string.error_invalid_email)) - false - } + viewModel.hasLeadingTrailingSpaces(passwordContent) -> { + Toaster.show( + view, + getString( + R.string.error_validation_cannot_contain_leading_or_trailing_spaces, + getString(R.string.password), + ), + ) + false + } - viewModel.isInputLengthInadequate(etPassword.text.toString()) -> { - Toaster.show( - rootView, - getString( - R.string.error_validation_minimum_chars, - getString(R.string.password), - resources.getInteger(R.integer.password_minimum_length), - ), - ) - return false - } + viewModel.isEmailInvalid(emailContent) -> { + Toaster.show(view, getString(R.string.error_invalid_email)) + false + } - (!isPhoneNumberValid(countryCodePicker)) -> { - Toaster.show(rootView, getString(R.string.invalid_phn_number)) - return false - } + viewModel.isInputLengthInadequate(passwordContent) -> { + Toaster.show( + view, + getString( + R.string.error_validation_minimum_chars, + getString(R.string.password), + resources.getInteger(R.integer.password_minimum_length), + ), + ) + return false + } - else -> true + (!isPhoneNumberValid( + phoneNumberContent, + getIsoCountryCodeFromCountryCode(countryCodeContent) + )) -> { + Log.d("COUNTRY_CODE",getIsoCountryCodeFromCountryCode(countryCodeContent).toString()) + Toaster.show(view, getString(R.string.invalid_phn_number)) + return false } + + else -> true + } } @@ -275,7 +252,7 @@ class RegistrationFragment : BaseFragment() { } fun showError(msg: String?) { - Toaster.show(binding.root, msg) + Toaster.show(view, msg) } fun showProgress() { @@ -286,14 +263,28 @@ class RegistrationFragment : BaseFragment() { hideMifosProgressDialog() } - override fun onDestroyView() { - super.onDestroyView() - _binding = null + private fun getIsoCountryCodeFromCountryCode(countryCode: String): String? { + val phoneNumberUtil = PhoneNumberUtil.getInstance() + return try { + val phoneNumber = phoneNumberUtil.parse("+$countryCode$phoneNumberContent", "") + Log.d("PHONE_NUM",phoneNumber.toString()) + val regionCode = phoneNumberUtil.getRegionCodeForNumber(phoneNumber) + regionCode + } catch (e: Exception) { + e.printStackTrace() + null + } } - private fun isPhoneNumberValid(ccp: CountryCodePicker): Boolean { - binding.countryCodePicker.registerCarrierNumberEditText(binding.etPhoneNumber) - return ccp.isValidFullNumber + private fun isPhoneNumberValid(phoneNumber: String?, countryCode: String?): Boolean { + val phoneUtil: PhoneNumberUtil = PhoneNumberUtil.getInstance() + try { + val numberProto: Phonenumber.PhoneNumber = phoneUtil.parse(phoneNumber, countryCode) + return phoneUtil.isValidNumber(numberProto) + } catch (e: NumberParseException) { + System.err.println("NumberParseException was thrown: $e") + } + return false } companion object { diff --git a/app/src/main/java/org/mifos/mobile/ui/registration/RegistrationScreen.kt b/app/src/main/java/org/mifos/mobile/ui/registration/RegistrationScreen.kt new file mode 100644 index 0000000000..f90aca75cd --- /dev/null +++ b/app/src/main/java/org/mifos/mobile/ui/registration/RegistrationScreen.kt @@ -0,0 +1,266 @@ +package org.mifos.mobile.ui.registration + +import android.content.res.Configuration +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.owlbuddy.www.countrycodechooser.CountryCodeChooser +import com.owlbuddy.www.countrycodechooser.utils.enums.CountryCodeType +import org.mifos.mobile.R +import org.mifos.mobile.core.ui.component.MifosOutlinedTextField + +@Composable +fun RegistrationScreen( + register: (accountNumber: String, username: String, firstName: String, lastName: String, phoneNumber: String, email: String, password: String, authMode: String, countryCode: String) -> Unit, + progress: (String) -> Float, +) { + + val keyboardController = LocalSoftwareKeyboardController.current + + var accountNumber by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) + } + var username by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) + } + var firstName by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) + } + var lastName by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) + } + var phoneNumber by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) + } + var email by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) + } + var password by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) + } + var onValueChangePassword by rememberSaveable { + mutableStateOf(false) + } + var confirmPassword by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) + } + var countryCode by rememberSaveable { + mutableStateOf("") + } + val radioOptions = + listOf(stringResource(id = R.string.rb_email), stringResource(id = R.string.rb_mobile)) + var authenticationMode by remember { mutableStateOf(radioOptions[0]) } + + val progressIndicator = progress(password.text) + var passwordVisibility: Boolean by remember { mutableStateOf(false) } + var confirmPasswordVisibility: Boolean by remember { mutableStateOf(false) } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 12.dp) + .pointerInput(Unit) { + detectTapGestures(onTap = { + keyboardController?.hide() + }) + }) { + MifosOutlinedTextField( + value = accountNumber, + onValueChange = { accountNumber = it }, + label = R.string.account_number, + supportingText = "" + ) + MifosOutlinedTextField( + value = username, + onValueChange = { username = it }, + label = R.string.username, + supportingText = "" + ) + MifosOutlinedTextField( + value = firstName, + onValueChange = { firstName = it }, + label = R.string.first_name, + supportingText = "" + ) + MifosOutlinedTextField( + value = lastName, + onValueChange = { lastName = it }, + label = R.string.last_name, + supportingText = "" + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + CountryCodeChooser( + modifier = Modifier + .padding(start = 16.dp) + .border( + width = 1.dp, + shape = RoundedCornerShape(5.dp), + color = Color.Gray + ) + .padding(10.dp), + defaultCountryCode = "91", + countryCodeType = CountryCodeType.FLAG, + onCountyCodeSelected = { code, codeWithPrefix -> + countryCode = code + } + ) + MifosOutlinedTextField( + value = phoneNumber, + onValueChange = { phoneNumber = it }, + label = R.string.phone_number, + supportingText = "" + ) + } + MifosOutlinedTextField( + value = email, + onValueChange = { email = it }, + label = R.string.email, + supportingText = "" + ) + MifosOutlinedTextField( + value = password, + onValueChange = { + password = it + onValueChangePassword = true + }, + label = R.string.password, + supportingText = "", + visualTransformation = if (passwordVisibility) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + val image = if (passwordVisibility) + Icons.Filled.Visibility + else Icons.Filled.VisibilityOff + IconButton(onClick = { passwordVisibility = !passwordVisibility }) { + Icon(imageVector = image, null) + } + } + ) + + if (onValueChangePassword) { + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp), + color = when (progressIndicator) { + 0.25f -> Color.Red + 0.5f -> Color(alpha = 255, red = 220, green = 185, blue = 0) + 0.75f -> Color.Green + else -> Color.Blue + }, + progress = progressIndicator, + trackColor = Color.White + ) + } + + MifosOutlinedTextField( + value = confirmPassword, + onValueChange = { confirmPassword = it }, + label = R.string.confirm_password, + supportingText = "", + visualTransformation = if (confirmPasswordVisibility) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + val image = if (confirmPasswordVisibility) + Icons.Filled.Visibility + else Icons.Filled.VisibilityOff + IconButton(onClick = { confirmPasswordVisibility = !confirmPasswordVisibility }) { + Icon(imageVector = image, null) + } + } + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = R.string.verification_mode), + modifier = Modifier.padding(end = 8.dp), + color = if (isSystemInDarkTheme()) Color.White else Color.Black + ) + radioOptions.forEach { authMode -> + RadioButton( + selected = (authMode == authenticationMode), + onClick = { authenticationMode = authMode } + ) + Text( + text = authMode, + color = if (isSystemInDarkTheme()) Color.White else Color.Black + ) + } + } + + Button( + onClick = { + register.invoke( + accountNumber.text, + username.text, + firstName.text, + lastName.text, + phoneNumber.text, + email.text, + password.text, + authenticationMode, + countryCode + ) + keyboardController?.hide() + }, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 4.dp), + contentPadding = PaddingValues(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = if (isSystemInDarkTheme()) Color( + 0xFF9bb1e3 + ) else Color(0xFF325ca8) + ) + ) { + Text(text = stringResource(id = R.string.register)) + } + } +} + +@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun RegistrationScreenPreview() { + RegistrationScreen({ _, _, _, _, _, _, _, _, _ -> }, { 0f }) +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_registration.xml b/app/src/main/res/layout/activity_registration.xml index 6950fd158b..3183031d20 100644 --- a/app/src/main/res/layout/activity_registration.xml +++ b/app/src/main/res/layout/activity_registration.xml @@ -2,8 +2,7 @@ + android:layout_height="match_parent">