diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt index c702c824..21a92847 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt @@ -14,6 +14,8 @@ import com.threegap.bitnagil.data.routine.datasource.RoutineRemoteDataSource import com.threegap.bitnagil.data.routine.datasourceImpl.RoutineRemoteDataSourceImpl import com.threegap.bitnagil.data.user.datasource.UserDataSource import com.threegap.bitnagil.data.user.datasourceImpl.UserDataSourceImpl +import com.threegap.bitnagil.data.version.datasource.VersionDataSource +import com.threegap.bitnagil.data.version.datasourceImpl.VersionDataSourceImpl import com.threegap.bitnagil.data.writeroutine.datasource.WriteRoutineDataSource import com.threegap.bitnagil.data.writeroutine.datasourceImpl.WriteRoutineDataSourceImpl import dagger.Binds @@ -57,4 +59,8 @@ abstract class DataSourceModule { @Binds @Singleton abstract fun bindRecommendRoutineDataSource(recommendRoutineDataSourceImpl: RecommendRoutineDataSourceImpl): RecommendRoutineDataSource + + @Binds + @Singleton + abstract fun bindVersionDataSource(versionDataSourceImpl: VersionDataSourceImpl): VersionDataSource } diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt index 073dd0e1..93fb9e37 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt @@ -6,6 +6,7 @@ import com.threegap.bitnagil.data.onboarding.repositoryImpl.OnBoardingRepository import com.threegap.bitnagil.data.recommendroutine.repositoryImpl.RecommendRoutineRepositoryImpl import com.threegap.bitnagil.data.routine.repositoryImpl.RoutineRepositoryImpl import com.threegap.bitnagil.data.user.repositoryImpl.UserRepositoryImpl +import com.threegap.bitnagil.data.version.repositoryImpl.VersionRepositoryImpl import com.threegap.bitnagil.data.writeroutine.repositoryImpl.WriteRoutineRepositoryImpl import com.threegap.bitnagil.domain.auth.repository.AuthRepository import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository @@ -13,6 +14,7 @@ import com.threegap.bitnagil.domain.onboarding.repository.OnBoardingRepository import com.threegap.bitnagil.domain.recommendroutine.repository.RecommendRoutineRepository import com.threegap.bitnagil.domain.routine.repository.RoutineRepository import com.threegap.bitnagil.domain.user.repository.UserRepository +import com.threegap.bitnagil.domain.version.repository.VersionRepository import com.threegap.bitnagil.domain.writeroutine.repository.WriteRoutineRepository import dagger.Binds import dagger.Module @@ -51,4 +53,8 @@ abstract class RepositoryModule { @Binds @Singleton abstract fun bindRecommendRoutineRepository(recommendRoutineRepositoryImpl: RecommendRoutineRepositoryImpl): RecommendRoutineRepository + + @Binds + @Singleton + abstract fun bindVersionRepository(versionRepositoryImpl: VersionRepositoryImpl): VersionRepository } diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt index a5d8bbe1..66376c96 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt @@ -6,6 +6,7 @@ import com.threegap.bitnagil.data.onboarding.service.OnBoardingService import com.threegap.bitnagil.data.recommendroutine.service.RecommendRoutineService import com.threegap.bitnagil.data.routine.service.RoutineService import com.threegap.bitnagil.data.user.service.UserService +import com.threegap.bitnagil.data.version.service.VersionService import com.threegap.bitnagil.data.writeroutine.service.WriteRoutineService import com.threegap.bitnagil.di.core.Auth import com.threegap.bitnagil.di.core.NoneAuth @@ -60,4 +61,9 @@ object ServiceModule { @Singleton fun provideRecommendRoutineService(@Auth retrofit: Retrofit): RecommendRoutineService = retrofit.create(RecommendRoutineService::class.java) + + @Provides + @Singleton + fun provideVersionService(@NoneAuth retrofit: Retrofit): VersionService = + retrofit.create(VersionService::class.java) } diff --git a/build-logic/convention/src/main/java/com/threegap/bitnagil/convention/AndroidApplicationPlugin.kt b/build-logic/convention/src/main/java/com/threegap/bitnagil/convention/AndroidApplicationPlugin.kt index 60a3e7eb..a70606ad 100644 --- a/build-logic/convention/src/main/java/com/threegap/bitnagil/convention/AndroidApplicationPlugin.kt +++ b/build-logic/convention/src/main/java/com/threegap/bitnagil/convention/AndroidApplicationPlugin.kt @@ -1,6 +1,7 @@ package com.threegap.bitnagil.convention import com.android.build.api.dsl.ApplicationExtension +import com.threegap.bitnagil.convention.extension.configureAppVersion import com.threegap.bitnagil.convention.extension.configureComposeAndroid import com.threegap.bitnagil.convention.extension.configureKotlinAndroid import org.gradle.api.Plugin @@ -21,10 +22,10 @@ class AndroidApplicationPlugin : Plugin { extensions.configure { configureKotlinAndroid(this) configureComposeAndroid(this) + configureAppVersion() with(defaultConfig) { targetSdk = libs.findVersion("targetSdk").get().requiredVersion.toInt() versionCode = libs.findVersion("versionCode").get().requiredVersion.toInt() - versionName = libs.findVersion("versionName").get().requiredVersion } } } diff --git a/build-logic/convention/src/main/java/com/threegap/bitnagil/convention/AndroidLibraryPlugin.kt b/build-logic/convention/src/main/java/com/threegap/bitnagil/convention/AndroidLibraryPlugin.kt index 3f40ef0d..ddcbf9fa 100644 --- a/build-logic/convention/src/main/java/com/threegap/bitnagil/convention/AndroidLibraryPlugin.kt +++ b/build-logic/convention/src/main/java/com/threegap/bitnagil/convention/AndroidLibraryPlugin.kt @@ -1,6 +1,7 @@ package com.threegap.bitnagil.convention import com.android.build.gradle.LibraryExtension +import com.threegap.bitnagil.convention.extension.configureAppVersion import com.threegap.bitnagil.convention.extension.configureKotlinAndroid import com.threegap.bitnagil.convention.extension.configureKotlinCoroutine import org.gradle.api.Plugin @@ -17,6 +18,7 @@ class AndroidLibraryPlugin : Plugin { extensions.configure { configureKotlinAndroid(this) configureKotlinCoroutine(this) + configureAppVersion() } } } diff --git a/build-logic/convention/src/main/java/com/threegap/bitnagil/convention/extension/AppVersion.kt b/build-logic/convention/src/main/java/com/threegap/bitnagil/convention/extension/AppVersion.kt new file mode 100644 index 00000000..e9b9613d --- /dev/null +++ b/build-logic/convention/src/main/java/com/threegap/bitnagil/convention/extension/AppVersion.kt @@ -0,0 +1,37 @@ +package com.threegap.bitnagil.convention.extension + +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.LibraryExtension +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.findByType +import org.gradle.kotlin.dsl.getByType + +internal fun Project.configureAppVersion() { + val libs = extensions.getByType().named("libs") + val major = libs.findVersion("versionMajor").get().requiredVersion + val minor = libs.findVersion("versionMinor").get().requiredVersion + val patch = libs.findVersion("versionPatch").get().requiredVersion + + extensions.findByType()?.let { appExtension -> + appExtension.defaultConfig { + versionName = "$major.$minor.$patch" + buildConfigField("int", "VERSION_MAJOR", major) + buildConfigField("int", "VERSION_MINOR", minor) + buildConfigField("int", "VERSION_PATCH", patch) + } + } + + extensions.findByType()?.let { libExtension -> + libExtension.apply { + defaultConfig { + buildConfigField("int", "VERSION_MAJOR", major) + buildConfigField("int", "VERSION_MINOR", minor) + buildConfigField("int", "VERSION_PATCH", patch) + } + buildFeatures { + buildConfig = true + } + } + } +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/version/datasource/VersionDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/version/datasource/VersionDataSource.kt new file mode 100644 index 00000000..bfdaf968 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/version/datasource/VersionDataSource.kt @@ -0,0 +1,7 @@ +package com.threegap.bitnagil.data.version.datasource + +import com.threegap.bitnagil.data.version.model.response.VersionCheckResponseDto + +interface VersionDataSource { + suspend fun checkVersion(): Result +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/version/datasourceImpl/VersionDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/version/datasourceImpl/VersionDataSourceImpl.kt new file mode 100644 index 00000000..e83a11e6 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/version/datasourceImpl/VersionDataSourceImpl.kt @@ -0,0 +1,22 @@ +package com.threegap.bitnagil.data.version.datasourceImpl + +import com.threegap.bitnagil.data.BuildConfig +import com.threegap.bitnagil.data.common.safeApiCall +import com.threegap.bitnagil.data.version.datasource.VersionDataSource +import com.threegap.bitnagil.data.version.model.response.VersionCheckResponseDto +import com.threegap.bitnagil.data.version.service.VersionService +import javax.inject.Inject + +class VersionDataSourceImpl @Inject constructor( + private val versionService: VersionService, +) : VersionDataSource { + + override suspend fun checkVersion(): Result = + safeApiCall { + versionService.checkVersion( + majorVersion = BuildConfig.VERSION_MAJOR, + minorVersion = BuildConfig.VERSION_MINOR, + patchVersion = BuildConfig.VERSION_PATCH, + ) + } +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/version/model/response/VersionCheckResponseDto.kt b/data/src/main/java/com/threegap/bitnagil/data/version/model/response/VersionCheckResponseDto.kt new file mode 100644 index 00000000..c31481e8 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/version/model/response/VersionCheckResponseDto.kt @@ -0,0 +1,16 @@ +package com.threegap.bitnagil.data.version.model.response + +import com.threegap.bitnagil.domain.version.model.UpdateRequirement +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class VersionCheckResponseDto( + @SerialName("forceUpdateYn") + val forceUpdateYn: Boolean, +) + +fun VersionCheckResponseDto.toDomain() = + UpdateRequirement( + isForced = this.forceUpdateYn, + ) diff --git a/data/src/main/java/com/threegap/bitnagil/data/version/repositoryImpl/VersionRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/version/repositoryImpl/VersionRepositoryImpl.kt new file mode 100644 index 00000000..85d3e772 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/version/repositoryImpl/VersionRepositoryImpl.kt @@ -0,0 +1,15 @@ +package com.threegap.bitnagil.data.version.repositoryImpl + +import com.threegap.bitnagil.data.version.datasource.VersionDataSource +import com.threegap.bitnagil.data.version.model.response.toDomain +import com.threegap.bitnagil.domain.version.model.UpdateRequirement +import com.threegap.bitnagil.domain.version.repository.VersionRepository +import javax.inject.Inject + +class VersionRepositoryImpl @Inject constructor( + private val versionDataSource: VersionDataSource, +) : VersionRepository { + + override suspend fun checkVersion(): Result = + versionDataSource.checkVersion().map { it.toDomain() } +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/version/service/VersionService.kt b/data/src/main/java/com/threegap/bitnagil/data/version/service/VersionService.kt new file mode 100644 index 00000000..4f8441b4 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/version/service/VersionService.kt @@ -0,0 +1,15 @@ +package com.threegap.bitnagil.data.version.service + +import com.threegap.bitnagil.data.version.model.response.VersionCheckResponseDto +import com.threegap.bitnagil.network.model.BaseResponse +import retrofit2.http.GET +import retrofit2.http.Query + +interface VersionService { + @GET("/api/v1/version/android/check") + suspend fun checkVersion( + @Query("major") majorVersion: Int, + @Query("minor") minorVersion: Int, + @Query("patch") patchVersion: Int, + ): BaseResponse +} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/version/model/UpdateRequirement.kt b/domain/src/main/java/com/threegap/bitnagil/domain/version/model/UpdateRequirement.kt new file mode 100644 index 00000000..1c511eb4 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/version/model/UpdateRequirement.kt @@ -0,0 +1,5 @@ +package com.threegap.bitnagil.domain.version.model + +data class UpdateRequirement( + val isForced: Boolean, +) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/version/repository/VersionRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/version/repository/VersionRepository.kt new file mode 100644 index 00000000..5051a26d --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/version/repository/VersionRepository.kt @@ -0,0 +1,7 @@ +package com.threegap.bitnagil.domain.version.repository + +import com.threegap.bitnagil.domain.version.model.UpdateRequirement + +interface VersionRepository { + suspend fun checkVersion(): Result +} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/version/usecase/CheckUpdateRequirementUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/version/usecase/CheckUpdateRequirementUseCase.kt new file mode 100644 index 00000000..09b655b4 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/version/usecase/CheckUpdateRequirementUseCase.kt @@ -0,0 +1,11 @@ +package com.threegap.bitnagil.domain.version.usecase + +import com.threegap.bitnagil.domain.version.repository.VersionRepository +import javax.inject.Inject + +class CheckUpdateRequirementUseCase @Inject constructor( + private val versionRepository: VersionRepository, +) { + suspend operator fun invoke(): Result = + versionRepository.checkVersion().map { it.isForced } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f0d91852..316b1e6b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,9 @@ targetSdk = "35" ## App Versioning versionCode = "4" -versionName = "1.0.2" +versionMajor = "1" +versionMinor = "0" +versionPatch = "2" # Android Gradle Plugin androidGradlePlugin = "8.10.1" diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt index b10d135a..5801baa4 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt @@ -1,5 +1,6 @@ package com.threegap.bitnagil.presentation.splash +import androidx.activity.ComponentActivity import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.foundation.background @@ -16,15 +17,20 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.R import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon import com.threegap.bitnagil.presentation.splash.component.template.BitnagilLottieAnimation +import com.threegap.bitnagil.presentation.splash.component.template.ForceUpdateDialog import com.threegap.bitnagil.presentation.splash.model.SplashSideEffect +import com.threegap.bitnagil.presentation.splash.util.openAppInPlayStore import org.orbitmvi.orbit.compose.collectSideEffect +import kotlin.system.exitProcess @Composable fun SplashScreenContainer( @@ -34,6 +40,10 @@ fun SplashScreenContainer( navigateToHome: () -> Unit, viewModel: SplashViewModel = hiltViewModel(), ) { + val context = LocalContext.current + val activity = context as? ComponentActivity + val uiState by viewModel.stateFlow.collectAsStateWithLifecycle() + viewModel.collectSideEffect { sideEffect -> when (sideEffect) { is SplashSideEffect.NavigateToLogin -> navigateToLogin() @@ -46,6 +56,16 @@ fun SplashScreenContainer( SplashScreen( onCompleted = viewModel::onAnimationCompleted, ) + + if (uiState.forceUpdateRequired) { + ForceUpdateDialog( + onUpdateClick = { openAppInPlayStore(activity) }, + onDismissRequest = { + activity?.finishAffinity() + exitProcess(0) + }, + ) + } } @Composable diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt index 23a7641a..8379d650 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.threegap.bitnagil.domain.auth.model.UserRole import com.threegap.bitnagil.domain.auth.usecase.AutoLoginUseCase +import com.threegap.bitnagil.domain.version.usecase.CheckUpdateRequirementUseCase import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel import com.threegap.bitnagil.presentation.splash.model.SplashIntent import com.threegap.bitnagil.presentation.splash.model.SplashSideEffect @@ -18,6 +19,7 @@ import javax.inject.Inject @HiltViewModel class SplashViewModel @Inject constructor( savedStateHandle: SavedStateHandle, + private val checkUpdateRequirementUseCase: CheckUpdateRequirementUseCase, private val autoLoginUseCase: AutoLoginUseCase, ) : MviViewModel( initState = SplashState(), @@ -25,7 +27,7 @@ class SplashViewModel @Inject constructor( ) { init { - performAutoLogin() + performForceUpdateCheck() } override suspend fun SimpleSyntax.reduceState( @@ -40,6 +42,13 @@ class SplashViewModel @Inject constructor( ) } + is SplashIntent.SetForceUpdateResult -> { + state.copy( + forceUpdateRequired = intent.isRequired, + isForceUpdateCheckCompleted = true, + ) + } + is SplashIntent.NavigateToLogin -> { sendSideEffect(SplashSideEffect.NavigateToLogin) null @@ -61,6 +70,20 @@ class SplashViewModel @Inject constructor( } } + private fun performForceUpdateCheck() { + viewModelScope.launch { + val isUpdateRequired = withTimeoutOrNull(5000) { + checkUpdateRequirementUseCase().getOrElse { false } + } ?: false + + sendIntent(SplashIntent.SetForceUpdateResult(isUpdateRequired)) + + if (!isUpdateRequired) { + performAutoLogin() + } + } + } + private fun performAutoLogin() { viewModelScope.launch { try { @@ -76,6 +99,9 @@ class SplashViewModel @Inject constructor( fun onAnimationCompleted() { val splashState = container.stateFlow.value + + if (splashState.forceUpdateRequired) return + if (!splashState.isAutoLoginCompleted) { viewModelScope.launch { delay(100) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/component/template/ForceUpdateDialog.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/component/template/ForceUpdateDialog.kt new file mode 100644 index 00000000..6ddcfcd9 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/component/template/ForceUpdateDialog.kt @@ -0,0 +1,102 @@ +package com.threegap.bitnagil.presentation.splash.component.template + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.LineHeightStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButton +import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButtonColor + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ForceUpdateDialog( + onUpdateClick: () -> Unit, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, +) { + BasicAlertDialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false, + ), + modifier = modifier + .background( + color = BitnagilTheme.colors.white, + shape = RoundedCornerShape(12.dp), + ) + .padding(vertical = 20.dp, horizontal = 24.dp), + ) { + Column( + modifier = Modifier, + ) { + Text( + text = "최신 버전으로 업데이트가 필요해요", + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.subtitle1SemiBold.copy( + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), + ), + ) + + Text( + text = "더 안정적이고 안전한 서비스를 위해\n최신 버전으로 업데이트해 주세요.", + color = BitnagilTheme.colors.coolGray40, + style = BitnagilTheme.typography.body2Medium.copy( + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), + ), + ) + + Spacer(modifier = Modifier.height(18.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + BitnagilTextButton( + text = "나가기", + onClick = onDismissRequest, + colors = BitnagilTextButtonColor.cancel(), + textStyle = BitnagilTheme.typography.body2Medium, + modifier = Modifier.weight(1f), + ) + + BitnagilTextButton( + text = "업데이트", + onClick = onUpdateClick, + textStyle = BitnagilTheme.typography.body2Medium, + modifier = Modifier.weight(1f), + ) + } + } + } +} + +@Preview +@Composable +private fun ForceUpdateDialogPreview() { + ForceUpdateDialog( + onUpdateClick = {}, + onDismissRequest = {}, + ) +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashIntent.kt index 55593d98..1e909bd7 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashIntent.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashIntent.kt @@ -9,4 +9,5 @@ sealed class SplashIntent : MviIntent { data object NavigateToHome : SplashIntent() data object NavigateToTermsAgreement : SplashIntent() data object NavigateToOnboarding : SplashIntent() + data class SetForceUpdateResult(val isRequired: Boolean) : SplashIntent() } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashState.kt index 61cbb355..f4267f6c 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashState.kt @@ -8,4 +8,6 @@ import kotlinx.parcelize.Parcelize data class SplashState( val userRole: UserRole? = null, val isAutoLoginCompleted: Boolean = false, + val isForceUpdateCheckCompleted: Boolean = false, + val forceUpdateRequired: Boolean = false, ) : MviState diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/util/PlayStoreUtils.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/util/PlayStoreUtils.kt new file mode 100644 index 00000000..8b60652a --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/util/PlayStoreUtils.kt @@ -0,0 +1,70 @@ +package com.threegap.bitnagil.presentation.splash.util + +import android.content.ActivityNotFoundException +import android.content.Intent +import androidx.activity.ComponentActivity +import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.system.exitProcess + +private const val PACKAGE_NAME = "com.threegap.bitnagil" +private const val GOOGLE_PLAY_PACKAGE = "com.android.vending" +private const val APP_EXIT_DELAY = 500L + +fun openAppInPlayStore( + activity: ComponentActivity?, + shouldFinishApp: Boolean = true, +) { + activity?.let { + val isSuccess = tryOpenPlayStore(it) || tryOpenWebBrowser(it) + + if (isSuccess && shouldFinishApp) { + finishAppWithDelay(it) + } + } +} + +private fun tryOpenPlayStore(activity: ComponentActivity): Boolean = + try { + val intent = createPlayStoreIntent() + activity.startActivity(intent) + true + } catch (e: ActivityNotFoundException) { + false + } + +private fun tryOpenWebBrowser(activity: ComponentActivity): Boolean = + try { + val intent = createWebIntent() + activity.startActivity(intent) + true + } catch (e: Exception) { + false + } + +private fun createPlayStoreIntent(): Intent = + Intent( + Intent.ACTION_VIEW, + "market://details?id=$PACKAGE_NAME".toUri(), + ).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + setPackage(GOOGLE_PLAY_PACKAGE) + } + +private fun createWebIntent(): Intent = + Intent( + Intent.ACTION_VIEW, + "https://play.google.com/store/apps/details?id=$PACKAGE_NAME".toUri(), + ).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + +private fun finishAppWithDelay(activity: ComponentActivity) { + activity.lifecycleScope.launch { + delay(APP_EXIT_DELAY) + activity.finishAffinity() + exitProcess(0) + } +}