login works
This commit is contained in:
@@ -48,6 +48,10 @@ dependencies {
|
||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.androidx.navigation.fragment.ktx)
|
||||
implementation(libs.androidx.navigation.ui.ktx)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.okhttp.logging)
|
||||
implementation(libs.gson)
|
||||
implementation(libs.security.crypto)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
@@ -2,6 +2,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
@@ -10,7 +12,8 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.GridFlow">
|
||||
android:theme="@style/Theme.GridFlow"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
<activity
|
||||
android:name=".LoginActivity"
|
||||
android:exported="true"
|
||||
@@ -22,6 +25,11 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".DebugLoginActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.GridFlow.NoActionBar" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
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.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
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
|
||||
import sh.sar.gridflow.utils.SecureStorage
|
||||
|
||||
class LoginActivity : AppCompatActivity() {
|
||||
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Log.d(TAG, "LoginActivity onCreate")
|
||||
|
||||
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -41,17 +79,91 @@ class LoginActivity : AppCompatActivity() {
|
||||
val mobileNumber = binding.etMobileNumber.text.toString().trim()
|
||||
val password = binding.etPassword.text.toString().trim()
|
||||
|
||||
Log.d(TAG, "handleSignIn called with mobile: $mobileNumber")
|
||||
|
||||
if (validateInput(mobileNumber, password)) {
|
||||
// TODO: Implement actual authentication logic
|
||||
// For now, just navigate to main activity
|
||||
Toast.makeText(this, "Signing in...", Toast.LENGTH_SHORT).show()
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
Log.d(TAG, "Input validation passed, calling performLogin")
|
||||
performLogin(mobileNumber, password)
|
||||
} else {
|
||||
Log.d(TAG, "Input validation failed")
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
Toast.makeText(this, "Register functionality coming soon", Toast.LENGTH_SHORT).show()
|
||||
// TODO: Navigate to registration screen
|
||||
@@ -78,6 +190,12 @@ class LoginActivity : AppCompatActivity() {
|
||||
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)
|
||||
if (!mobileNumber.matches(Regex("^[79]\\d{6}$"))) {
|
||||
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?
|
||||
): View {
|
||||
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)
|
||||
val root: View = binding.root
|
||||
|
||||
val textView: TextView = binding.textHome
|
||||
homeViewModel.text.observe(viewLifecycleOwner) {
|
||||
textView.text = it
|
||||
val welcomeTextView: TextView = binding.textHome
|
||||
val billsStatusTextView: TextView = binding.textBillsStatus
|
||||
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
|
||||
}
|
||||
|
||||
|
@@ -1,13 +1,77 @@
|
||||
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.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 {
|
||||
value = "This is home Fragment"
|
||||
private val secureStorage = SecureStorage(application)
|
||||
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"?>
|
||||
<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:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
tools:context=".ui.home.HomeFragment">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_home"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:textAlignment="center"
|
||||
android:textSize="20sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<!-- Welcome Section -->
|
||||
<TextView
|
||||
android:id="@+id/text_home"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
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"
|
||||
navigationFragmentKtx = "2.6.0"
|
||||
navigationUiKtx = "2.6.0"
|
||||
okhttp = "4.12.0"
|
||||
gson = "2.10.1"
|
||||
security = "1.1.0-alpha06"
|
||||
|
||||
[libraries]
|
||||
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-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" }
|
||||
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]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
Reference in New Issue
Block a user