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
10 changes: 10 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.hilt.android)
alias(libs.plugins.ksp)
}

val localProperties = Properties().apply {
Expand Down Expand Up @@ -72,6 +75,7 @@ android {
dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
Expand All @@ -85,6 +89,12 @@ dependencies {
implementation(libs.androidx.credentials.play.services.auth)
implementation(libs.googleid)
implementation(libs.kakao.user)
implementation(libs.kotlinx.serialization.json)
implementation(libs.retrofit)
implementation(libs.retrofit.converter.gson)
implementation(libs.hilt.android)
implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
ksp(libs.hilt.compiler)
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(platform(libs.androidx.compose.bom))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.it_da.data.local

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class DefaultAuthTokenLocalDataSourceTest {
private val localDataSource = DefaultAuthTokenLocalDataSource(
InstrumentationRegistry.getInstrumentation().targetContext
)

@Before
fun clearTokenBeforeTest() = runBlocking {
localDataSource.clearToken()
}

@After
fun clearTokenAfterTest() = runBlocking {
localDataSource.clearToken()
}

@Test
fun savesLoadsAndClearsTokenInPreferencesDataStore() = runBlocking {
localDataSource.saveToken("stored-token")

assertEquals("stored-token", localDataSource.getToken())

localDataSource.clearToken()

assertNull(localDataSource.getToken())
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/com/example/it_da/ItdaApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package com.example.it_da

import android.app.Application
import com.kakao.sdk.common.KakaoSdk
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class ItdaApplication : Application() {
// Initializes Kakao SDK once when the app process starts if a native app key is configured.
override fun onCreate() {
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/example/it_da/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import com.example.it_da.ui.ItdaApp
import com.example.it_da.ui.theme.ITDATheme
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.example.it_da.data.auth

import com.example.it_da.domain.model.SocialAuthAccount
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

class SocialAuthSessionStore {
@Singleton
class SocialAuthSessionStore @Inject constructor() {
private val _currentAccount = MutableStateFlow<SocialAuthAccount?>(null)
val currentAccount = _currentAccount.asStateFlow()

Expand All @@ -17,8 +20,4 @@ class SocialAuthSessionStore {
fun clear() {
_currentAccount.value = null
}

companion object {
val default = SocialAuthSessionStore()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.it_da.data.local

interface AuthTokenLocalDataSource {
// Loads the locally stored authentication token used by the launch session check.
suspend fun getToken(): String?

// Persists the authentication token so the next app launch can restore the session.
suspend fun saveToken(token: String)

// Removes the locally stored authentication token when the session should end.
suspend fun clearToken()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.example.it_da.data.local

import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.first

private const val AuthSessionDataStoreName = "auth_session"

private val Context.authSessionDataStore by preferencesDataStore(
name = AuthSessionDataStoreName
)

@Singleton
class DefaultAuthTokenLocalDataSource @Inject constructor(
@param:ApplicationContext private val context: Context
) : AuthTokenLocalDataSource {
// Loads the token from Preferences DataStore for the app launch session decision.
override suspend fun getToken(): String? {
return context.authSessionDataStore.data.first()[AuthTokenKey]
}

// Stores the token in Preferences DataStore after a temporary authentication success.
override suspend fun saveToken(token: String) {
context.authSessionDataStore.edit { preferences ->
preferences[AuthTokenKey] = token
}
}

// Deletes the token from Preferences DataStore for a future logout flow.
override suspend fun clearToken() {
context.authSessionDataStore.edit { preferences ->
preferences.remove(AuthTokenKey)
}
}

private companion object {
val AuthTokenKey = stringPreferencesKey("auth_token")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.it_da.data.repository

interface AuthSessionRepository {
// Checks whether a non-empty local token is available for automatic login.
suspend fun hasStoredToken(): Result<Boolean>

// Saves a temporary token until the server authentication contract is connected.
suspend fun saveTemporaryToken(): Result<Unit>

// Clears the stored token for the future logout flow.
suspend fun clearToken(): Result<Unit>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example.it_da.data.repository

import com.example.it_da.data.local.AuthTokenLocalDataSource
import javax.inject.Inject
import javax.inject.Singleton

private const val TemporaryAuthToken = "temporary-auth-token"

@Singleton
class DefaultAuthSessionRepository @Inject constructor(
private val authTokenLocalDataSource: AuthTokenLocalDataSource
) : AuthSessionRepository {
// Treats any non-empty local token as an authenticated session until server validation is connected.
override suspend fun hasStoredToken(): Result<Boolean> {
return runCatching {
!authTokenLocalDataSource.getToken().isNullOrBlank()
}
}

// Persists a placeholder token so the pre-server authentication flow can be verified end to end.
override suspend fun saveTemporaryToken(): Result<Unit> {
return runCatching {
authTokenLocalDataSource.saveToken(TemporaryAuthToken)
}
}

// Removes the current token behind the repository boundary for the future logout UI.
override suspend fun clearToken(): Result<Unit> {
return runCatching {
authTokenLocalDataSource.clearToken()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import android.content.Context
import com.example.it_da.data.auth.SocialAuthClient
import com.example.it_da.domain.model.SocialAuthAccount
import com.example.it_da.domain.model.SocialAuthProvider
import javax.inject.Inject
import kotlin.jvm.JvmSuppressWildcards

class DefaultSocialAuthRepository(
private val socialAuthClients: List<SocialAuthClient>
class DefaultSocialAuthRepository @Inject constructor(
private val socialAuthClients: List<@JvmSuppressWildcards SocialAuthClient>
) : SocialAuthRepository {
// Delegates authentication to the SDK client that owns the requested provider.
override suspend fun authenticate(
Expand Down
Loading