From cf4e2307b5cbe7ad96f7d386ad5f968eb0a62b89 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Thu, 24 Jul 2025 16:25:58 +0500 Subject: [PATCH] login works --- app/build.gradle.kts | 4 + app/src/main/AndroidManifest.xml | 10 +- .../sh/sar/gridflow/DebugLoginActivity.kt | 69 +++++++++ .../java/sh/sar/gridflow/LoginActivity.kt | 132 +++++++++++++++++- .../main/java/sh/sar/gridflow/data/Models.kt | 28 ++++ .../sar/gridflow/network/FenakaApiService.kt | 130 +++++++++++++++++ .../sh/sar/gridflow/ui/home/HomeFragment.kt | 26 +++- .../sh/sar/gridflow/ui/home/HomeViewModel.kt | 74 +++++++++- .../sh/sar/gridflow/utils/SecureStorage.kt | 84 +++++++++++ app/src/main/res/layout/fragment_home.xml | 83 +++++++++-- .../main/res/xml/network_security_config.xml | 6 + gradle/libs.versions.toml | 7 + 12 files changed, 623 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/sh/sar/gridflow/DebugLoginActivity.kt create mode 100644 app/src/main/java/sh/sar/gridflow/data/Models.kt create mode 100644 app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt create mode 100644 app/src/main/java/sh/sar/gridflow/utils/SecureStorage.kt create mode 100644 app/src/main/res/xml/network_security_config.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5b45422..66154f8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 871f62d..78bfe18 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + android:theme="@style/Theme.GridFlow" + android:networkSecurityConfig="@xml/network_security_config"> + { + 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 + } + } +} diff --git a/app/src/main/java/sh/sar/gridflow/LoginActivity.kt b/app/src/main/java/sh/sar/gridflow/LoginActivity.kt index 61c1ffb..4a34841 100644 --- a/app/src/main/java/sh/sar/gridflow/LoginActivity.kt +++ b/app/src/main/java/sh/sar/gridflow/LoginActivity.kt @@ -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" diff --git a/app/src/main/java/sh/sar/gridflow/data/Models.kt b/app/src/main/java/sh/sar/gridflow/data/Models.kt new file mode 100644 index 0000000..d2b3839 --- /dev/null +++ b/app/src/main/java/sh/sar/gridflow/data/Models.kt @@ -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? = null, + val error: String? = null +) + +data class ValidationError( + val value: String, + val msg: String, + val param: String, + val location: String +) diff --git a/app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt b/app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt new file mode 100644 index 0000000..8131157 --- /dev/null +++ b/app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt @@ -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 = 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> = 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::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 { + data class Success(val data: T, val cookie: String?) : ApiResult() + data class Error(val message: String, val code: Int) : ApiResult() +} diff --git a/app/src/main/java/sh/sar/gridflow/ui/home/HomeFragment.kt b/app/src/main/java/sh/sar/gridflow/ui/home/HomeFragment.kt index bf84aea..1ae36a5 100644 --- a/app/src/main/java/sh/sar/gridflow/ui/home/HomeFragment.kt +++ b/app/src/main/java/sh/sar/gridflow/ui/home/HomeFragment.kt @@ -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 } diff --git a/app/src/main/java/sh/sar/gridflow/ui/home/HomeViewModel.kt b/app/src/main/java/sh/sar/gridflow/ui/home/HomeViewModel.kt index 5be63ed..1fd1e51 100644 --- a/app/src/main/java/sh/sar/gridflow/ui/home/HomeViewModel.kt +++ b/app/src/main/java/sh/sar/gridflow/ui/home/HomeViewModel.kt @@ -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().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() + val welcomeText: LiveData = _welcomeText + + private val _billsStatus = MutableLiveData() + val billsStatus: LiveData = _billsStatus + + private val _isLoading = MutableLiveData() + val isLoading: LiveData = _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 = _text } \ No newline at end of file diff --git a/app/src/main/java/sh/sar/gridflow/utils/SecureStorage.kt b/app/src/main/java/sh/sar/gridflow/utils/SecureStorage.kt new file mode 100644 index 0000000..c680bf4 --- /dev/null +++ b/app/src/main/java/sh/sar/gridflow/utils/SecureStorage.kt @@ -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() + } +} diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index f3d9b08..6e3daae 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -1,22 +1,79 @@ - - - \ No newline at end of file + android:orientation="vertical" + android:padding="20dp"> + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..8a512f5 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,6 @@ + + + + api.fenaka.mv + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4fa3f2a..e3b8d95 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }