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:label="@string/app_name"
|
||||
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>
|
||||
|
||||
</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() {
|
||||
val mobileNumber = binding.etMobileNumber.text.toString().trim()
|
||||
val password = binding.etPassword.text.toString().trim()
|
||||
@@ -131,13 +148,13 @@ class LoginActivity : AppCompatActivity() {
|
||||
is ApiResult.Error -> {
|
||||
Log.d(TAG, "Login failed: ${result.message} (code: ${result.code})")
|
||||
setLoading(false)
|
||||
Toast.makeText(this@LoginActivity, result.message, Toast.LENGTH_LONG).show()
|
||||
showLoginError(result.message)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception in performLogin coroutine", e)
|
||||
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() {
|
||||
Toast.makeText(this, "Register functionality coming soon", Toast.LENGTH_SHORT).show()
|
||||
// TODO: Navigate to registration screen
|
||||
Log.d(TAG, "Register button clicked")
|
||||
val intent = Intent(this, RegisterActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun handleForgotPassword() {
|
||||
Toast.makeText(this, "Forgot password functionality coming soon", Toast.LENGTH_SHORT).show()
|
||||
// TODO: Navigate to forgot password screen
|
||||
Log.d(TAG, "Forgot password button clicked")
|
||||
val intent = Intent(this, ForgotPasswordActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
data class SignupRequest(
|
||||
val email: String,
|
||||
val mobile: String,
|
||||
val password: String
|
||||
)
|
||||
|
||||
data class ForgotPasswordRequest(
|
||||
val mobile: String
|
||||
)
|
||||
|
||||
data class LoginResponse(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
@@ -15,6 +25,15 @@ data class LoginResponse(
|
||||
val deletedAt: String?
|
||||
)
|
||||
|
||||
data class SignupResponse(
|
||||
val email: String,
|
||||
val mobile: String
|
||||
)
|
||||
|
||||
data class ForgotPasswordResponse(
|
||||
val mobile: String
|
||||
)
|
||||
|
||||
data class ErrorResponse(
|
||||
val errors: List<ValidationError>? = null,
|
||||
val error: String? = null
|
||||
|
@@ -9,8 +9,12 @@ import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
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.LoginResponse
|
||||
import sh.sar.gridflow.data.SignupRequest
|
||||
import sh.sar.gridflow.data.SignupResponse
|
||||
import java.io.IOException
|
||||
|
||||
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) {
|
||||
val request = Request.Builder()
|
||||
.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