Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions mobile/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
id("com.google.devtools.ksp")
}

android {
Expand Down Expand Up @@ -28,6 +29,11 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
buildConfigField("String", "BASE_URL", "\"https://api.smartjam.com/\"")
}

getByName("debug") {
buildConfigField("String", "BASE_URL", "\"http://10.0.2.2:8000/\"")
}
}
compileOptions {
Expand All @@ -36,10 +42,39 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
}

dependencies {

val nav_version = "2.9.7"
// Jetpack Compose integration
implementation("androidx.navigation:navigation-compose:$nav_version+")
Comment on lines +51 to +53
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Invalid version format: "$nav_version+" produces "2.9.7+".

Appending + to a complete version string (2.9.7+) is not a valid Maven version specifier. The + wildcard should replace a version segment (e.g., 2.9.+), not be appended to an already-specified version.

Either use the exact version or restructure the pattern:

🐛 Proposed fix
-    val nav_version = "2.9.7"
-    // Jetpack Compose integration
-    implementation("androidx.navigation:navigation-compose:$nav_version+")
+    // Jetpack Compose integration
+    implementation("androidx.navigation:navigation-compose:2.9.7")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val nav_version = "2.9.7"
// Jetpack Compose integration
implementation("androidx.navigation:navigation-compose:$nav_version+")
// Jetpack Compose integration
implementation("androidx.navigation:navigation-compose:2.9.7")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/app/build.gradle.kts` around lines 44 - 46, The dependency version is
malformed: the implementation line uses "$nav_version+" which yields "2.9.7+"
(invalid). Fix by removing the trailing "+" from the implementation usage so it
uses the exact nav_version value, or if you intended a wildcard, change the
nav_version declaration to a segment wildcard like "2.9.+" and keep the
implementation as
implementation("androidx.navigation:navigation-compose:$nav_version"); update
the nav_version variable or the implementation string accordingly (refer to the
nav_version variable and the
implementation("androidx.navigation:navigation-compose:$nav_version+")
occurrence).


//network
implementation("com.squareup.retrofit2:retrofit:2.11.+")
implementation("com.squareup.okhttp3:okhttp:4.12.+")

//serialization
implementation("com.squareup.retrofit2:converter-gson:2.11.+")

//logging
implementation("com.squareup.okhttp3:logging-interceptor:4.12.+")

//database
implementation("androidx.datastore:datastore-preferences:1.1.+")

//coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.+")

//ne pon
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.+")
Comment on lines +56 to +72
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What are the latest stable versions of retrofit, okhttp, datastore-preferences, kotlinx-coroutines-android, and lifecycle-viewmodel-compose as of March 2026?

💡 Result:

As of March 10, 2026, the latest stable versions are:

  • Retrofit (com.squareup.retrofit2:retrofit): 3.0.0 [1]
  • OkHttp (com.squareup.okhttp3:okhttp): 5.3.2 [2]
  • DataStore Preferences (androidx.datastore:datastore-preferences): 1.2.0 [3]
  • kotlinx-coroutines-android (org.jetbrains.kotlinx:kotlinx-coroutines-android): 1.10.2 [4]
  • lifecycle-viewmodel-compose (androidx.lifecycle:lifecycle-viewmodel-compose): 2.10.0 [5]

Sources
[1] (mvnrepository.com)
[2] (square.github.io)
[3] (mvnrepository.com)
[4] (github.com)
[5] (developer.android.com)

Citations:


Avoid dynamic/floating versions (+) for reproducible builds.

Using + wildcards (e.g., 2.11.+, 4.12.+) makes builds non-reproducible and can unexpectedly introduce breaking changes or security vulnerabilities when a new version is published. Pin dependencies to exact versions for stability. Consider also evaluating newer major versions available (e.g., Retrofit 3.0.0, OkHttp 5.3.2) to benefit from latest features and security patches.

🔒 Proposed fix: pin versions to exact releases

Replace each dynamic version with an exact version number. For example:

-    implementation("com.squareup.retrofit2:retrofit:2.11.+")
+    implementation("com.squareup.retrofit2:retrofit:2.11.0")

Consult the latest stable releases on mvnrepository.com to ensure you're not pinning to outdated versions, especially for major version upgrades (Retrofit 3.x, OkHttp 5.x) that may offer significant improvements.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/app/build.gradle.kts` around lines 49 - 65, The build file uses
dynamic versions with '+' (e.g., dependency coordinates in build.gradle.kts like
"com.squareup.retrofit2:retrofit:2.11.+", "com.squareup.okhttp3:okhttp:4.12.+",
"com.squareup.retrofit2:converter-gson:2.11.+",
"com.squareup.okhttp3:logging-interceptor:4.12.+",
"androidx.datastore:datastore-preferences:1.1.+",
"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.+",
"androidx.lifecycle:lifecycle-viewmodel-compose:2.8.+") which makes builds
non-reproducible; replace each '+' wildcard with a specific, exact version
number (pin each artifact to a concrete release) by looking up the current
stable versions for Retrofit, OkHttp, converter-gson, logging-interceptor,
datastore-preferences, kotlinx-coroutines-android, and
lifecycle-viewmodel-compose and update the corresponding implementation(...)
lines accordingly, and optionally centralize those version numbers in a versions
object or Gradle extra properties for maintainability.

Comment on lines +71 to +72
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Clarify or remove unclear comment.

The comment //ne pon is unclear and appears to be shorthand or non-English. Replace it with a descriptive comment explaining what this dependency is for.

✏️ Suggested clarification
-    //ne pon
-    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.+")
+    // ViewModel integration for Jetpack Compose
+    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
//ne pon
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.+")
// ViewModel integration for Jetpack Compose
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/app/build.gradle.kts` around lines 64 - 65, The inline comment "//ne
pon" above the dependency
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.+") is
unclear—either replace it with a short descriptive comment stating that this
dependency provides Jetpack Lifecycle ViewModel integration for Compose (enables
using ViewModel with Compose lifecycle-aware state) or remove the comment if
redundant; update the comment near the implementation(...) line so readers
understand why the dependency is included.


implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
ksp("androidx.room:room-compiler:2.5.0")
Comment on lines +74 to +76
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n mobile/app/build.gradle.kts | sed -n '70,80p'

Repository: Satlykovs/SmartJam

Length of output: 530


🌐 Web query:

Room library version compatibility 2.6.1 2.5.0 compiler runtime

💡 Result:

Android Room versions 2.6.1 and 2.5.0 are both compatible with standard Android compiler (KAPT/annotationProcessor) and runtime setups. Room 2.6.1 (released Nov 29, 2023) introduces Kotlin code generation option via KSP (room.generateKotlin, available from 2.6.0), but supports both KAPT and KSP processors. No explicit incompatibilities between 2.6.1 and 2.5.0 for compiler/runtime. Key compatibility notes: - Both versions work with kapt("androidx.room:room-compiler:VERSION") or ksp("androidx.room:room-compiler:VERSION") for Kotlin projects. - For Room 2.6.1 with KSP, requires compatible KSP version matching Kotlin (e.g., Kotlin 1.9.21 with KSP 1.9.21-1.0.15; mismatches cause NoSuchMethodError as in KSP issue #1633). - Room Gradle Plugin (from 2.6.0+) requires AGP 8.1+ for schema config. - Runtime (room-runtime) has no version-specific restrictions; depends on androidx.sqlite etc., backward compatible. - Upgrade from 2.5.0 to 2.6.1: Potential KSP/Kotlin version alignment needed; some early 2.6.0 KSP issues fixed in 2.6.1. No official compatibility matrix found, but official docs confirm KSP support added in 2.6.0 without breaking prior setups. Use latest stable Kotlin/AGP/KSP for best results (as of 2026: Kotlin 2.x, AGP 8.x+, KSP 2.x).

Citations:


Align Room compiler version with runtime.

Line 76 uses room-compiler:2.5.0 while lines 74-75 use Room 2.6.1. While these versions are compatible, keeping compiler and runtime versions aligned is a best practice to avoid potential code generation issues.

Suggested fix
-    ksp("androidx.room:room-compiler:2.5.0")
+    ksp("androidx.room:room-compiler:2.6.1")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
ksp("androidx.room:room-compiler:2.5.0")
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/app/build.gradle.kts` around lines 74 - 76, The Room compiler
dependency version is out of sync: the build file adds
implementation("androidx.room:room-runtime:2.6.1") and
implementation("androidx.room:room-ktx:2.6.1") but uses
ksp("androidx.room:room-compiler:2.5.0"); update the room-compiler coordinate
used in the ksp(...) declaration to match the runtime/ktx version (2.6.1) so the
compiler and runtime are aligned (change the
ksp("androidx.room:room-compiler:2.5.0") entry to use 2.6.1).


implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
Expand All @@ -55,4 +90,5 @@ dependencies {
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)

}
72 changes: 43 additions & 29 deletions mobile/app/src/main/java/com/smartjam/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,57 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.smartjam.app.ui.theme.SmartJamTheme
import androidx.compose.ui.graphics.Color
import androidx.navigation.compose.rememberNavController
import androidx.room.Room
import com.smartjam.app.data.api.AuthApi
import com.smartjam.app.data.api.NetworkModule
import com.smartjam.app.data.api.SmartJamApi
import com.smartjam.app.data.local.SmartJamDatabase
import com.smartjam.app.data.local.TokenStorage
import com.smartjam.app.domain.repository.AuthRepository
import com.smartjam.app.domain.repository.ConnectionRepository
import com.smartjam.app.ui.navigation.SmartJamNavGraph
import kotlin.jvm.java

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()

