Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9a3ecd7
[feat/#148] refresh 만료시 로그인 화면으로 이동
hwidung Aug 2, 2025
119897a
[refactor/#148] 투데이 상세 시간 포맷 분 단위 표시
hwidung Aug 4, 2025
67a3996
[feat/#148] 투데이 시간 포맷 표시 처리
hwidung Aug 5, 2025
aec2d22
[refactor/#148] 리프레시 토큰 만료시 사용되는 global scope를 application scope로 변경
hwidung Aug 5, 2025
8d11839
[chore/#148] 불필요한 로그 삭제
hwidung Aug 5, 2025
8b4476c
Merge remote-tracking branch 'origin/develop' into fix/#148-token-and…
hwidung Aug 18, 2025
81ce86e
[refactor/#148] 기존 global event 로직 삭제
hwidung Aug 23, 2025
aed5559
[refactor/#148] 로그아웃 관련 event type 추가
hwidung Aug 23, 2025
01d9f00
[refactor/#148] 메인 네비게이션으로 부터 Onboarding Navigation 분리
hwidung Aug 23, 2025
4eeb5ec
[refactor/#148] 로그아웃 로직 인터셉터가 감지하도록 로직 추가 및 인터셉터 리팩토링
hwidung Aug 23, 2025
6b4fa40
[refactor/#148] 토큰 저장소 로그인 성공 여부 및 온보딩 필요 여부 감지 세팅 추가
hwidung Aug 23, 2025
52a314a
[refactor/#148] 로그인 성공 시, 기존 메인 네비게이션 이동 로직을 변수 트리거로 변경
hwidung Aug 23, 2025
1d8fd2b
[refactor/#148] 온보딩 시, 기존 네비게이션 이동 로직을 변수 트리거로 변경
hwidung Aug 23, 2025
5b55016
[refactor/#148] 세팅 화면에서, 기존 네비게이션 이동 로직을 이벤트 버스 변수 트리거로 변경
hwidung Aug 23, 2025
5e95cce
[refactor/#148] 메인에서 이벤트 버스 감지 로직 추가 및 분기에 따른 로직 처리 추가
hwidung Aug 23, 2025
1ee1f40
[refactor/#148] 중복 코드 제거
hwidung Aug 23, 2025
a2dd282
[refactor/#148] SharedPreferences extension 함수로 축약
hwidung Aug 25, 2025
c2b1d80
[refactor/#148] 세션 삭제, 정보 삭제 분기처리 가능하도록 SharedPreferences 관리 함수 추가
hwidung Aug 25, 2025
8e04c9d
[refactor/#148] 로그아웃 시나리오에 대한 각 뷰모델에서의 이벤트 발생 시키는 로직
hwidung Aug 25, 2025
54245a6
[refactor/#148] 감지된 로그아웃 이벤트 타입을 기반으로 분기처리 함수 호출
hwidung Aug 25, 2025
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
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package org.memento.data.datastore

import kotlinx.coroutines.flow.Flow

interface TokenDataStore {
var accessToken: String
var refreshToken: String
var isNewUser: Boolean
var userEmail: String
var loginSuccess: Boolean
val loginSuccessFlow: Flow<Boolean>
var onboardingCompleted: Boolean
val onboardingCompletedFlow: Flow<Boolean>

fun clearSession()

fun clearInfo()
fun clearAllInfo()
}
54 changes: 46 additions & 8 deletions app/src/main/java/org/memento/data/datastore/TokenDataStoreImpl.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package org.memento.data.datastore

import android.content.SharedPreferences
import androidx.datastore.preferences.core.edit
import androidx.core.content.edit
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import javax.inject.Inject

class TokenDataStoreImpl
Expand All @@ -15,7 +18,7 @@ class TokenDataStoreImpl
return token
}
set(value) {
sharedPreferences.edit().putString(ACCESS_TOKEN, value).apply()
sharedPreferences.edit { putString(ACCESS_TOKEN, value) }
}

override var refreshToken: String
Expand All @@ -24,7 +27,7 @@ class TokenDataStoreImpl
return refreshToken
}
set(value) {
sharedPreferences.edit().putString(REFRESH_TOKEN, value).apply()
sharedPreferences.edit { putString(REFRESH_TOKEN, value) }
}

override var isNewUser: Boolean
Expand All @@ -33,7 +36,7 @@ class TokenDataStoreImpl
return isNewUser
}
set(value) {
sharedPreferences.edit().putBoolean(IS_NEW_USER, value).apply()
sharedPreferences.edit { putBoolean(IS_NEW_USER, value) }
}

override var userEmail: String
Expand All @@ -42,24 +45,59 @@ class TokenDataStoreImpl
return userEmail
}
set(value) {
sharedPreferences.edit().putString(USER_EMAIL, value).apply()
sharedPreferences.edit { putString(USER_EMAIL, value) }
}

override var loginSuccess: Boolean
get() = sharedPreferences.getBoolean(LOGIN_SUCCESS, false)
set(value) {
sharedPreferences.edit().putBoolean(LOGIN_SUCCESS, value).apply()
sharedPreferences.edit { putBoolean(LOGIN_SUCCESS, value) }
}

override fun clearInfo() {
sharedPreferences.edit().clear().apply()
override val loginSuccessFlow: Flow<Boolean> = getBooleanFlow(LOGIN_SUCCESS, false)

override var onboardingCompleted: Boolean
get() = sharedPreferences.getBoolean(ONBOARDING_COMPLETED, false)
set(value) {
sharedPreferences.edit { putBoolean(ONBOARDING_COMPLETED, value) }
}

override val onboardingCompletedFlow: Flow<Boolean> = getBooleanFlow(ONBOARDING_COMPLETED, false)

override fun clearSession() {
sharedPreferences.edit {
remove(ACCESS_TOKEN)
remove(REFRESH_TOKEN)
putBoolean(LOGIN_SUCCESS, false)
}
}

override fun clearAllInfo() {
sharedPreferences.edit { clear() }
}

private fun getBooleanFlow(
key: String,
defaultValue: Boolean,
): Flow<Boolean> =
callbackFlow {
val listener =
SharedPreferences.OnSharedPreferenceChangeListener { _, changedKey ->
if (changedKey == key || changedKey == null) {
trySend(sharedPreferences.getBoolean(key, defaultValue))
}
}
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
trySend(sharedPreferences.getBoolean(key, defaultValue))
awaitClose { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) }
}

companion object {
private const val ACCESS_TOKEN = "ACCESS_TOKEN"
private const val REFRESH_TOKEN = "REFRESH_TOKEN"
private const val IS_NEW_USER = "IS_NEW_USER"
private const val USER_EMAIL = "USER_EMAIL"
private const val LOGIN_SUCCESS = "LOGIN_SUCCESS"
private const val ONBOARDING_COMPLETED = "ONBOARDING_COMPLETED"
}
}
93 changes: 53 additions & 40 deletions app/src/main/java/org/memento/data/util/Interceptor.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.memento.data.util

import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
Expand All @@ -8,16 +9,19 @@ import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okio.IOException
import org.memento.BuildConfig
import org.memento.core.event.EventBus
import org.memento.data.datastore.TokenDataStore
import org.memento.data.dto.BaseResponse
import org.memento.data.dto.response.ResponseRefreshDto
import org.memento.presentation.type.EventType
import timber.log.Timber
import javax.inject.Inject

class Interceptor
@Inject
constructor(
private val json: Json,
private val eventBus: EventBus,
private val tokenDataStore: TokenDataStore,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
Expand All @@ -27,66 +31,75 @@ class Interceptor
if (!tokenDataStore.isNewUser) {
originalRequest.newAuthBuilder()
} else {
Timber.d("❌ No access token found.")
originalRequest
}

val response = chain.proceed(authRequest)

when (response.code) {
TOKEN_EXPIRED_CODE -> {
Timber.d("⏳ Token expired, trying to refresh token.")
if (response.code == TOKEN_EXPIRED_CODE) {
response.close()

val refreshTokenRequest =
originalRequest.newBuilder()
.url("${BuildConfig.BASE_URL}api/v1/auth/token/refresh")
.post("{}".toRequestBody("application/json".toMediaType()))
.addHeader(AUTHORIZATION, "$BEARER ${tokenDataStore.refreshToken}")
.build()
synchronized(this) {
val isRefreshSuccessful = refreshToken(chain)

response.close()

val refreshTokenResponse = chain.proceed(refreshTokenRequest)

val responseBodyString = refreshTokenResponse.peekBody(Long.MAX_VALUE).string()

if (refreshTokenResponse.isSuccessful) {
Timber.d("✅ Token refresh successful.")
if (isRefreshSuccessful) {
return chain.proceed(originalRequest.newAuthBuilder())
}
}
throw IOException("Session expired. Please log in again.")
}
return response
}

val responseRefresh = json.decodeFromString<BaseResponse<ResponseRefreshDto>>(responseBodyString)
private fun refreshToken(chain: Interceptor.Chain): Boolean {
Timber.d("⏳ Token expired, trying to refresh token.")

responseRefresh.data?.let {
with(tokenDataStore) {
accessToken = responseRefresh.data.accessToken
refreshToken = responseRefresh.data.refreshToken
}
}
val currentRefreshToken = tokenDataStore.refreshToken
if (currentRefreshToken.isBlank()) {
handleSessionExpired()
return false
}

Timber.d("🎉 Updated accessToken")
val refreshTokenRequest =
chain.request().newBuilder()
.url("${BuildConfig.BASE_URL}api/v1/auth/token/refresh")
.post("{}".toRequestBody("application/json".toMediaType()))
.addHeader(AUTHORIZATION, "$BEARER ${tokenDataStore.refreshToken}")
.build()

refreshTokenResponse.close()
val refreshTokenResponse = chain.proceed(refreshTokenRequest)

val newRequest = originalRequest.newAuthBuilder()
Timber.d("🚀 Sending new request with updated access token.")
if (refreshTokenResponse.isSuccessful) {
Timber.d("✅ Token refresh successful.")
val responseBodyString = refreshTokenResponse.peekBody(Long.MAX_VALUE).string()
val responseRefresh = json.decodeFromString<BaseResponse<ResponseRefreshDto>>(responseBodyString)

return chain.proceed(newRequest)
} else {
Timber.d("❌ Failed to refresh token, response: ${refreshTokenResponse.code}")
Timber.d("❌ Error body: $responseBodyString")
refreshTokenResponse.close()
throw IOException("Failed to refresh token")
responseRefresh.data?.let {
with(tokenDataStore) {
accessToken = responseRefresh.data.accessToken
refreshToken = responseRefresh.data.refreshToken
}
}
Timber.d("🎉 Updated accessToken")

refreshTokenResponse.close()
return true
} else {
Timber.d("❌ Failed to refresh token, response: ${refreshTokenResponse.code}")
refreshTokenResponse.close()
handleSessionExpired()
return false
}
return response
}

private fun Request.newAuthBuilder() = this.newBuilder().addHeader(AUTHORIZATION, "$BEARER ${tokenDataStore.accessToken}").build()

private fun clearUserInfo() {
tokenDataStore.clearInfo()
private fun handleSessionExpired() {
runBlocking {
eventBus.emit(EventType.TokenExpired)
}
}

private fun Request.newAuthBuilder() = this.newBuilder().addHeader(AUTHORIZATION, "$BEARER ${tokenDataStore.accessToken}").build()

companion object {
private const val TOKEN_EXPIRED_CODE = 401
private const val BEARER = "Bearer"
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/org/memento/di/NetworkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.memento.BuildConfig
import org.memento.core.event.EventBus
import org.memento.data.datastore.TokenDataStore
import org.memento.data.util.Interceptor
import retrofit2.Retrofit
Expand All @@ -36,9 +37,10 @@ object NetworkModule {
@Singleton
fun provideInterceptor(
json: Json,
eventBus: EventBus,
tokenDataStore: TokenDataStore,
): Interceptor {
return Interceptor(json, tokenDataStore)
return Interceptor(json, eventBus, tokenDataStore)
}

@Singleton
Expand Down
Loading