forked from LibreMV/GridFlow
login works
This commit is contained in:
@@ -48,6 +48,10 @@ dependencies {
|
|||||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||||
implementation(libs.androidx.navigation.fragment.ktx)
|
implementation(libs.androidx.navigation.fragment.ktx)
|
||||||
implementation(libs.androidx.navigation.ui.ktx)
|
implementation(libs.androidx.navigation.ui.ktx)
|
||||||
|
implementation(libs.okhttp)
|
||||||
|
implementation(libs.okhttp.logging)
|
||||||
|
implementation(libs.gson)
|
||||||
|
implementation(libs.security.crypto)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
@@ -2,6 +2,8 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
@@ -10,7 +12,8 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.GridFlow">
|
android:theme="@style/Theme.GridFlow"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
<activity
|
<activity
|
||||||
android:name=".LoginActivity"
|
android:name=".LoginActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -22,6 +25,11 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".DebugLoginActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/Theme.GridFlow.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
69
app/src/main/java/sh/sar/gridflow/DebugLoginActivity.kt
Normal file
69
app/src/main/java/sh/sar/gridflow/DebugLoginActivity.kt
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
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.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import sh.sar.gridflow.databinding.ActivityLoginBinding
|
||||||
|
import sh.sar.gridflow.network.ApiResult
|
||||||
|
import sh.sar.gridflow.network.FenakaApiService
|
||||||
|
|
||||||
|
class DebugLoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityLoginBinding
|
||||||
|
private lateinit var apiService: FenakaApiService
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "DebugLoginActivity"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
Log.d(TAG, "DebugLoginActivity onCreate")
|
||||||
|
|
||||||
|
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
apiService = FenakaApiService()
|
||||||
|
|
||||||
|
// Simple test button
|
||||||
|
binding.btnSignIn.setOnClickListener {
|
||||||
|
testLogin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testLogin() {
|
||||||
|
Log.d(TAG, "Testing login with hardcoded values")
|
||||||
|
|
||||||
|
binding.btnSignIn.text = "Testing..."
|
||||||
|
binding.btnSignIn.isEnabled = false
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Making API call...")
|
||||||
|
val result = apiService.login("9999999", "testpassword123")
|
||||||
|
|
||||||
|
when (result) {
|
||||||
|
is ApiResult.Success -> {
|
||||||
|
Log.d(TAG, "Success: ${result.data}")
|
||||||
|
Toast.makeText(this@DebugLoginActivity, "Success!", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
is ApiResult.Error -> {
|
||||||
|
Log.d(TAG, "Error: ${result.message}")
|
||||||
|
Toast.makeText(this@DebugLoginActivity, "Error: ${result.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Exception in test", e)
|
||||||
|
Toast.makeText(this@DebugLoginActivity, "Exception: ${e.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnSignIn.text = "Sign In"
|
||||||
|
binding.btnSignIn.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,20 +2,58 @@ package sh.sar.gridflow
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import sh.sar.gridflow.databinding.ActivityLoginBinding
|
import sh.sar.gridflow.databinding.ActivityLoginBinding
|
||||||
|
import sh.sar.gridflow.network.ApiResult
|
||||||
|
import sh.sar.gridflow.network.FenakaApiService
|
||||||
|
import sh.sar.gridflow.utils.SecureStorage
|
||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var binding: ActivityLoginBinding
|
private lateinit var binding: ActivityLoginBinding
|
||||||
|
private lateinit var secureStorage: SecureStorage
|
||||||
|
private lateinit var apiService: FenakaApiService
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LoginActivity"
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
Log.d(TAG, "LoginActivity onCreate")
|
||||||
|
|
||||||
binding = ActivityLoginBinding.inflate(layoutInflater)
|
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
try {
|
||||||
|
secureStorage = SecureStorage(this)
|
||||||
|
apiService = FenakaApiService()
|
||||||
|
|
||||||
|
// Check if already logged in
|
||||||
|
if (secureStorage.isLoggedIn()) {
|
||||||
|
Log.d(TAG, "User already logged in, navigating to main")
|
||||||
|
navigateToMain()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-fill saved credentials
|
||||||
|
secureStorage.getMobile()?.let { mobile ->
|
||||||
|
Log.d(TAG, "Pre-filling mobile number: $mobile")
|
||||||
|
binding.etMobileNumber.setText(mobile)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to initialize SecureStorage, continuing without it", e)
|
||||||
|
apiService = FenakaApiService()
|
||||||
|
Toast.makeText(this, "Warning: Secure storage not available", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
setupClickListeners()
|
setupClickListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,17 +79,91 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
val mobileNumber = binding.etMobileNumber.text.toString().trim()
|
val mobileNumber = binding.etMobileNumber.text.toString().trim()
|
||||||
val password = binding.etPassword.text.toString().trim()
|
val password = binding.etPassword.text.toString().trim()
|
||||||
|
|
||||||
|
Log.d(TAG, "handleSignIn called with mobile: $mobileNumber")
|
||||||
|
|
||||||
if (validateInput(mobileNumber, password)) {
|
if (validateInput(mobileNumber, password)) {
|
||||||
// TODO: Implement actual authentication logic
|
Log.d(TAG, "Input validation passed, calling performLogin")
|
||||||
// For now, just navigate to main activity
|
performLogin(mobileNumber, password)
|
||||||
Toast.makeText(this, "Signing in...", Toast.LENGTH_SHORT).show()
|
} else {
|
||||||
|
Log.d(TAG, "Input validation failed")
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun performLogin(mobile: String, password: String) {
|
||||||
|
Log.d(TAG, "performLogin called with mobile: $mobile")
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
Log.d(TAG, "Starting coroutine for API call")
|
||||||
|
try {
|
||||||
|
when (val result = apiService.login(mobile, password)) {
|
||||||
|
is ApiResult.Success -> {
|
||||||
|
Log.d(TAG, "Login successful: ${result.data}")
|
||||||
|
|
||||||
|
// Save credentials and user info (if SecureStorage is available)
|
||||||
|
if (this@LoginActivity::secureStorage.isInitialized) {
|
||||||
|
try {
|
||||||
|
secureStorage.saveCredentials(mobile, password)
|
||||||
|
secureStorage.saveUserInfo(
|
||||||
|
result.data.name,
|
||||||
|
result.data.email,
|
||||||
|
result.data.id
|
||||||
|
)
|
||||||
|
|
||||||
|
// Extract and save cookie
|
||||||
|
result.cookie?.let { cookie ->
|
||||||
|
val sessionId = extractSessionId(cookie)
|
||||||
|
Log.d(TAG, "Extracted session ID: $sessionId")
|
||||||
|
secureStorage.saveCookie(sessionId)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to save credentials securely", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
navigateToMain()
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractSessionId(setCookieHeader: String): String {
|
||||||
|
// Extract the session ID from Set-Cookie header
|
||||||
|
// Format: connect.sid=s%3A-vUZGRtHZZygj5Xm1Xg9nKdcZqanQCWm.JVdk7%2Bv63292vx5TWsOiws4QiGwKCYKjh%2FUhLWGEYVs; HttpOnly; Path=/; Expires=...
|
||||||
|
return setCookieHeader.substringBefore(";").substringAfter("connect.sid=")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLoading(isLoading: Boolean) {
|
||||||
|
binding.btnSignIn.isEnabled = !isLoading
|
||||||
|
binding.btnSignIn.text = if (isLoading) "Signing in..." else "Sign In"
|
||||||
|
|
||||||
|
// Disable other buttons during loading
|
||||||
|
binding.btnRegister.isEnabled = !isLoading
|
||||||
|
binding.btnForgotPassword.isEnabled = !isLoading
|
||||||
|
binding.btnPayWithoutAccount.isEnabled = !isLoading
|
||||||
|
|
||||||
|
// Show/hide progress indicator
|
||||||
|
binding.etMobileNumber.isEnabled = !isLoading
|
||||||
|
binding.etPassword.isEnabled = !isLoading
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToMain() {
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleRegister() {
|
private fun handleRegister() {
|
||||||
Toast.makeText(this, "Register functionality coming soon", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "Register functionality coming soon", Toast.LENGTH_SHORT).show()
|
||||||
// TODO: Navigate to registration screen
|
// TODO: Navigate to registration screen
|
||||||
@@ -78,6 +190,12 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Client-side password validation - must be at least 8 characters
|
||||||
|
if (password.length < 8) {
|
||||||
|
binding.etPassword.error = "Password must be at least 8 characters"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Basic Maldives mobile number validation (7xxxxxx format)
|
// Basic Maldives mobile number validation (7xxxxxx format)
|
||||||
if (!mobileNumber.matches(Regex("^[79]\\d{6}$"))) {
|
if (!mobileNumber.matches(Regex("^[79]\\d{6}$"))) {
|
||||||
binding.etMobileNumber.error = "Enter a valid Maldives mobile number"
|
binding.etMobileNumber.error = "Enter a valid Maldives mobile number"
|
||||||
|
28
app/src/main/java/sh/sar/gridflow/data/Models.kt
Normal file
28
app/src/main/java/sh/sar/gridflow/data/Models.kt
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package sh.sar.gridflow.data
|
||||||
|
|
||||||
|
data class LoginRequest(
|
||||||
|
val mobile: String,
|
||||||
|
val password: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LoginResponse(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
val mobile: String,
|
||||||
|
val email: String,
|
||||||
|
val createdAt: String,
|
||||||
|
val updatedAt: String,
|
||||||
|
val deletedAt: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ErrorResponse(
|
||||||
|
val errors: List<ValidationError>? = null,
|
||||||
|
val error: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ValidationError(
|
||||||
|
val value: String,
|
||||||
|
val msg: String,
|
||||||
|
val param: String,
|
||||||
|
val location: String
|
||||||
|
)
|
130
app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt
Normal file
130
app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package sh.sar.gridflow.network
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.*
|
||||||
|
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.LoginRequest
|
||||||
|
import sh.sar.gridflow.data.LoginResponse
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class FenakaApiService {
|
||||||
|
|
||||||
|
private val gson = Gson()
|
||||||
|
|
||||||
|
private val client = OkHttpClient.Builder()
|
||||||
|
.addInterceptor(
|
||||||
|
HttpLoggingInterceptor { message ->
|
||||||
|
Log.d(TAG, message)
|
||||||
|
}.apply {
|
||||||
|
level = HttpLoggingInterceptor.Level.BODY
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "FenakaApiService"
|
||||||
|
private const val BASE_URL = "https://api.fenaka.mv"
|
||||||
|
private const val BEARER_TOKEN = "T5kr13UksdBT5Mq3NqNrXQr8uapaje7ONveQMPJsS3" // Hardcoded as requested
|
||||||
|
private const val JSON_MEDIA_TYPE = "application/json; charset=utf-8"
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun login(mobile: String, password: String): ApiResult<LoginResponse> = withContext(Dispatchers.IO) {
|
||||||
|
Log.d(TAG, "Attempting login for mobile: $mobile")
|
||||||
|
|
||||||
|
val loginRequest = LoginRequest(mobile, password)
|
||||||
|
val requestBody = gson.toJson(loginRequest).toRequestBody(JSON_MEDIA_TYPE.toMediaType())
|
||||||
|
|
||||||
|
Log.d(TAG, "Request body: ${gson.toJson(loginRequest)}")
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$BASE_URL/auth/signin")
|
||||||
|
.post(requestBody)
|
||||||
|
.header("Authorization", "Bearer $BEARER_TOKEN")
|
||||||
|
.header("Content-Type", JSON_MEDIA_TYPE)
|
||||||
|
.header("Host", "api.fenaka.mv")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
Log.d(TAG, "Making request to: ${request.url}")
|
||||||
|
|
||||||
|
try {
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
Log.d(TAG, "Response code: ${response.code}")
|
||||||
|
Log.d(TAG, "Response headers: ${response.headers}")
|
||||||
|
|
||||||
|
val responseBody = response.body?.string()
|
||||||
|
Log.d(TAG, "Response body: $responseBody")
|
||||||
|
|
||||||
|
when (response.code) {
|
||||||
|
200 -> {
|
||||||
|
val loginResponse = gson.fromJson(responseBody, LoginResponse::class.java)
|
||||||
|
val setCookieHeader = response.header("Set-Cookie")
|
||||||
|
Log.d(TAG, "Login successful. Set-Cookie: $setCookieHeader")
|
||||||
|
ApiResult.Success(loginResponse, setCookieHeader)
|
||||||
|
}
|
||||||
|
401 -> {
|
||||||
|
val errorResponse = gson.fromJson(responseBody, ErrorResponse::class.java)
|
||||||
|
Log.d(TAG, "Login failed: 401 Unauthorized")
|
||||||
|
ApiResult.Error(errorResponse.error ?: "Incorrect credentials", 401)
|
||||||
|
}
|
||||||
|
422 -> {
|
||||||
|
val errorResponse = gson.fromJson(responseBody, ErrorResponse::class.java)
|
||||||
|
val errorMessage = errorResponse.errors?.firstOrNull()?.msg ?: "Validation error"
|
||||||
|
Log.d(TAG, "Login failed: 422 Validation error - $errorMessage")
|
||||||
|
ApiResult.Error(errorMessage, 422)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d(TAG, "Login failed: Unknown error ${response.code}")
|
||||||
|
ApiResult.Error("Unknown error occurred", response.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Network error during login", e)
|
||||||
|
ApiResult.Error("Network error: ${e.message}", -1)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unexpected error during login", 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")
|
||||||
|
.get()
|
||||||
|
.header("Authorization", "Bearer $BEARER_TOKEN")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Cookie", cookie)
|
||||||
|
.header("Host", "api.fenaka.mv")
|
||||||
|
.header("User-Agent", "Dart/3.3 (dart:io)")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
when (response.code) {
|
||||||
|
200 -> {
|
||||||
|
val responseBody = response.body?.string() ?: "[]"
|
||||||
|
// For now, we just need to know if it's empty or not
|
||||||
|
val bills = gson.fromJson(responseBody, Array<Any>::class.java).toList()
|
||||||
|
ApiResult.Success(bills, null)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
ApiResult.Error("Failed to fetch outstanding bills", response.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
ApiResult.Error("Network error: ${e.message}", -1)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ApiResult.Error("Unexpected error: ${e.message}", -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ApiResult<T> {
|
||||||
|
data class Success<T>(val data: T, val cookie: String?) : ApiResult<T>()
|
||||||
|
data class Error<T>(val message: String, val code: Int) : ApiResult<T>()
|
||||||
|
}
|
@@ -23,15 +23,33 @@ class HomeFragment : Fragment() {
|
|||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
val homeViewModel =
|
val homeViewModel =
|
||||||
ViewModelProvider(this).get(HomeViewModel::class.java)
|
ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory.getInstance(requireActivity().application))
|
||||||
|
.get(HomeViewModel::class.java)
|
||||||
|
|
||||||
_binding = FragmentHomeBinding.inflate(inflater, container, false)
|
_binding = FragmentHomeBinding.inflate(inflater, container, false)
|
||||||
val root: View = binding.root
|
val root: View = binding.root
|
||||||
|
|
||||||
val textView: TextView = binding.textHome
|
val welcomeTextView: TextView = binding.textHome
|
||||||
homeViewModel.text.observe(viewLifecycleOwner) {
|
val billsStatusTextView: TextView = binding.textBillsStatus
|
||||||
textView.text = it
|
val progressBar = binding.progressBills
|
||||||
|
|
||||||
|
homeViewModel.welcomeText.observe(viewLifecycleOwner) {
|
||||||
|
welcomeTextView.text = it
|
||||||
}
|
}
|
||||||
|
|
||||||
|
homeViewModel.billsStatus.observe(viewLifecycleOwner) {
|
||||||
|
billsStatusTextView.text = it
|
||||||
|
}
|
||||||
|
|
||||||
|
homeViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||||
|
if (isLoading) {
|
||||||
|
progressBar.visibility = View.VISIBLE
|
||||||
|
billsStatusTextView.text = "Checking bills status..."
|
||||||
|
} else {
|
||||||
|
progressBar.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,13 +1,77 @@
|
|||||||
package sh.sar.gridflow.ui.home
|
package sh.sar.gridflow.ui.home
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import sh.sar.gridflow.network.ApiResult
|
||||||
|
import sh.sar.gridflow.network.FenakaApiService
|
||||||
|
import sh.sar.gridflow.utils.SecureStorage
|
||||||
|
|
||||||
class HomeViewModel : ViewModel() {
|
class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
private val _text = MutableLiveData<String>().apply {
|
private val secureStorage = SecureStorage(application)
|
||||||
value = "This is home Fragment"
|
private val apiService = FenakaApiService()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "HomeViewModel"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _welcomeText = MutableLiveData<String>()
|
||||||
|
val welcomeText: LiveData<String> = _welcomeText
|
||||||
|
|
||||||
|
private val _billsStatus = MutableLiveData<String>()
|
||||||
|
val billsStatus: LiveData<String> = _billsStatus
|
||||||
|
|
||||||
|
private val _isLoading = MutableLiveData<Boolean>()
|
||||||
|
val isLoading: LiveData<Boolean> = _isLoading
|
||||||
|
|
||||||
|
init {
|
||||||
|
Log.d(TAG, "HomeViewModel initialized")
|
||||||
|
loadUserData()
|
||||||
|
checkOutstandingBills()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadUserData() {
|
||||||
|
val userName = secureStorage.getUserName() ?: "User"
|
||||||
|
Log.d(TAG, "Loading user data: $userName")
|
||||||
|
_welcomeText.value = "Welcome\n$userName"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkOutstandingBills() {
|
||||||
|
Log.d(TAG, "Starting bills check")
|
||||||
|
val cookie = secureStorage.getCookie()
|
||||||
|
Log.d(TAG, "Retrieved cookie: ${cookie?.take(20)}...") // Only log first 20 chars for security
|
||||||
|
|
||||||
|
if (cookie == null) {
|
||||||
|
Log.d(TAG, "No cookie found, cannot check bills")
|
||||||
|
_billsStatus.value = "Unable to check bills status"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoading.value = true
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
Log.d(TAG, "Making API call to check outstanding bills")
|
||||||
|
when (val result = apiService.getOutstandingBills("connect.sid=$cookie")) {
|
||||||
|
is ApiResult.Success -> {
|
||||||
|
Log.d(TAG, "Bills check successful: ${result.data.size} bills found")
|
||||||
|
_isLoading.value = false
|
||||||
|
if (result.data.isEmpty()) {
|
||||||
|
_billsStatus.value = "You don't have any pending bills left"
|
||||||
|
} else {
|
||||||
|
_billsStatus.value = "You have ${result.data.size} pending bills"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ApiResult.Error -> {
|
||||||
|
Log.d(TAG, "Bills check failed: ${result.message}")
|
||||||
|
_isLoading.value = false
|
||||||
|
_billsStatus.value = "Unable to check bills status"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val text: LiveData<String> = _text
|
|
||||||
}
|
}
|
84
app/src/main/java/sh/sar/gridflow/utils/SecureStorage.kt
Normal file
84
app/src/main/java/sh/sar/gridflow/utils/SecureStorage.kt
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package sh.sar.gridflow.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
|
import androidx.security.crypto.MasterKey
|
||||||
|
|
||||||
|
class SecureStorage(context: Context) {
|
||||||
|
|
||||||
|
private val sharedPreferences: SharedPreferences
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "SecureStorage"
|
||||||
|
private const val PREFS_NAME = "gridflow_secure_prefs"
|
||||||
|
private const val KEY_MOBILE = "mobile"
|
||||||
|
private const val KEY_PASSWORD = "password"
|
||||||
|
private const val KEY_COOKIE = "cookie"
|
||||||
|
private const val KEY_USER_NAME = "user_name"
|
||||||
|
private const val KEY_USER_EMAIL = "user_email"
|
||||||
|
private const val KEY_USER_ID = "user_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
Log.d(TAG, "Initializing SecureStorage")
|
||||||
|
try {
|
||||||
|
val masterKey = MasterKey.Builder(context)
|
||||||
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
sharedPreferences = EncryptedSharedPreferences.create(
|
||||||
|
context,
|
||||||
|
PREFS_NAME,
|
||||||
|
masterKey,
|
||||||
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||||
|
)
|
||||||
|
Log.d(TAG, "SecureStorage initialized successfully")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to initialize SecureStorage", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveCredentials(mobile: String, password: String) {
|
||||||
|
Log.d(TAG, "Saving credentials for mobile: $mobile")
|
||||||
|
sharedPreferences.edit()
|
||||||
|
.putString(KEY_MOBILE, mobile)
|
||||||
|
.putString(KEY_PASSWORD, password)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveCookie(cookie: String) {
|
||||||
|
Log.d(TAG, "Saving cookie: $cookie")
|
||||||
|
sharedPreferences.edit()
|
||||||
|
.putString(KEY_COOKIE, cookie)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveUserInfo(name: String, email: String, userId: Int) {
|
||||||
|
Log.d(TAG, "Saving user info: name=$name, email=$email, userId=$userId")
|
||||||
|
sharedPreferences.edit()
|
||||||
|
.putString(KEY_USER_NAME, name)
|
||||||
|
.putString(KEY_USER_EMAIL, email)
|
||||||
|
.putInt(KEY_USER_ID, userId)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMobile(): String? = sharedPreferences.getString(KEY_MOBILE, null)
|
||||||
|
fun getPassword(): String? = sharedPreferences.getString(KEY_PASSWORD, null)
|
||||||
|
fun getCookie(): String? = sharedPreferences.getString(KEY_COOKIE, null)
|
||||||
|
fun getUserName(): String? = sharedPreferences.getString(KEY_USER_NAME, null)
|
||||||
|
fun getUserEmail(): String? = sharedPreferences.getString(KEY_USER_EMAIL, null)
|
||||||
|
fun getUserId(): Int = sharedPreferences.getInt(KEY_USER_ID, -1)
|
||||||
|
|
||||||
|
fun isLoggedIn(): Boolean {
|
||||||
|
return getCookie() != null && getUserName() != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearCredentials() {
|
||||||
|
Log.d(TAG, "Clearing all credentials")
|
||||||
|
sharedPreferences.edit().clear().apply()
|
||||||
|
}
|
||||||
|
}
|
@@ -1,22 +1,79 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="true"
|
||||||
tools:context=".ui.home.HomeFragment">
|
tools:context=".ui.home.HomeFragment">
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/text_home"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:orientation="vertical"
|
||||||
android:layout_marginTop="8dp"
|
android:padding="20dp">
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:textAlignment="center"
|
<!-- Welcome Section -->
|
||||||
android:textSize="20sp"
|
<TextView
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:id="@+id/text_home"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:layout_width="match_parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
android:layout_marginBottom="24dp"
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:lineSpacingExtra="4dp"
|
||||||
|
tools:text="Welcome\nShihaam Abdul Rahman" />
|
||||||
|
|
||||||
|
<!-- Bills Status Card -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/card_bills_status"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
app:cardCornerRadius="12dp"
|
||||||
|
app:cardElevation="2dp"
|
||||||
|
app:cardBackgroundColor="#E8F5E8">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_bills_status"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="#2E7D32"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="You don't have any pending bills left" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_bills"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:indeterminateTint="#2E7D32" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- Placeholder for future features -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="More features coming soon..."
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:alpha="0.6"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginTop="32dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
6
app/src/main/res/xml/network_security_config.xml
Normal file
6
app/src/main/res/xml/network_security_config.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<domain-config cleartextTrafficPermitted="true">
|
||||||
|
<domain includeSubdomains="true">api.fenaka.mv</domain>
|
||||||
|
</domain-config>
|
||||||
|
</network-security-config>
|
@@ -12,6 +12,9 @@ lifecycleLivedataKtx = "2.6.1"
|
|||||||
lifecycleViewmodelKtx = "2.6.1"
|
lifecycleViewmodelKtx = "2.6.1"
|
||||||
navigationFragmentKtx = "2.6.0"
|
navigationFragmentKtx = "2.6.0"
|
||||||
navigationUiKtx = "2.6.0"
|
navigationUiKtx = "2.6.0"
|
||||||
|
okhttp = "4.12.0"
|
||||||
|
gson = "2.10.1"
|
||||||
|
security = "1.1.0-alpha06"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -25,6 +28,10 @@ androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecy
|
|||||||
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
|
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
|
||||||
androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
|
androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
|
||||||
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" }
|
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" }
|
||||||
|
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||||
|
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
|
||||||
|
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
|
||||||
|
security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "security" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
Reference in New Issue
Block a user