val tokenStorage = TokenStorage(context = this)

val appDatabase = Room.databaseBuilder(
applicationContext,
SmartJamDatabase::class.java,
"smartjam_database"
).build()


val retrofit = NetworkModule.createRetrofit(tokenStorage)
val smartJamApi = retrofit.create(SmartJamApi::class.java)
val authApi = retrofit.create(AuthApi::class.java)

val authRepository = AuthRepository(tokenStorage, authApi)
val connectionRepository = ConnectionRepository(smartJamApi, appDatabase.connectionDao())

setContent {
SmartJamTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
val navController = rememberNavController()

Surface(
modifier = Modifier.fillMaxSize(),
color = Color(0xFF05050A)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Extract hardcoded color to theme.

The background color Color(0xFF05050A) should be defined in your app's theme (e.g., MaterialTheme.colorScheme.background) for consistency and easier maintenance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/app/src/main/java/com/smartjam/app/MainActivity.kt` at line 31,
Replace the hardcoded Color(0xFF05050A) used in MainActivity (the color
assignment on the composable background) with your theme color (e.g.,
MaterialTheme.colorScheme.background) and define that hex value in your app
theme color scheme (e.g., in your Theme.kt where dark/light ColorScheme or
Background variable is declared) so the background uses the themed color instead
of a literal.

) {

SmartJamNavGraph(
navController = navController,
authRepository = authRepository,
connectionRepository = connectionRepository,
tokenStorage = tokenStorage
)
Comment on lines +28 to +57
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

App starts at Home screen without checking authentication state.

The navigation graph has startDestination = Screen.Home.route, but the app doesn't verify if the user has a valid token before navigating. An unauthenticated user would land on the Home screen without valid credentials.

Consider checking token presence at startup and navigating to Login if no valid session exists:

val startDestination = if (tokenStorage.getAccessToken() != null) {
    Screen.Home.route
} else {
    Screen.Login.route
}

Then pass startDestination to SmartJamNavGraph.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/app/src/main/java/com/smartjam/app/MainActivity.kt` around lines 22 -
36, The app currently always starts SmartJamNavGraph with startDestination =
Screen.Home.route which lets unauthenticated users land on Home; check
TokenStorage.getAccessToken() at startup and compute a startDestination variable
(use Screen.Home.route when token exists, otherwise Screen.Login.route) and pass
that startDestination into SmartJamNavGraph (update the call site where
SmartJamNavGraph is invoked to accept the computed startDestination and use
TokenStorage/ getAccessToken() to decide the initial route).

}
}
}
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
SmartJamTheme {
Greeting("Android")
}
}
20 changes: 20 additions & 0 deletions mobile/app/src/main/java/com/smartjam/app/data/api/AuthApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.smartjam.app.data.api

import com.smartjam.app.data.model.LoginRequest
import com.smartjam.app.data.model.LoginResponse
import com.smartjam.app.data.model.RefreshRequest
import com.smartjam.app.data.model.RegisterRequest
import retrofit2.http.Body
import retrofit2.http.POST

interface AuthApi {

@POST("/api/auth/register")
suspend fun register(@Body request: RegisterRequest): LoginResponse

@POST("/api/auth/login")
suspend fun login(@Body request: LoginRequest): LoginResponse

@POST("/api/auth/refresh")
suspend fun refresh(@Body request: RefreshRequest): LoginResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.smartjam.app.data.api

import com.smartjam.app.data.local.TokenStorage
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Response

class AuthInterceptor (
private val tokenStorage: TokenStorage
): Interceptor{
override fun intercept(chain: Interceptor.Chain): Response {
val token = runBlocking {
tokenStorage.accessToken.first()
}
val originalRequest = chain.request()

val requestBuilder = originalRequest.newBuilder()
if (token != null){
requestBuilder.addHeader("Authorization",token)
}

val newRequest = requestBuilder.build()
return chain.proceed(newRequest)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.smartjam.app.data.api

import com.smartjam.app.BuildConfig
import com.smartjam.app.data.local.TokenStorage
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit


object NetworkModule {

fun createRetrofit(tokenStorage: TokenStorage): Retrofit {
val authInterceptor = AuthInterceptor(tokenStorage)

val okHttpClient = OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.addInterceptor(loggingInterceptor)
.build()

return Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}

private val loggingInterceptor by lazy {
HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else{
HttpLoggingInterceptor.Level.NONE
}
}
}
Comment on lines +14 to +37
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's explore the repository structure
git ls-files | head -20

Repository: Satlykovs/SmartJam

Length of output: 938


🏁 Script executed:

# Find MainActivity to verify AuthApi creation
fd -t f "MainActivity.kt" mobile/

Repository: Satlykovs/SmartJam

Length of output: 119


🏁 Script executed:

# Find AuthApi interface to see what endpoints exist
fd -t f "AuthApi.kt" mobile/

Repository: Satlykovs/SmartJam

Length of output: 123


🏁 Script executed:

# Look for NetworkModule and related setup
fd -t f "NetworkModule.kt" mobile/

Repository: Satlykovs/SmartJam

Length of output: 129


🏁 Script executed:

cat -n mobile/app/src/main/java/com/smartjam/app/MainActivity.kt

Repository: Satlykovs/SmartJam

Length of output: 2628


🏁 Script executed:

cat -n mobile/app/src/main/java/com/smartjam/app/data/api/AuthApi.kt

Repository: Satlykovs/SmartJam

Length of output: 814


🏁 Script executed:

cat -n mobile/app/src/main/java/com/smartjam/app/data/api/NetworkModule.kt

Repository: Satlykovs/SmartJam

Length of output: 1445


Don't use BODY logging on the shared auth client.

MainActivity creates both AuthApi and SmartJamApi from the Retrofit returned here. Debug Level.BODY logging will dump login, register, and refresh request/response bodies—including credentials and refresh tokens—to logcat. Redacting the Authorization header is insufficient because the secrets are also embedded in the JSON payloads.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/app/src/main/java/com/smartjam/app/data/api/NetworkModule.kt` around
lines 14 - 37, The current shared loggingInterceptor uses
HttpLoggingInterceptor.Level.BODY which will log sensitive JSON payloads for
both AuthApi and SmartJamApi; change the shared logging level to a non-body
level (e.g., HttpLoggingInterceptor.Level.HEADERS or BASIC) so
createRetrofit(tokenStorage) no longer emits request/response bodies, and add a
new, dedicated factory (e.g., createAuthRetrofit or createAuthOkHttpClient) that
uses a separate HttpLoggingInterceptor with Level.BODY only when you explicitly
need it for non-production debug of non-shared clients; locate and update the
loggingInterceptor lazy val and the createRetrofit function to use the safer
shared logger and add the separate auth-specific factory so MainActivity uses
the appropriate client for AuthApi.


}
44 changes: 44 additions & 0 deletions mobile/app/src/main/java/com/smartjam/app/data/api/SmartJamApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.smartjam.app.data.api

import com.smartjam.app.data.model.*
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path

interface SmartJamApi {
@POST("/api/connections/invite-code")
suspend fun generateInviteCode(): Response<InviteCodeResponse>

@POST("/api/connections/join")
suspend fun joinByCode(@Body request: JoinRequest): Response<Unit>

@GET("/api/connections/pending")
suspend fun getPendingConnections(): Response<List<ConnectionDto>>

@GET("/api/connections/active")
suspend fun getActiveConnections(): Response<List<ConnectionDto>>

@POST("/api/connections/{connectionId}/respond")
suspend fun respondToConnection(
@Path("connectionId") connectionId: String,
@Body request: RespondConnectionRequest
): Response<Unit>

@POST("/api/assignments")
suspend fun createAssignment(
@Body request: CreateAssignmentRequest
): Response<PresignedUrlResponse>

@POST("/api/submissions")
suspend fun createSubmission(
@Body request: CreateSubmissionRequest
): Response<PresignedUrlResponse>

@GET("/api/submissions/{submissionId}/status")
suspend fun getSubmissionStatus(
@Path("submissionId") submissionId: String
): Response<SubmissionStatusResponse>

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.smartjam.app.data.local

import androidx.room.Database
import androidx.room.RoomDatabase
import com.smartjam.app.data.local.dao.ConnectionDao
import com.smartjam.app.data.local.entity.ConnectionEntity

@Database(entities = [ConnectionEntity::class], version = 1, exportSchema = false)
abstract class SmartJamDatabase : RoomDatabase() {
abstract fun connectionDao(): ConnectionDao
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.smartjam.app.data.local

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map

private val Context.dataStore : DataStore<Preferences> by preferencesDataStore( //TODO: make encrypted storage
name = "auth_preferences"
)
class TokenStorage(private val context: Context) {
private companion object Keys{
val ACCESS_TOKEN = stringPreferencesKey("access_token")
val REFRESH_TOKEN = stringPreferencesKey("refresh_token")

val ACCESS_EXPIRED_AT = longPreferencesKey("access_expires_at")

val REFRESH_EXPIRED_AT = longPreferencesKey("refresh_expired_at")
}

suspend fun saveToken(accessToken: String, refreshToken: String, accessExpiredAt: Long, refreshExpiredAt: Long){
context.dataStore.edit { preferences ->
preferences[ACCESS_TOKEN] = accessToken
preferences[REFRESH_TOKEN] = refreshToken
preferences[ACCESS_EXPIRED_AT] = accessExpiredAt
preferences[REFRESH_EXPIRED_AT] = refreshExpiredAt
}
}

val accessToken : Flow<String?> = context.dataStore.data
.map { preferences -> preferences[ACCESS_TOKEN] }

val refreshToken : Flow<String?> = context.dataStore.data
.map { preferences -> preferences[REFRESH_TOKEN] }

val accessExpiredAt : Flow<Long?> = context.dataStore.data
.map {preferences -> preferences[ACCESS_EXPIRED_AT]}

val refreshExpiredAt : Flow<Long?> = context.dataStore.data
.map {preferences -> preferences[REFRESH_EXPIRED_AT]}

suspend fun clearTokens(){
context.dataStore.edit { preferences ->
preferences.remove(ACCESS_TOKEN)
preferences.remove(REFRESH_TOKEN)
preferences.remove(ACCESS_EXPIRED_AT)
preferences.remove(REFRESH_EXPIRED_AT)
}
}

suspend fun isAccessTokenExpired(): Boolean {
val expires = context.dataStore.data
.map { preferences -> preferences[ACCESS_EXPIRED_AT] }
.first()
val currentTime = System.currentTimeMillis() / 1000
return (expires == null) || (currentTime > expires)

}

suspend fun isRefreshTokenExpired(): Boolean {
val expires = context.dataStore.data
.map { preferences -> preferences[REFRESH_EXPIRED_AT] }
.first()
val currentTime = System.currentTimeMillis() / 1000
return (expires == null) || (currentTime > expires)

}

suspend fun isAuthenticated(): Boolean {
return !isRefreshTokenExpired()
}

}
Loading