From ac63f9da9d948602836b68b56ced82b81d46c952 Mon Sep 17 00:00:00 2001 From: Ray Jang Date: Tue, 20 Feb 2024 19:24:04 +0900 Subject: [PATCH] [App] Check Invalid Token --- app/build.gradle.kts | 3 +- .../template/android/TemplateApplication.kt | 49 ++++++++++++++++++- .../MockAuthenticationRepository.kt | 27 ++++++++-- .../RealAuthenticationRepository.kt | 10 ++++ .../repository/AuthenticationRepository.kt | 3 ++ gradle/libs.versions.toml | 3 ++ presentation/src/main/AndroidManifest.xml | 4 ++ .../ui/invalid/InvalidJwtTokenActivity.kt | 44 +++++++++++++++++ .../ui/invalid/InvalidJwtTokenViewModel.kt | 8 +++ presentation/src/main/res/values/strings.xml | 2 + 10 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 presentation/src/main/kotlin/com/ray/template/android/presentation/ui/invalid/InvalidJwtTokenActivity.kt create mode 100644 presentation/src/main/kotlin/com/ray/template/android/presentation/ui/invalid/InvalidJwtTokenViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9cd67ca..bdd28d1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -103,7 +103,8 @@ dependencies { implementation(libs.hilt.android) ksp(libs.hilt.android.compiler) - implementation(libs.bundles.androidx.data) + implementation(libs.androidx.compose.lifecycle) + implementation(libs.androidx.lifecycle.process) implementation(libs.bundles.network) implementation(platform(libs.firebase.bom)) diff --git a/app/src/main/kotlin/com/ray/template/android/TemplateApplication.kt b/app/src/main/kotlin/com/ray/template/android/TemplateApplication.kt index 739c786..a355e01 100644 --- a/app/src/main/kotlin/com/ray/template/android/TemplateApplication.kt +++ b/app/src/main/kotlin/com/ray/template/android/TemplateApplication.kt @@ -1,16 +1,63 @@ package com.ray.template.android import android.app.Application +import android.content.Intent +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.ktx.Firebase +import com.ray.template.android.domain.repository.AuthenticationRepository +import com.ray.template.android.presentation.ui.invalid.InvalidJwtTokenActivity import dagger.hilt.android.HiltAndroidApp +import io.sentry.Sentry +import javax.inject.Inject +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.launch +import timber.log.Timber @HiltAndroidApp open class TemplateApplication : Application() { + + @Inject + lateinit var authenticationRepository: AuthenticationRepository + + private val handler = CoroutineExceptionHandler { _, exception -> + Timber.d(exception) + Sentry.captureException(exception) + Firebase.crashlytics.recordException(exception) + } + override fun onCreate() { super.onCreate() + initializeFirebase() + observeRefreshTokenValidation() + } - // Firebase Initialize + private fun initializeFirebase() { Firebase.analytics } + + private fun observeRefreshTokenValidation() { + with(ProcessLifecycleOwner.get()) { + lifecycleScope.launch(handler) { + repeatOnLifecycle(Lifecycle.State.STARTED) { + authenticationRepository.isRefreshTokenInvalid.collect { isRefreshTokenInvalid -> + if (isRefreshTokenInvalid) { + val intent = Intent( + this@TemplateApplication, + InvalidJwtTokenActivity::class.java + ).apply { + flags = + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + startActivity(intent) + } + } + } + } + } + } } diff --git a/data/src/main/kotlin/com/ray/template/android/data/repository/authentication/MockAuthenticationRepository.kt b/data/src/main/kotlin/com/ray/template/android/data/repository/authentication/MockAuthenticationRepository.kt index 220328a..c3184ca 100644 --- a/data/src/main/kotlin/com/ray/template/android/data/repository/authentication/MockAuthenticationRepository.kt +++ b/data/src/main/kotlin/com/ray/template/android/data/repository/authentication/MockAuthenticationRepository.kt @@ -6,6 +6,9 @@ import com.ray.template.android.domain.model.error.ServerException import com.ray.template.android.domain.repository.AuthenticationRepository import javax.inject.Inject import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow class MockAuthenticationRepository @Inject constructor( private val sharedPreferencesManager: SharedPreferencesManager @@ -23,18 +26,30 @@ class MockAuthenticationRepository @Inject constructor( set(value) = sharedPreferencesManager.setBoolean(IS_REGISTERED, value) get() = sharedPreferencesManager.getBoolean(IS_REGISTERED, false) + private val _isRefreshTokenInvalid: MutableStateFlow = MutableStateFlow(false) + override val isRefreshTokenInvalid: StateFlow = _isRefreshTokenInvalid.asStateFlow() + override suspend fun refreshToken( refreshToken: String ): Result { randomShortDelay() - return Result.success( - JwtToken( - accessToken = "mock_access_token", - refreshToken = "mock_refresh_token" + return if (refreshToken.isEmpty()) { + Result.failure( + ServerException("MOCK_ERROR", "refreshToken 이 만료되었습니다.") + ) + } else { + Result.success( + JwtToken( + accessToken = "mock_access_token", + refreshToken = "mock_refresh_token" + ) ) - ).onSuccess { token -> + }.onSuccess { token -> this.refreshToken = token.refreshToken this.accessToken = token.accessToken + _isRefreshTokenInvalid.value = false + }.onFailure { exception -> + _isRefreshTokenInvalid.value = true }.map { token -> JwtToken( accessToken = token.accessToken, @@ -86,6 +101,7 @@ class MockAuthenticationRepository @Inject constructor( }.onSuccess { this.refreshToken = "mock_access_token" this.accessToken = "mock_refresh_token" + isRegistered = true } } @@ -106,6 +122,7 @@ class MockAuthenticationRepository @Inject constructor( }.onSuccess { this.refreshToken = "" this.accessToken = "" + isRegistered = false } } diff --git a/data/src/main/kotlin/com/ray/template/android/data/repository/authentication/RealAuthenticationRepository.kt b/data/src/main/kotlin/com/ray/template/android/data/repository/authentication/RealAuthenticationRepository.kt index 67a3caf..4df8848 100644 --- a/data/src/main/kotlin/com/ray/template/android/data/repository/authentication/RealAuthenticationRepository.kt +++ b/data/src/main/kotlin/com/ray/template/android/data/repository/authentication/RealAuthenticationRepository.kt @@ -5,6 +5,9 @@ import com.ray.template.android.data.remote.network.api.AuthenticationApi import com.ray.template.android.domain.model.authentication.JwtToken import com.ray.template.android.domain.repository.AuthenticationRepository import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow class RealAuthenticationRepository @Inject constructor( private val authenticationApi: AuthenticationApi, @@ -18,6 +21,9 @@ class RealAuthenticationRepository @Inject constructor( set(value) = sharedPreferencesManager.setString(ACCESS_TOKEN, value) get() = sharedPreferencesManager.getString(ACCESS_TOKEN, "") + private val _isRefreshTokenInvalid: MutableStateFlow = MutableStateFlow(false) + override val isRefreshTokenInvalid: StateFlow = _isRefreshTokenInvalid.asStateFlow() + override suspend fun refreshToken( refreshToken: String ): Result { @@ -26,6 +32,10 @@ class RealAuthenticationRepository @Inject constructor( ).onSuccess { token -> this.refreshToken = token.refreshToken this.accessToken = token.accessToken + _isRefreshTokenInvalid.value = false + }.onFailure { exception -> + // TODO : ID 체크 + _isRefreshTokenInvalid.value = true }.map { token -> JwtToken( accessToken = token.accessToken, diff --git a/domain/src/main/kotlin/com/ray/template/android/domain/repository/AuthenticationRepository.kt b/domain/src/main/kotlin/com/ray/template/android/domain/repository/AuthenticationRepository.kt index 7c464f9..6afbf0e 100644 --- a/domain/src/main/kotlin/com/ray/template/android/domain/repository/AuthenticationRepository.kt +++ b/domain/src/main/kotlin/com/ray/template/android/domain/repository/AuthenticationRepository.kt @@ -1,6 +1,7 @@ package com.ray.template.android.domain.repository import com.ray.template.android.domain.model.authentication.JwtToken +import kotlinx.coroutines.flow.StateFlow interface AuthenticationRepository { @@ -8,6 +9,8 @@ interface AuthenticationRepository { var accessToken: String + val isRefreshTokenInvalid: StateFlow + suspend fun refreshToken( refreshToken: String ): Result diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c0d29cc..2cb1203 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,7 @@ kotlinx-datetime = "0.5.0" hilt = "2.50" hilt-compose = "1.1.0" # AndroidX +androidx-lifecycle-process = "2.7.0" androidx-core = "1.12.0" androidx-appcompat = "1.6.1" androidx-room = "2.6.1" @@ -60,6 +61,8 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version. hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } hilt-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hilt-compose" } +# AndroidX +androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "androidx-lifecycle-process" } # AndroidX Presentation androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } diff --git a/presentation/src/main/AndroidManifest.xml b/presentation/src/main/AndroidManifest.xml index 589f030..00416f9 100644 --- a/presentation/src/main/AndroidManifest.xml +++ b/presentation/src/main/AndroidManifest.xml @@ -11,5 +11,9 @@ + + diff --git a/presentation/src/main/kotlin/com/ray/template/android/presentation/ui/invalid/InvalidJwtTokenActivity.kt b/presentation/src/main/kotlin/com/ray/template/android/presentation/ui/invalid/InvalidJwtTokenActivity.kt new file mode 100644 index 0000000..7952560 --- /dev/null +++ b/presentation/src/main/kotlin/com/ray/template/android/presentation/ui/invalid/InvalidJwtTokenActivity.kt @@ -0,0 +1,44 @@ +package com.ray.template.android.presentation.ui.invalid + +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import com.ray.template.android.presentation.R +import com.ray.template.android.presentation.common.theme.TemplateTheme +import com.ray.template.android.presentation.common.view.DialogScreen +import com.ray.template.android.presentation.ui.main.MainActivity +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class InvalidJwtTokenActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + InvalidJwtTokenScreen() + } + } + + @Composable + fun InvalidJwtTokenScreen( + viewModel: InvalidJwtTokenViewModel = hiltViewModel() + ) { + val context = LocalContext.current + TemplateTheme { + DialogScreen( + title = stringResource(R.string.invalid_jwt_token_dialog_title), + message = stringResource(R.string.invalid_jwt_token_dialog_content), + onConfirm = { + val intent = Intent(context, MainActivity::class.java) + context.startActivity(intent) + finishAfterTransition() + }, + onDismissRequest = {} + ) + } + } +} diff --git a/presentation/src/main/kotlin/com/ray/template/android/presentation/ui/invalid/InvalidJwtTokenViewModel.kt b/presentation/src/main/kotlin/com/ray/template/android/presentation/ui/invalid/InvalidJwtTokenViewModel.kt new file mode 100644 index 0000000..4fbae4e --- /dev/null +++ b/presentation/src/main/kotlin/com/ray/template/android/presentation/ui/invalid/InvalidJwtTokenViewModel.kt @@ -0,0 +1,8 @@ +package com.ray.template.android.presentation.ui.invalid + +import com.ray.template.android.presentation.common.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class InvalidJwtTokenViewModel @Inject constructor() : BaseViewModel() diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 1d99d68..39c5ab0 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -11,4 +11,6 @@ 현재 서버와의 접속이 원활하지 않습니다.\n잠시 후 다시 진행해주세요. 다음 + 회원 오류 + 로그인이 만료되었습니다.\n다시 로그인해주세요.