diff --git a/core/common/src/main/java/com/teamwable/common/restarter/AppReStarter.kt b/core/common/src/main/java/com/teamwable/common/restarter/AppReStarter.kt new file mode 100644 index 000000000..1fe2f01ef --- /dev/null +++ b/core/common/src/main/java/com/teamwable/common/restarter/AppReStarter.kt @@ -0,0 +1,7 @@ +package com.teamwable.common.restarter + +interface AppReStarter { + fun restartApp() + + fun makeToast(message: String) +} diff --git a/core/common/src/main/java/com/teamwable/common/restarter/AppReStarterModule.kt b/core/common/src/main/java/com/teamwable/common/restarter/AppReStarterModule.kt new file mode 100644 index 000000000..a451a6a66 --- /dev/null +++ b/core/common/src/main/java/com/teamwable/common/restarter/AppReStarterModule.kt @@ -0,0 +1,17 @@ +package com.teamwable.common.restarter + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class AppReStarterModule { + @Binds + @Singleton + abstract fun bindsAppRestarter( + appRestarter: DefaultAppReStarter, + ): AppReStarter +} diff --git a/core/common/src/main/java/com/teamwable/common/restarter/DefaultAppReStarter.kt b/core/common/src/main/java/com/teamwable/common/restarter/DefaultAppReStarter.kt new file mode 100644 index 000000000..d6235862c --- /dev/null +++ b/core/common/src/main/java/com/teamwable/common/restarter/DefaultAppReStarter.kt @@ -0,0 +1,36 @@ +package com.teamwable.common.restarter + +import android.content.Context +import android.content.Intent +import android.widget.Toast +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.inject.Inject + +class DefaultAppReStarter @Inject constructor( + @ApplicationContext private val context: Context, +) : AppReStarter { + private var currentToast: Toast? = null + private val scope = CoroutineScope(Dispatchers.Main) + + override fun restartApp() { + scope.launch { + val restartIntent = context.packageManager + .getLaunchIntentForPackage(context.packageName) + ?.component + ?.let(Intent::makeRestartActivityTask) + + context.startActivity(restartIntent) + } + } + + override fun makeToast(message: String) { + scope.launch { + currentToast?.cancel() + currentToast = Toast.makeText(context, message, Toast.LENGTH_SHORT) + currentToast?.show() + } + } +} diff --git a/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultAuthRepository.kt b/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultAuthRepository.kt index e2995e75d..7c4258b1f 100644 --- a/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultAuthRepository.kt +++ b/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultAuthRepository.kt @@ -2,10 +2,10 @@ package com.teamwable.data.repositoryimpl import com.teamwable.data.mapper.toModel.toUserModel import com.teamwable.data.repository.AuthRepository -import com.teamwable.data.util.runHandledCatching import com.teamwable.model.auth.UserModel import com.teamwable.network.datasource.AuthService import com.teamwable.network.dto.request.RequestSocialLoginDto +import com.teamwable.network.util.runHandledCatching import javax.inject.Inject internal class DefaultAuthRepository @Inject constructor( diff --git a/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultCommunityRepository.kt b/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultCommunityRepository.kt index 6a8af0541..ae4b37af5 100644 --- a/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultCommunityRepository.kt +++ b/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultCommunityRepository.kt @@ -3,9 +3,9 @@ package com.teamwable.data.repositoryimpl import com.teamwable.data.mapper.toModel.toCommunityModel import com.teamwable.data.mapper.toModel.toRequestCommunityDto import com.teamwable.data.repository.CommunityRepository -import com.teamwable.data.util.runHandledCatching import com.teamwable.model.community.CommunityModel import com.teamwable.network.datasource.CommunityService +import com.teamwable.network.util.runHandledCatching import com.teamwable.network.util.toCustomError import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch diff --git a/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultFeedImageRepository.kt b/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultFeedImageRepository.kt index 1cf595f7a..2114cd111 100644 --- a/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultFeedImageRepository.kt +++ b/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultFeedImageRepository.kt @@ -4,7 +4,7 @@ import com.teamwable.data.gallery.BitmapFetcher import com.teamwable.data.gallery.GallerySaver import com.teamwable.data.gallery.GallerySaver.Companion.FILE_EXTENSION import com.teamwable.data.repository.FeedImageRepository -import com.teamwable.data.util.runHandledCatching +import com.teamwable.network.util.runHandledCatching import javax.inject.Inject internal class DefaultFeedImageRepository @Inject constructor( diff --git a/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultProfileRepository.kt b/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultProfileRepository.kt index 3f1979f67..06c79d115 100644 --- a/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultProfileRepository.kt +++ b/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultProfileRepository.kt @@ -7,13 +7,13 @@ import com.teamwable.data.mapper.toModel.toMemberDataModel import com.teamwable.data.mapper.toModel.toProfile import com.teamwable.data.repository.ProfileRepository import com.teamwable.data.util.createImagePart -import com.teamwable.data.util.runHandledCatching import com.teamwable.model.Profile import com.teamwable.model.profile.MemberDataModel import com.teamwable.model.profile.MemberInfoEditModel import com.teamwable.network.datasource.ProfileService import com.teamwable.network.dto.request.RequestWithdrawalDto import com.teamwable.network.util.handleThrowable +import com.teamwable.network.util.runHandledCatching import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index e6cf361e1..9fdf46575 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -32,5 +32,6 @@ android { dependencies { implementation(project(":core:model")) implementation(project(":core:datastore")) + implementation(project(":core:common")) implementation(libs.paging) } diff --git a/core/network/src/main/java/com/teamwable/network/TokenAuthenticator.kt b/core/network/src/main/java/com/teamwable/network/TokenAuthenticator.kt new file mode 100644 index 000000000..361805c02 --- /dev/null +++ b/core/network/src/main/java/com/teamwable/network/TokenAuthenticator.kt @@ -0,0 +1,65 @@ +package com.teamwable.network + +import com.teamwable.common.restarter.AppReStarter +import com.teamwable.datastore.datasource.WablePreferencesDataSource +import com.teamwable.network.datasource.AuthService +import com.teamwable.network.util.runSuspendCatching +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import okhttp3.Authenticator +import okhttp3.Request +import okhttp3.Response +import okhttp3.Route +import timber.log.Timber +import javax.inject.Inject + +class TokenAuthenticator @Inject constructor( + private val dataStore: WablePreferencesDataSource, + private val authService: AuthService, + private val appRestarter: AppReStarter, +) : Authenticator { + private val mutex = Mutex() + + override fun authenticate(route: Route?, response: Response): Request? { + if (response.code != 401) return null + + if (response.request.header("Authorization-Retry") != null) return null + + return runBlocking { + val newAccessToken = refreshToken() ?: return@runBlocking null + response.request + .newBuilder() + .header("Authorization", newAccessToken) + .header("Authorization-Retry", "true") + .build() + } + } + + private suspend fun refreshToken(): String? { + return mutex.withLock { + val accessToken = dataStore.accessToken.first() + val refreshToken = dataStore.refreshToken.first() + + runSuspendCatching { + authService.getReissueToken(accessToken, refreshToken) + }.onSuccess { + val newAccess = "Bearer ${it.data.accessToken}" + dataStore.updateAccessToken(newAccess) + dataStore.updateRefreshToken(it.data.refreshToken) + return newAccess + }.onFailure { + Timber.e(it) + dataStore.clear() + notifyReLoginRequired() + } + null + } + } + + private fun notifyReLoginRequired() { + appRestarter.makeToast("재 로그인이 필요해요") + appRestarter.restartApp() + } +} diff --git a/core/network/src/main/java/com/teamwable/network/TokenInterceptor.kt b/core/network/src/main/java/com/teamwable/network/TokenInterceptor.kt index 0b920ca72..6eb0e9650 100644 --- a/core/network/src/main/java/com/teamwable/network/TokenInterceptor.kt +++ b/core/network/src/main/java/com/teamwable/network/TokenInterceptor.kt @@ -1,112 +1,22 @@ package com.teamwable.network -import android.app.Application -import android.content.Intent -import android.widget.Toast import com.teamwable.datastore.datasource.WablePreferencesDataSource -import com.teamwable.network.datasource.AuthService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response import javax.inject.Inject -import javax.inject.Singleton -@Singleton class TokenInterceptor @Inject constructor( - private val wablePreferencesDataSource: WablePreferencesDataSource, - private val context: Application, - private val authService: AuthService, + private val dataStore: WablePreferencesDataSource, ) : Interceptor { - private val mutex = Mutex() - private var currentToast: Toast? = null - override fun intercept(chain: Interceptor.Chain): Response { - val originalRequest = chain.request() - var response = chain.proceed(originalRequest.newAuthBuilder()) - - if (response.code == CODE_TOKEN_EXPIRE) { - response.close() - val tokenRefreshed = runBlocking { - refreshTokenIfNeeded() - } - if (tokenRefreshed) { - response = chain.proceed(originalRequest.newAuthBuilder()) - } else { - handleFailedTokenReissue() - } - } - return response - } - - private suspend fun refreshTokenIfNeeded(): Boolean { - mutex.withLock { - val accessToken = wablePreferencesDataSource.accessToken.first() - val refreshToken = wablePreferencesDataSource.refreshToken.first() - - return try { - val tokenResult = runBlocking(Dispatchers.IO) { - authService.getReissueToken(accessToken, refreshToken) - } - - when (tokenResult.success) { - true -> { - wablePreferencesDataSource.updateAccessToken( - BEARER + tokenResult.data.accessToken, - ) - true - } - - false -> false - } - } catch (e: Exception) { - false - } - } - } - - private fun handleFailedTokenReissue() = CoroutineScope(Dispatchers.Main).launch { - showToast() - withContext(Dispatchers.IO) { - wablePreferencesDataSource.clear() - } - restartActivity() - } - - private fun showToast() { - currentToast?.cancel() - currentToast = Toast.makeText(context, "재 로그인이 필요해요", Toast.LENGTH_SHORT) - currentToast?.show() - } - - private suspend fun restartActivity() = with(context) { - mutex.withLock { - startActivity( - Intent.makeRestartActivityTask( - packageManager.getLaunchIntentForPackage(packageName)?.component, - ), - ) - } - } - - private fun Request.newAuthBuilder() = newBuilder() - .addHeader( - name = AUTHORIZATION, - value = runBlocking { - wablePreferencesDataSource.accessToken.first() - }, - ).build() + val accessToken = runBlocking { dataStore.accessToken.first() } + val authRequest: Request = chain.request().newBuilder() + .addHeader("Authorization", accessToken) + .build() - companion object { - const val CODE_TOKEN_EXPIRE = 401 - const val AUTHORIZATION = "Authorization" - const val BEARER = "Bearer " + return chain.proceed(authRequest) } } diff --git a/core/network/src/main/java/com/teamwable/network/di/NetworkModule.kt b/core/network/src/main/java/com/teamwable/network/di/NetworkModule.kt index 319d80c2a..b40cfa9e4 100644 --- a/core/network/src/main/java/com/teamwable/network/di/NetworkModule.kt +++ b/core/network/src/main/java/com/teamwable/network/di/NetworkModule.kt @@ -2,6 +2,7 @@ package com.teamwable.network.di import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import com.teamwable.network.BuildConfig +import com.teamwable.network.TokenAuthenticator import com.teamwable.network.TokenInterceptor import com.teamwable.network.util.isJsonArray import com.teamwable.network.util.isJsonObject @@ -10,7 +11,6 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.serialization.json.Json -import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor @@ -35,21 +35,18 @@ internal object NetworkModule { @Singleton @Provides fun provideOkHttpClient( - @AccessToken tokenInterceptor: Interceptor, + tokenInterceptor: TokenInterceptor, loggingInterceptor: HttpLoggingInterceptor, + tokenAuthenticator: TokenAuthenticator, ): OkHttpClient = OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(5, TimeUnit.SECONDS) .addInterceptor(tokenInterceptor) .addInterceptor(loggingInterceptor) + .authenticator(tokenAuthenticator) .build() - @Provides - @Singleton - @AccessToken - fun provideAuthInterceptor(interceptor: TokenInterceptor): Interceptor = interceptor - @Singleton @Provides fun provideLoggingInterceptor(): HttpLoggingInterceptor { diff --git a/core/network/src/main/java/com/teamwable/network/di/Qualifier.kt b/core/network/src/main/java/com/teamwable/network/di/Qualifier.kt index 8003ce636..2962f106f 100644 --- a/core/network/src/main/java/com/teamwable/network/di/Qualifier.kt +++ b/core/network/src/main/java/com/teamwable/network/di/Qualifier.kt @@ -6,10 +6,6 @@ import javax.inject.Qualifier @Retention(AnnotationRetention.BINARY) annotation class WableRetrofit -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class AccessToken - @Qualifier @Retention(AnnotationRetention.BINARY) annotation class WithoutTokenInterceptor diff --git a/core/data/src/main/java/com/teamwable/data/util/runSuspendCatching.kt b/core/network/src/main/java/com/teamwable/network/util/runSuspendCatching.kt similarity index 89% rename from core/data/src/main/java/com/teamwable/data/util/runSuspendCatching.kt rename to core/network/src/main/java/com/teamwable/network/util/runSuspendCatching.kt index 425bdf369..8d6713b5d 100644 --- a/core/data/src/main/java/com/teamwable/data/util/runSuspendCatching.kt +++ b/core/network/src/main/java/com/teamwable/network/util/runSuspendCatching.kt @@ -1,6 +1,5 @@ -package com.teamwable.data.util +package com.teamwable.network.util -import com.teamwable.network.util.toCustomError import kotlinx.coroutines.TimeoutCancellationException import kotlin.coroutines.cancellation.CancellationException diff --git a/feature/profile/src/main/java/com/teamwable/profile/hamburger/ProfileDeleteConfirmFragment.kt b/feature/profile/src/main/java/com/teamwable/profile/hamburger/ProfileDeleteConfirmFragment.kt index e9f0d0b46..01103dbe0 100644 --- a/feature/profile/src/main/java/com/teamwable/profile/hamburger/ProfileDeleteConfirmFragment.kt +++ b/feature/profile/src/main/java/com/teamwable/profile/hamburger/ProfileDeleteConfirmFragment.kt @@ -1,10 +1,10 @@ package com.teamwable.profile.hamburger -import android.content.Intent import androidx.fragment.app.viewModels import androidx.lifecycle.flowWithLifecycle import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import com.teamwable.common.restarter.AppReStarter import com.teamwable.common.uistate.UiState import com.teamwable.common.util.AmplitudeAuthTag.CLICK_NEXT_DELETEGUIDE import com.teamwable.common.util.AmplitudeUtil.trackEvent @@ -23,6 +23,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber +import javax.inject.Inject @AndroidEntryPoint class ProfileDeleteConfirmFragment : BindingFragment(FragmentProfileDeleteConfirmBinding::inflate) { @@ -31,6 +32,9 @@ class ProfileDeleteConfirmFragment : BindingFragment() + @Inject + lateinit var appRestarter: AppReStarter + override fun initView() { reasons = args.reasons.toList() @@ -62,11 +66,7 @@ class ProfileDeleteConfirmFragment : BindingFragment(BottomsheetProfileHamburgerBinding::inflate) { private val viewModel: ProfileHamburgerViewModel by viewModels() + @Inject + lateinit var appRestarter: AppReStarter + override fun initView() { initAccountInformationBtnClickListener() initNotificationSettingBtnClickListener() @@ -61,18 +61,12 @@ class ProfileHamburgerBottomSheet : BindingBottomSheetFragment - viewLifeCycleScope.launch(Dispatchers.Main) { - viewModel.saveIsAutoLogin(false) - navigateToSplashScreen() - } + viewModel.saveIsAutoLogin(false) + navigateToSplashScreen() } } private fun navigateToSplashScreen() { - startActivity( - Intent.makeRestartActivityTask( - requireActivity().packageManager.getLaunchIntentForPackage(requireActivity().packageName)?.component, - ) - ) + appRestarter.restartApp() } }