login works

This commit is contained in:
2025-07-24 16:25:58 +05:00
parent 039fcd690f
commit cf4e2307b5
12 changed files with 623 additions and 30 deletions

View File

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

View File

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

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

View File

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

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

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

View File

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

View File

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

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

View File

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

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

View File

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