forgot password and registration pages... cant fully test because fenaka sms is not working
This commit is contained in:
@@ -35,6 +35,26 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.GridFlow.NoActionBar" />
|
android:theme="@style/Theme.GridFlow.NoActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name=".ForgotPasswordActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="Reset Password"
|
||||||
|
android:theme="@style/Theme.GridFlow.NoActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name=".RegisterActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="Create Account"
|
||||||
|
android:theme="@style/Theme.GridFlow.NoActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name=".OtpVerificationActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="Verify OTP"
|
||||||
|
android:theme="@style/Theme.GridFlow.NoActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name=".PasswordResetOtpActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="Reset Password OTP"
|
||||||
|
android:theme="@style/Theme.GridFlow.NoActionBar" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
141
app/src/main/java/sh/sar/gridflow/ForgotPasswordActivity.kt
Normal file
141
app/src/main/java/sh/sar/gridflow/ForgotPasswordActivity.kt
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package sh.sar.gridflow
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import sh.sar.gridflow.databinding.ActivityForgotPasswordBinding
|
||||||
|
import sh.sar.gridflow.network.ApiResult
|
||||||
|
import sh.sar.gridflow.network.FenakaApiService
|
||||||
|
|
||||||
|
class ForgotPasswordActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityForgotPasswordBinding
|
||||||
|
private lateinit var apiService: FenakaApiService
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "ForgotPasswordActivity"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Force system theme (follows device dark mode setting)
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||||
|
|
||||||
|
Log.d(TAG, "ForgotPasswordActivity onCreate")
|
||||||
|
|
||||||
|
binding = ActivityForgotPasswordBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
apiService = FenakaApiService()
|
||||||
|
|
||||||
|
setupClickListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupClickListeners() {
|
||||||
|
binding.btnContinue.setOnClickListener {
|
||||||
|
handleContinue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showMobileNotFoundDialog(mobile: String) {
|
||||||
|
val dialog = androidx.appcompat.app.AlertDialog.Builder(this)
|
||||||
|
.setTitle("Mobile Number Not Registered")
|
||||||
|
.setMessage("The mobile number $mobile is not registered with any account. Would you like to create a new account instead?")
|
||||||
|
.setPositiveButton("Register") { _, _ ->
|
||||||
|
// Navigate to registration screen
|
||||||
|
val intent = Intent(this, RegisterActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
.setNegativeButton("Cancel") { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.setCancelable(true)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleContinue() {
|
||||||
|
val mobileNumber = binding.etMobileNumber.text.toString().trim()
|
||||||
|
|
||||||
|
Log.d(TAG, "handleContinue called with mobile: $mobileNumber")
|
||||||
|
|
||||||
|
if (validateInput(mobileNumber)) {
|
||||||
|
Log.d(TAG, "Input validation passed, sending reset request")
|
||||||
|
performPasswordReset(mobileNumber)
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Input validation failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performPasswordReset(mobile: String) {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
when (val result = apiService.forgotPassword(mobile)) {
|
||||||
|
is ApiResult.Success -> {
|
||||||
|
Log.d(TAG, "Forgot password successful: ${result.data}")
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
// Navigate to OTP verification for password reset
|
||||||
|
val intent = Intent(this@ForgotPasswordActivity, PasswordResetOtpActivity::class.java)
|
||||||
|
intent.putExtra(PasswordResetOtpActivity.EXTRA_MOBILE_NUMBER, mobile)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
is ApiResult.Error -> {
|
||||||
|
Log.d(TAG, "Forgot password failed: ${result.message} (code: ${result.code})")
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
if (result.message == "MOBILE_NOT_FOUND") {
|
||||||
|
showMobileNotFoundDialog(mobile)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
this@ForgotPasswordActivity,
|
||||||
|
result.message,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Exception in password reset", e)
|
||||||
|
setLoading(false)
|
||||||
|
Toast.makeText(
|
||||||
|
this@ForgotPasswordActivity,
|
||||||
|
"Failed to send reset instructions: ${e.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateInput(mobileNumber: String): Boolean {
|
||||||
|
if (mobileNumber.isEmpty()) {
|
||||||
|
binding.etMobileNumber.error = "Mobile number is required"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic Maldives mobile number validation (7xxxxxx format)
|
||||||
|
if (!mobileNumber.matches(Regex("^[79]\\d{6}$"))) {
|
||||||
|
binding.etMobileNumber.error = "Enter a valid Maldives mobile number"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLoading(isLoading: Boolean) {
|
||||||
|
binding.btnContinue.isEnabled = !isLoading
|
||||||
|
binding.btnContinue.text = if (isLoading) "Sending..." else "Continue"
|
||||||
|
binding.etMobileNumber.isEnabled = !isLoading
|
||||||
|
}
|
||||||
|
}
|
@@ -79,6 +79,23 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showLoginError(message: String) {
|
||||||
|
val dialog = androidx.appcompat.app.AlertDialog.Builder(this)
|
||||||
|
.setTitle("Login Failed")
|
||||||
|
.setMessage(message)
|
||||||
|
.setPositiveButton("Retry") { _, _ ->
|
||||||
|
// Retry the login with the same credentials
|
||||||
|
handleSignIn()
|
||||||
|
}
|
||||||
|
.setNegativeButton("Cancel") { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.setCancelable(true)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleSignIn() {
|
private fun handleSignIn() {
|
||||||
val mobileNumber = binding.etMobileNumber.text.toString().trim()
|
val mobileNumber = binding.etMobileNumber.text.toString().trim()
|
||||||
val password = binding.etPassword.text.toString().trim()
|
val password = binding.etPassword.text.toString().trim()
|
||||||
@@ -131,13 +148,13 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
is ApiResult.Error -> {
|
is ApiResult.Error -> {
|
||||||
Log.d(TAG, "Login failed: ${result.message} (code: ${result.code})")
|
Log.d(TAG, "Login failed: ${result.message} (code: ${result.code})")
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
Toast.makeText(this@LoginActivity, result.message, Toast.LENGTH_LONG).show()
|
showLoginError(result.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Exception in performLogin coroutine", e)
|
Log.e(TAG, "Exception in performLogin coroutine", e)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
Toast.makeText(this@LoginActivity, "Login failed: ${e.message}", Toast.LENGTH_LONG).show()
|
showLoginError("Login failed: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,13 +186,15 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleRegister() {
|
private fun handleRegister() {
|
||||||
Toast.makeText(this, "Register functionality coming soon", Toast.LENGTH_SHORT).show()
|
Log.d(TAG, "Register button clicked")
|
||||||
// TODO: Navigate to registration screen
|
val intent = Intent(this, RegisterActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleForgotPassword() {
|
private fun handleForgotPassword() {
|
||||||
Toast.makeText(this, "Forgot password functionality coming soon", Toast.LENGTH_SHORT).show()
|
Log.d(TAG, "Forgot password button clicked")
|
||||||
// TODO: Navigate to forgot password screen
|
val intent = Intent(this, ForgotPasswordActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePayWithoutAccount() {
|
private fun handlePayWithoutAccount() {
|
||||||
|
205
app/src/main/java/sh/sar/gridflow/OtpVerificationActivity.kt
Normal file
205
app/src/main/java/sh/sar/gridflow/OtpVerificationActivity.kt
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
package sh.sar.gridflow
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.CountDownTimer
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import sh.sar.gridflow.databinding.ActivityOtpVerificationBinding
|
||||||
|
import sh.sar.gridflow.network.ApiResult
|
||||||
|
import sh.sar.gridflow.network.FenakaApiService
|
||||||
|
|
||||||
|
class OtpVerificationActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityOtpVerificationBinding
|
||||||
|
private lateinit var apiService: FenakaApiService
|
||||||
|
private var countDownTimer: CountDownTimer? = null
|
||||||
|
private var mobileNumber: String = ""
|
||||||
|
private var email: String = ""
|
||||||
|
private var password: String = ""
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "OtpVerificationActivity"
|
||||||
|
const val EXTRA_MOBILE_NUMBER = "mobile_number"
|
||||||
|
const val EXTRA_EMAIL = "email"
|
||||||
|
const val EXTRA_PASSWORD = "password"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Force system theme (follows device dark mode setting)
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||||
|
|
||||||
|
Log.d(TAG, "OtpVerificationActivity onCreate")
|
||||||
|
|
||||||
|
binding = ActivityOtpVerificationBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
apiService = FenakaApiService()
|
||||||
|
|
||||||
|
// Get data from intent
|
||||||
|
mobileNumber = intent.getStringExtra(EXTRA_MOBILE_NUMBER) ?: ""
|
||||||
|
email = intent.getStringExtra(EXTRA_EMAIL) ?: ""
|
||||||
|
password = intent.getStringExtra(EXTRA_PASSWORD) ?: ""
|
||||||
|
|
||||||
|
setupUI()
|
||||||
|
setupClickListeners()
|
||||||
|
startCountdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupUI() {
|
||||||
|
// Format mobile number for display: (+960) 7697583
|
||||||
|
val formattedNumber = "(+960) $mobileNumber"
|
||||||
|
binding.tvOtpMessage.text = "We have sent an OTP code to your\nnumber $formattedNumber.\nDidn't receive the code?"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupClickListeners() {
|
||||||
|
binding.btnContinue.setOnClickListener {
|
||||||
|
handleContinue()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnResend.setOnClickListener {
|
||||||
|
handleResend()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleContinue() {
|
||||||
|
val otpCode = binding.etOtpCode.text.toString().trim()
|
||||||
|
|
||||||
|
Log.d(TAG, "handleContinue called with OTP: $otpCode")
|
||||||
|
|
||||||
|
if (validateOtp(otpCode)) {
|
||||||
|
Log.d(TAG, "OTP validation passed, verifying with server")
|
||||||
|
performOtpVerification(otpCode)
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "OTP validation failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleResend() {
|
||||||
|
Log.d(TAG, "Resend OTP requested - resubmitting registration")
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
when (val result = apiService.signup(email, mobileNumber, password)) {
|
||||||
|
is ApiResult.Success -> {
|
||||||
|
Log.d(TAG, "Registration resubmitted successfully")
|
||||||
|
setLoading(false)
|
||||||
|
Toast.makeText(this@OtpVerificationActivity, "New OTP sent to $mobileNumber", Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
|
// Restart countdown
|
||||||
|
startCountdown()
|
||||||
|
}
|
||||||
|
is ApiResult.Error -> {
|
||||||
|
Log.d(TAG, "Registration resubmission failed: ${result.message}")
|
||||||
|
setLoading(false)
|
||||||
|
Toast.makeText(
|
||||||
|
this@OtpVerificationActivity,
|
||||||
|
"Failed to resend OTP: ${result.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Exception in resend", e)
|
||||||
|
setLoading(false)
|
||||||
|
Toast.makeText(
|
||||||
|
this@OtpVerificationActivity,
|
||||||
|
"Failed to resend OTP: ${e.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performOtpVerification(otpCode: String) {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
// TODO: Implement actual OTP verification API call
|
||||||
|
// For now, just simulate success
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
// Simulate API call
|
||||||
|
kotlinx.coroutines.delay(1500)
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
Toast.makeText(
|
||||||
|
this@OtpVerificationActivity,
|
||||||
|
"Account verified successfully!",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
|
||||||
|
// Navigate back to login
|
||||||
|
val intent = Intent(this@OtpVerificationActivity, LoginActivity::class.java)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Exception in OTP verification", e)
|
||||||
|
setLoading(false)
|
||||||
|
Toast.makeText(
|
||||||
|
this@OtpVerificationActivity,
|
||||||
|
"Failed to verify OTP: ${e.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateOtp(otpCode: String): Boolean {
|
||||||
|
if (otpCode.isEmpty()) {
|
||||||
|
binding.etOtpCode.error = "OTP code is required"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otpCode.length != 6) {
|
||||||
|
binding.etOtpCode.error = "OTP code must be 6 digits"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!otpCode.matches(Regex("^\\d{6}$"))) {
|
||||||
|
binding.etOtpCode.error = "OTP code must contain only numbers"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startCountdown() {
|
||||||
|
binding.btnResend.isEnabled = false
|
||||||
|
binding.btnResend.text = "30"
|
||||||
|
|
||||||
|
countDownTimer?.cancel()
|
||||||
|
countDownTimer = object : CountDownTimer(30000, 1000) {
|
||||||
|
override fun onTick(millisUntilFinished: Long) {
|
||||||
|
val seconds = millisUntilFinished / 1000
|
||||||
|
binding.btnResend.text = seconds.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinish() {
|
||||||
|
binding.btnResend.isEnabled = true
|
||||||
|
binding.btnResend.text = "Resend"
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLoading(isLoading: Boolean) {
|
||||||
|
binding.btnContinue.isEnabled = !isLoading
|
||||||
|
binding.btnContinue.text = if (isLoading) "Verifying..." else "Continue"
|
||||||
|
binding.etOtpCode.isEnabled = !isLoading
|
||||||
|
binding.btnResend.isEnabled = !isLoading && binding.btnResend.text == "Resend"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
countDownTimer?.cancel()
|
||||||
|
}
|
||||||
|
}
|
200
app/src/main/java/sh/sar/gridflow/PasswordResetOtpActivity.kt
Normal file
200
app/src/main/java/sh/sar/gridflow/PasswordResetOtpActivity.kt
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
package sh.sar.gridflow
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.CountDownTimer
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import sh.sar.gridflow.databinding.ActivityPasswordResetOtpBinding
|
||||||
|
import sh.sar.gridflow.network.ApiResult
|
||||||
|
import sh.sar.gridflow.network.FenakaApiService
|
||||||
|
|
||||||
|
class PasswordResetOtpActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityPasswordResetOtpBinding
|
||||||
|
private lateinit var apiService: FenakaApiService
|
||||||
|
private var countDownTimer: CountDownTimer? = null
|
||||||
|
private var mobileNumber: String = ""
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "PasswordResetOtpActivity"
|
||||||
|
const val EXTRA_MOBILE_NUMBER = "mobile_number"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Force system theme (follows device dark mode setting)
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||||
|
|
||||||
|
Log.d(TAG, "PasswordResetOtpActivity onCreate")
|
||||||
|
|
||||||
|
binding = ActivityPasswordResetOtpBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
apiService = FenakaApiService()
|
||||||
|
|
||||||
|
// Get mobile number from intent
|
||||||
|
mobileNumber = intent.getStringExtra(EXTRA_MOBILE_NUMBER) ?: ""
|
||||||
|
|
||||||
|
setupUI()
|
||||||
|
setupClickListeners()
|
||||||
|
startCountdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupUI() {
|
||||||
|
// Format mobile number for display: (+960) 7697583
|
||||||
|
val formattedNumber = "(+960) $mobileNumber"
|
||||||
|
binding.tvOtpMessage.text = "We have sent a password reset OTP to your\nnumber $formattedNumber.\nDidn't receive the code?"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupClickListeners() {
|
||||||
|
binding.btnContinue.setOnClickListener {
|
||||||
|
handleContinue()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnResend.setOnClickListener {
|
||||||
|
handleResend()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleContinue() {
|
||||||
|
val otpCode = binding.etOtpCode.text.toString().trim()
|
||||||
|
|
||||||
|
Log.d(TAG, "handleContinue called with OTP: $otpCode")
|
||||||
|
|
||||||
|
if (validateOtp(otpCode)) {
|
||||||
|
Log.d(TAG, "OTP validation passed, verifying with server")
|
||||||
|
performOtpVerification(otpCode)
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "OTP validation failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleResend() {
|
||||||
|
Log.d(TAG, "Resend OTP requested - resubmitting forgot password request")
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
when (val result = apiService.forgotPassword(mobileNumber)) {
|
||||||
|
is ApiResult.Success -> {
|
||||||
|
Log.d(TAG, "Forgot password resubmitted successfully")
|
||||||
|
setLoading(false)
|
||||||
|
Toast.makeText(this@PasswordResetOtpActivity, "New OTP sent to $mobileNumber", Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
|
// Restart countdown
|
||||||
|
startCountdown()
|
||||||
|
}
|
||||||
|
is ApiResult.Error -> {
|
||||||
|
Log.d(TAG, "Forgot password resubmission failed: ${result.message}")
|
||||||
|
setLoading(false)
|
||||||
|
Toast.makeText(
|
||||||
|
this@PasswordResetOtpActivity,
|
||||||
|
"Failed to resend OTP: ${result.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Exception in resend", e)
|
||||||
|
setLoading(false)
|
||||||
|
Toast.makeText(
|
||||||
|
this@PasswordResetOtpActivity,
|
||||||
|
"Failed to resend OTP: ${e.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performOtpVerification(otpCode: String) {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
// TODO: Implement actual OTP verification API call for password reset
|
||||||
|
// For now, just simulate success
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
// Simulate API call
|
||||||
|
kotlinx.coroutines.delay(1500)
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
Toast.makeText(
|
||||||
|
this@PasswordResetOtpActivity,
|
||||||
|
"OTP verified! You can now set a new password.",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
|
||||||
|
// Navigate to set new password screen (TODO: implement this screen)
|
||||||
|
// For now, go back to login
|
||||||
|
val intent = Intent(this@PasswordResetOtpActivity, LoginActivity::class.java)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Exception in OTP verification", e)
|
||||||
|
setLoading(false)
|
||||||
|
Toast.makeText(
|
||||||
|
this@PasswordResetOtpActivity,
|
||||||
|
"Failed to verify OTP: ${e.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateOtp(otpCode: String): Boolean {
|
||||||
|
if (otpCode.isEmpty()) {
|
||||||
|
binding.etOtpCode.error = "OTP code is required"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otpCode.length != 6) {
|
||||||
|
binding.etOtpCode.error = "OTP code must be 6 digits"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!otpCode.matches(Regex("^\\d{6}$"))) {
|
||||||
|
binding.etOtpCode.error = "OTP code must contain only numbers"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startCountdown() {
|
||||||
|
binding.btnResend.isEnabled = false
|
||||||
|
binding.btnResend.text = "30"
|
||||||
|
|
||||||
|
countDownTimer?.cancel()
|
||||||
|
countDownTimer = object : CountDownTimer(30000, 1000) {
|
||||||
|
override fun onTick(millisUntilFinished: Long) {
|
||||||
|
val seconds = millisUntilFinished / 1000
|
||||||
|
binding.btnResend.text = seconds.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinish() {
|
||||||
|
binding.btnResend.isEnabled = true
|
||||||
|
binding.btnResend.text = "Resend"
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLoading(isLoading: Boolean) {
|
||||||
|
binding.btnContinue.isEnabled = !isLoading
|
||||||
|
binding.btnContinue.text = if (isLoading) "Verifying..." else "Continue"
|
||||||
|
binding.etOtpCode.isEnabled = !isLoading
|
||||||
|
binding.btnResend.isEnabled = !isLoading && binding.btnResend.text == "Resend"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
countDownTimer?.cancel()
|
||||||
|
}
|
||||||
|
}
|
211
app/src/main/java/sh/sar/gridflow/RegisterActivity.kt
Normal file
211
app/src/main/java/sh/sar/gridflow/RegisterActivity.kt
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
package sh.sar.gridflow
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import sh.sar.gridflow.databinding.ActivityRegisterBinding
|
||||||
|
import sh.sar.gridflow.network.ApiResult
|
||||||
|
import sh.sar.gridflow.network.FenakaApiService
|
||||||
|
|
||||||
|
class RegisterActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityRegisterBinding
|
||||||
|
private lateinit var apiService: FenakaApiService
|
||||||
|
private var hasEmailError: Boolean = false
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "RegisterActivity"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Force system theme (follows device dark mode setting)
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||||
|
|
||||||
|
Log.d(TAG, "RegisterActivity onCreate")
|
||||||
|
|
||||||
|
binding = ActivityRegisterBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
apiService = FenakaApiService()
|
||||||
|
|
||||||
|
setupClickListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupClickListeners() {
|
||||||
|
binding.btnRegister.setOnClickListener {
|
||||||
|
handleRegister()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showMobileExistsDialog(mobile: String) {
|
||||||
|
val dialog = androidx.appcompat.app.AlertDialog.Builder(this)
|
||||||
|
.setTitle("Mobile Number Already Registered")
|
||||||
|
.setMessage("The mobile number $mobile is already registered with an account. Would you like to reset your password instead?")
|
||||||
|
.setPositiveButton("Reset Password") { _, _ ->
|
||||||
|
// Navigate to forgot password screen
|
||||||
|
val intent = Intent(this, ForgotPasswordActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
.setNegativeButton("Cancel") { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
// Check if we should also show email error
|
||||||
|
checkForEmailError()
|
||||||
|
}
|
||||||
|
.setCancelable(true)
|
||||||
|
.setOnCancelListener {
|
||||||
|
// If dialog is cancelled (back button), also check for email error
|
||||||
|
checkForEmailError()
|
||||||
|
}
|
||||||
|
.create()
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkForEmailError() {
|
||||||
|
// If we detected an email error along with mobile error, show it now
|
||||||
|
if (hasEmailError) {
|
||||||
|
binding.etEmail.error = "Email address is also already registered"
|
||||||
|
binding.etEmail.requestFocus()
|
||||||
|
hasEmailError = false // Reset the flag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleValidationErrors(errorMessage: String) {
|
||||||
|
// For other validation errors, show a toast for now
|
||||||
|
// In the future, you could parse more specific field errors here
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
"Validation error: $errorMessage",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRegister() {
|
||||||
|
val email = binding.etEmail.text.toString().trim()
|
||||||
|
val mobileNumber = binding.etMobileNumber.text.toString().trim()
|
||||||
|
val password = binding.etPassword.text.toString().trim()
|
||||||
|
|
||||||
|
Log.d(TAG, "handleRegister called for: $email, $mobileNumber")
|
||||||
|
|
||||||
|
if (validateInput(email, mobileNumber, password)) {
|
||||||
|
Log.d(TAG, "Input validation passed, creating account")
|
||||||
|
performRegistration(email, mobileNumber, password)
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Input validation failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performRegistration(email: String, mobile: String, password: String) {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
when (val result = apiService.signup(email, mobile, password)) {
|
||||||
|
is ApiResult.Success -> {
|
||||||
|
Log.d(TAG, "Signup successful: ${result.data}")
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
// Navigate to OTP verification
|
||||||
|
val intent = Intent(this@RegisterActivity, OtpVerificationActivity::class.java)
|
||||||
|
intent.putExtra(OtpVerificationActivity.EXTRA_MOBILE_NUMBER, mobile)
|
||||||
|
intent.putExtra(OtpVerificationActivity.EXTRA_EMAIL, email)
|
||||||
|
intent.putExtra(OtpVerificationActivity.EXTRA_PASSWORD, password)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
is ApiResult.Error -> {
|
||||||
|
Log.d(TAG, "Signup failed: ${result.message} (code: ${result.code})")
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
if (result.message == "MOBILE_ALREADY_EXISTS") {
|
||||||
|
showMobileExistsDialog(mobile)
|
||||||
|
} else if (result.message == "MOBILE_AND_EMAIL_EXIST") {
|
||||||
|
hasEmailError = true
|
||||||
|
showMobileExistsDialog(mobile)
|
||||||
|
} else if (result.message == "EMAIL_ALREADY_EXISTS") {
|
||||||
|
// Show error directly on email field
|
||||||
|
binding.etEmail.error = "Email address is already registered"
|
||||||
|
binding.etEmail.requestFocus()
|
||||||
|
} else {
|
||||||
|
// Check if it's a validation error we can parse
|
||||||
|
if (result.code == 422) {
|
||||||
|
handleValidationErrors(result.message)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
this@RegisterActivity,
|
||||||
|
result.message,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Exception in registration", e)
|
||||||
|
setLoading(false)
|
||||||
|
Toast.makeText(
|
||||||
|
this@RegisterActivity,
|
||||||
|
"Failed to create account: ${e.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateInput(
|
||||||
|
email: String,
|
||||||
|
mobileNumber: String,
|
||||||
|
password: String
|
||||||
|
): Boolean {
|
||||||
|
|
||||||
|
if (email.isEmpty()) {
|
||||||
|
binding.etEmail.error = "Email is required"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
|
||||||
|
binding.etEmail.error = "Enter a valid email address"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mobileNumber.isEmpty()) {
|
||||||
|
binding.etMobileNumber.error = "Mobile number is required"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic Maldives mobile number validation (7xxxxxx format)
|
||||||
|
if (!mobileNumber.matches(Regex("^[79]\\d{6}$"))) {
|
||||||
|
binding.etMobileNumber.error = "Enter a valid Maldives mobile number"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.isEmpty()) {
|
||||||
|
binding.etPassword.error = "Password is required"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 8) {
|
||||||
|
binding.etPassword.error = "Password must be at least 8 characters"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLoading(isLoading: Boolean) {
|
||||||
|
binding.btnRegister.isEnabled = !isLoading
|
||||||
|
binding.btnRegister.text = if (isLoading) "Creating Account..." else "Sign up"
|
||||||
|
|
||||||
|
binding.etEmail.isEnabled = !isLoading
|
||||||
|
binding.etMobileNumber.isEnabled = !isLoading
|
||||||
|
binding.etPassword.isEnabled = !isLoading
|
||||||
|
}
|
||||||
|
}
|
@@ -5,6 +5,16 @@ data class LoginRequest(
|
|||||||
val password: String
|
val password: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class SignupRequest(
|
||||||
|
val email: String,
|
||||||
|
val mobile: String,
|
||||||
|
val password: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ForgotPasswordRequest(
|
||||||
|
val mobile: String
|
||||||
|
)
|
||||||
|
|
||||||
data class LoginResponse(
|
data class LoginResponse(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
@@ -15,6 +25,15 @@ data class LoginResponse(
|
|||||||
val deletedAt: String?
|
val deletedAt: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class SignupResponse(
|
||||||
|
val email: String,
|
||||||
|
val mobile: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ForgotPasswordResponse(
|
||||||
|
val mobile: String
|
||||||
|
)
|
||||||
|
|
||||||
data class ErrorResponse(
|
data class ErrorResponse(
|
||||||
val errors: List<ValidationError>? = null,
|
val errors: List<ValidationError>? = null,
|
||||||
val error: String? = null
|
val error: String? = null
|
||||||
|
@@ -9,8 +9,12 @@ import okhttp3.MediaType.Companion.toMediaType
|
|||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import sh.sar.gridflow.data.ErrorResponse
|
import sh.sar.gridflow.data.ErrorResponse
|
||||||
|
import sh.sar.gridflow.data.ForgotPasswordRequest
|
||||||
|
import sh.sar.gridflow.data.ForgotPasswordResponse
|
||||||
import sh.sar.gridflow.data.LoginRequest
|
import sh.sar.gridflow.data.LoginRequest
|
||||||
import sh.sar.gridflow.data.LoginResponse
|
import sh.sar.gridflow.data.LoginResponse
|
||||||
|
import sh.sar.gridflow.data.SignupRequest
|
||||||
|
import sh.sar.gridflow.data.SignupResponse
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class FenakaApiService {
|
class FenakaApiService {
|
||||||
@@ -92,6 +96,131 @@ class FenakaApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun forgotPassword(mobile: String): ApiResult<ForgotPasswordResponse> = withContext(Dispatchers.IO) {
|
||||||
|
Log.d(TAG, "Attempting forgot password for mobile: $mobile")
|
||||||
|
|
||||||
|
val forgotPasswordRequest = ForgotPasswordRequest(mobile)
|
||||||
|
val requestBody = gson.toJson(forgotPasswordRequest).toRequestBody(JSON_MEDIA_TYPE.toMediaType())
|
||||||
|
|
||||||
|
Log.d(TAG, "Forgot password request body: ${gson.toJson(forgotPasswordRequest)}")
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$BASE_URL/auth/password-reset/request")
|
||||||
|
.post(requestBody)
|
||||||
|
.header("Authorization", "Bearer $BEARER_TOKEN")
|
||||||
|
.header("Content-Type", JSON_MEDIA_TYPE)
|
||||||
|
.header("Host", "api.fenaka.mv")
|
||||||
|
.header("User-Agent", "Dart/3.5 (dart:io)")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
Log.d(TAG, "Making forgot password request to: ${request.url}")
|
||||||
|
|
||||||
|
try {
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
Log.d(TAG, "Forgot password response code: ${response.code}")
|
||||||
|
Log.d(TAG, "Forgot password response headers: ${response.headers}")
|
||||||
|
|
||||||
|
val responseBody = response.body?.string()
|
||||||
|
Log.d(TAG, "Forgot password response body: $responseBody")
|
||||||
|
|
||||||
|
when (response.code) {
|
||||||
|
200 -> {
|
||||||
|
val forgotPasswordResponse = gson.fromJson(responseBody, ForgotPasswordResponse::class.java)
|
||||||
|
Log.d(TAG, "Forgot password successful for: ${forgotPasswordResponse.mobile}")
|
||||||
|
ApiResult.Success(forgotPasswordResponse, null)
|
||||||
|
}
|
||||||
|
404 -> {
|
||||||
|
Log.d(TAG, "Forgot password failed: 404 Mobile number not found")
|
||||||
|
ApiResult.Error("MOBILE_NOT_FOUND", 404)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d(TAG, "Forgot password failed: Unknown error ${response.code}")
|
||||||
|
ApiResult.Error("Unknown error occurred", response.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Network error during forgot password", e)
|
||||||
|
ApiResult.Error("Network error: ${e.message}", -1)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unexpected error during forgot password", e)
|
||||||
|
ApiResult.Error("Unexpected error: ${e.message}", -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun signup(email: String, mobile: String, password: String): ApiResult<SignupResponse> = withContext(Dispatchers.IO) {
|
||||||
|
Log.d(TAG, "Attempting signup for mobile: $mobile, email: $email")
|
||||||
|
|
||||||
|
val signupRequest = SignupRequest(email, mobile, password)
|
||||||
|
val requestBody = gson.toJson(signupRequest).toRequestBody(JSON_MEDIA_TYPE.toMediaType())
|
||||||
|
|
||||||
|
Log.d(TAG, "Signup request body: ${gson.toJson(signupRequest)}")
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$BASE_URL/auth/signup")
|
||||||
|
.post(requestBody)
|
||||||
|
.header("Authorization", "Bearer $BEARER_TOKEN")
|
||||||
|
.header("Content-Type", JSON_MEDIA_TYPE)
|
||||||
|
.header("Host", "api.fenaka.mv")
|
||||||
|
.header("User-Agent", "Dart/3.5 (dart:io)")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
Log.d(TAG, "Making signup request to: ${request.url}")
|
||||||
|
|
||||||
|
try {
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
Log.d(TAG, "Signup response code: ${response.code}")
|
||||||
|
Log.d(TAG, "Signup response headers: ${response.headers}")
|
||||||
|
|
||||||
|
val responseBody = response.body?.string()
|
||||||
|
Log.d(TAG, "Signup response body: $responseBody")
|
||||||
|
|
||||||
|
when (response.code) {
|
||||||
|
200 -> {
|
||||||
|
val signupResponse = gson.fromJson(responseBody, SignupResponse::class.java)
|
||||||
|
Log.d(TAG, "Signup successful for: ${signupResponse.email}, ${signupResponse.mobile}")
|
||||||
|
ApiResult.Success(signupResponse, null)
|
||||||
|
}
|
||||||
|
422 -> {
|
||||||
|
val errorResponse = gson.fromJson(responseBody, ErrorResponse::class.java)
|
||||||
|
Log.d(TAG, "Signup failed: 422 Validation errors - ${errorResponse.errors}")
|
||||||
|
|
||||||
|
// Check for multiple errors and prioritize mobile over email
|
||||||
|
val mobileError = errorResponse.errors?.find { it.param == "mobile" && it.msg?.contains("already exists", ignoreCase = true) == true }
|
||||||
|
val emailError = errorResponse.errors?.find { it.param == "email" && it.msg?.contains("already exists", ignoreCase = true) == true }
|
||||||
|
|
||||||
|
when {
|
||||||
|
mobileError != null -> {
|
||||||
|
// Mobile already exists takes priority (used for login)
|
||||||
|
// Pass additional info about email error if it exists
|
||||||
|
val message = if (emailError != null) "MOBILE_AND_EMAIL_EXIST" else "MOBILE_ALREADY_EXISTS"
|
||||||
|
ApiResult.Error(message, 422)
|
||||||
|
}
|
||||||
|
emailError != null -> {
|
||||||
|
// Email already exists (only if mobile is not duplicate)
|
||||||
|
ApiResult.Error("EMAIL_ALREADY_EXISTS", 422)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Other validation errors
|
||||||
|
val firstError = errorResponse.errors?.firstOrNull()
|
||||||
|
val errorMessage = firstError?.msg ?: "Validation error"
|
||||||
|
ApiResult.Error(errorMessage, 422)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d(TAG, "Signup failed: Unknown error ${response.code}")
|
||||||
|
ApiResult.Error("Unknown error occurred", response.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Network error during signup", e)
|
||||||
|
ApiResult.Error("Network error: ${e.message}", -1)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unexpected error during signup", e)
|
||||||
|
ApiResult.Error("Unexpected error: ${e.message}", -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getOutstandingBills(cookie: String): ApiResult<List<Any>> = withContext(Dispatchers.IO) {
|
suspend fun getOutstandingBills(cookie: String): ApiResult<List<Any>> = withContext(Dispatchers.IO) {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url("$BASE_URL/saiph/subscriptions/summaries/outstanding")
|
.url("$BASE_URL/saiph/subscriptions/summaries/outstanding")
|
||||||
|
10
app/src/main/res/drawable/ic_arrow_back.xml
Normal file
10
app/src/main/res/drawable/ic_arrow_back.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorOnSurface">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||||
|
</vector>
|
99
app/src/main/res/layout/activity_forgot_password.xml
Normal file
99
app/src/main/res/layout/activity_forgot_password.xml
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="true"
|
||||||
|
android:background="?android:attr/colorBackground">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="32dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<!-- Logo and Title Section -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_app_logo"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="120dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:src="@mipmap/ic_launcher"
|
||||||
|
android:contentDescription="GridFlow Logo" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Reset password"
|
||||||
|
android:textSize="28sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Enter your phone number"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginBottom="32dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Form Section -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginTop="32dp">
|
||||||
|
|
||||||
|
<!-- Mobile Number Input -->
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:hint="Mobile number"
|
||||||
|
app:boxStrokeColor="@color/design_default_color_primary"
|
||||||
|
app:hintTextColor="@color/design_default_color_primary"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/et_mobile_number"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="phone"
|
||||||
|
android:maxLength="7"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- Continue Button -->
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_continue"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:text="Continue"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
app:cornerRadius="8dp"
|
||||||
|
android:layout_marginBottom="32dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Spacer for bottom -->
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="32dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
115
app/src/main/res/layout/activity_otp_verification.xml
Normal file
115
app/src/main/res/layout/activity_otp_verification.xml
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="true"
|
||||||
|
android:background="?android:attr/colorBackground">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="32dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<!-- Logo and Title Section -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_app_logo"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="120dp"
|
||||||
|
android:layout_marginBottom="32dp"
|
||||||
|
android:src="@mipmap/ic_launcher"
|
||||||
|
android:contentDescription="GridFlow Logo" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Enter OTP code"
|
||||||
|
android:textSize="28sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:gravity="center" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_otp_message"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="We have sent an OTP code to your\nnumber (+960) 7697583.\nDidn't receive the code?"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:gravity="center"
|
||||||
|
android:lineSpacingExtra="4dp"
|
||||||
|
android:layout_marginBottom="24dp" />
|
||||||
|
|
||||||
|
<!-- Countdown/Resend Button -->
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_resend"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:text="30"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:enabled="false"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||||
|
android:layout_marginBottom="32dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- OTP Form Section -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- OTP Code Input -->
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:hint="OTP Code"
|
||||||
|
app:boxStrokeColor="@color/design_default_color_primary"
|
||||||
|
app:hintTextColor="@color/design_default_color_primary"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/et_otp_code"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"
|
||||||
|
android:maxLength="6"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:letterSpacing="0.5" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- Continue Button -->
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_continue"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:text="Continue"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
app:cornerRadius="8dp"
|
||||||
|
android:layout_marginBottom="32dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Spacer for bottom -->
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="32dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
115
app/src/main/res/layout/activity_password_reset_otp.xml
Normal file
115
app/src/main/res/layout/activity_password_reset_otp.xml
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="true"
|
||||||
|
android:background="?android:attr/colorBackground">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="32dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<!-- Logo and Title Section -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_app_logo"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="120dp"
|
||||||
|
android:layout_marginBottom="32dp"
|
||||||
|
android:src="@mipmap/ic_launcher"
|
||||||
|
android:contentDescription="GridFlow Logo" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Enter OTP code"
|
||||||
|
android:textSize="28sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:gravity="center" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_otp_message"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="We have sent a password reset OTP to your\nnumber (+960) 7697583.\nDidn't receive the code?"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:gravity="center"
|
||||||
|
android:lineSpacingExtra="4dp"
|
||||||
|
android:layout_marginBottom="24dp" />
|
||||||
|
|
||||||
|
<!-- Countdown/Resend Button -->
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_resend"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:text="30"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:enabled="false"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||||
|
android:layout_marginBottom="32dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- OTP Form Section -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- OTP Code Input -->
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:hint="OTP Code"
|
||||||
|
app:boxStrokeColor="@color/design_default_color_primary"
|
||||||
|
app:hintTextColor="@color/design_default_color_primary"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/et_otp_code"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"
|
||||||
|
android:maxLength="6"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:letterSpacing="0.5" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- Continue Button -->
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_continue"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:text="Continue"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
app:cornerRadius="8dp"
|
||||||
|
android:layout_marginBottom="32dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Spacer for bottom -->
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="32dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
129
app/src/main/res/layout/activity_register.xml
Normal file
129
app/src/main/res/layout/activity_register.xml
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="true"
|
||||||
|
android:background="?android:attr/colorBackground">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="32dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<!-- Logo and Title Section -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_app_logo"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="120dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:src="@mipmap/ic_launcher"
|
||||||
|
android:contentDescription="GridFlow Logo" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Sign Up"
|
||||||
|
android:textSize="28sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:layout_marginBottom="32dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Registration Form -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginTop="32dp">
|
||||||
|
|
||||||
|
<!-- Email Input -->
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:hint="Email"
|
||||||
|
app:boxStrokeColor="@color/design_default_color_primary"
|
||||||
|
app:hintTextColor="@color/design_default_color_primary"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/et_email"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textEmailAddress"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- Mobile Number Input -->
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:hint="Mobile number"
|
||||||
|
app:boxStrokeColor="@color/design_default_color_primary"
|
||||||
|
app:hintTextColor="@color/design_default_color_primary"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/et_mobile_number"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="phone"
|
||||||
|
android:maxLength="7"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- Password Input -->
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:hint="Password"
|
||||||
|
app:boxStrokeColor="@color/design_default_color_primary"
|
||||||
|
app:hintTextColor="@color/design_default_color_primary"
|
||||||
|
app:passwordToggleEnabled="true"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/et_password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- Sign Up Button -->
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_register"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:text="Sign up"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
app:cornerRadius="8dp"
|
||||||
|
android:layout_marginBottom="32dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Spacer for bottom -->
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="32dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
Reference in New Issue
Block a user