forgot password and registration pages... cant fully test because fenaka sms is not working

This commit is contained in:
2025-07-24 18:25:11 +05:00
parent f13211fbd2
commit 2918cd79d0
13 changed files with 1418 additions and 6 deletions

View File

@@ -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>

View 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
}
}

View File

@@ -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() {

View 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()
}
}

View 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()
}
}

View 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
}
}

View File

@@ -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

View File

@@ -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")

View 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>

View 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>

View 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>

View 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>

View 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>