Skip to content

Commit fa5ab6e

Browse files
committed
[BOOK-274] feat: 강제 업데이트 구현
1 parent c5552a2 commit fa5ab6e

13 files changed

Lines changed: 169 additions & 29 deletions

File tree

build-logic/src/main/kotlin/AndroidApplicationConventionPlugin.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import com.ninecraft.booket.convention.ApplicationConstants
33
import com.ninecraft.booket.convention.Plugins
44
import com.ninecraft.booket.convention.applyPlugins
55
import com.ninecraft.booket.convention.configureAndroid
6+
import com.ninecraft.booket.convention.libs
67
import org.gradle.api.Plugin
78
import org.gradle.api.Project
89
import org.gradle.kotlin.dsl.configure
@@ -19,9 +20,9 @@ internal class AndroidApplicationConventionPlugin : Plugin<Project> {
1920
configureAndroid(this)
2021

2122
defaultConfig {
22-
targetSdk = ApplicationConstants.TARGET_SDK
23-
versionName = ApplicationConstants.VERSION_NAME
24-
versionCode = ApplicationConstants.VERSION_CODE
23+
targetSdk = libs.versions.targetSdk.get().toInt()
24+
versionName = libs.versions.versionName.get()
25+
versionCode = libs.versions.versionCode.get().toInt()
2526
}
2627
}
2728
}

build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import com.android.build.gradle.LibraryExtension
22
import com.ninecraft.booket.convention.Plugins
33
import com.ninecraft.booket.convention.applyPlugins
44
import com.ninecraft.booket.convention.configureAndroid
5+
import com.ninecraft.booket.convention.libs
56
import org.gradle.api.Plugin
67
import org.gradle.api.Project
78
import org.gradle.kotlin.dsl.configure
8-
import com.ninecraft.booket.convention.ApplicationConstants
99

1010
internal class AndroidLibraryConventionPlugin : Plugin<Project> {
1111
override fun apply(target: Project) {
@@ -19,7 +19,7 @@ internal class AndroidLibraryConventionPlugin : Plugin<Project> {
1919
configureAndroid(this)
2020

2121
defaultConfig.apply {
22-
targetSdk = ApplicationConstants.TARGET_SDK
22+
targetSdk = libs.versions.targetSdk.get().toInt()
2323
}
2424
}
2525
}

build-logic/src/main/kotlin/com/ninecraft/booket/convention/Android.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
88

99
internal fun Project.configureAndroid(extension: CommonExtension<*, *, *, *, *, *>) {
1010
extension.apply {
11-
compileSdk = ApplicationConstants.COMPILE_SDK
11+
compileSdk = libs.versions.compileSdk.get().toInt()
1212

1313
defaultConfig {
14-
minSdk = ApplicationConstants.MIN_SDK
14+
minSdk = libs.versions.minSdk.get().toInt()
1515
}
1616

1717
compileOptions {

build-logic/src/main/kotlin/com/ninecraft/booket/convention/ApplicationConstants.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@ package com.ninecraft.booket.convention
33
import org.gradle.api.JavaVersion
44

55
internal object ApplicationConstants {
6-
const val MIN_SDK = 28
7-
const val TARGET_SDK = 35
8-
const val COMPILE_SDK = 35
9-
const val VERSION_CODE = 3
10-
const val VERSION_NAME = "1.0.0"
116
const val JAVA_VERSION_INT = 17
127
val javaVersion = JavaVersion.VERSION_17
138
}

core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/RemoteConfigRepository.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ package com.ninecraft.booket.core.data.api.repository
22

33
interface RemoteConfigRepository {
44
suspend fun getLatestVersion(): Result<String>
5+
suspend fun shouldUpdate(): Result<Boolean>
56
}

core/data/impl/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ android {
1212
buildFeatures {
1313
buildConfig = true
1414
}
15+
16+
defaultConfig {
17+
buildConfigField("String", "APP_VERSION", "\"${libs.versions.versionName.get()}\"")
18+
}
1519
}
1620

1721
dependencies {

core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultRemoteConfigRepository.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.ninecraft.booket.core.data.impl.repository
33
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
44
import com.google.firebase.remoteconfig.get
55
import com.ninecraft.booket.core.data.api.repository.RemoteConfigRepository
6+
import com.ninecraft.booket.core.data.impl.BuildConfig
67
import com.orhanobut.logger.Logger
78
import kotlinx.coroutines.suspendCancellableCoroutine
89
import javax.inject.Inject
@@ -24,7 +25,39 @@ class DefaultRemoteConfigRepository @Inject constructor(
2425
}
2526
}
2627

28+
override suspend fun shouldUpdate(): Result<Boolean> = suspendCancellableCoroutine { continuation ->
29+
remoteConfig.fetchAndActivate().addOnCompleteListener { task ->
30+
if (task.isSuccessful) {
31+
val latestVersion = remoteConfig[KEY_MIN_VERSION].asString()
32+
val currentVersion = BuildConfig.APP_VERSION
33+
continuation.resume(Result.success(checkMinVersion(currentVersion, latestVersion)))
34+
} else {
35+
Logger.e(task.exception, "shouldUpdate: getMinVersion failed")
36+
continuation.resume(Result.failure(task.exception ?: Exception("Unknown error")))
37+
}
38+
}
39+
}
40+
41+
private fun checkMinVersion(currentVersion: String, minVersion: String): Boolean {
42+
Logger.d("checkMinVersion: current: $currentVersion, min: $minVersion")
43+
if (!Regex("""^\d+\.\d+\.\d+$""").matches(currentVersion)) return false
44+
if (!Regex("""^\d+\.\d+\.\d+$""").matches(minVersion)) return false
45+
46+
val current = currentVersion.split('.').map { it.toInt() }
47+
val min = minVersion.split('.').map { it.toInt() }
48+
49+
// 메이저 버전 비교
50+
if (current[0] != min[0]) return current[0] < min[0]
51+
52+
// 마이너 버전 비교
53+
if (current[1] != min[1]) return current[1] < min[1]
54+
55+
// 패치 버전 비교
56+
return current[2] < min[2]
57+
}
58+
2759
companion object {
2860
private const val KEY_LATEST_VERSION = "LatestVersion"
61+
private const val KEY_MIN_VERSION = "LatestVersion"
2962
}
3063
}

feature/splash/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ plugins {
66

77
android {
88
namespace = "com.ninecraft.booket.feature.splash"
9+
10+
buildFeatures {
11+
buildConfig = true
12+
}
13+
14+
defaultConfig {
15+
buildConfigField("String", "PACKAGE_NAME", "\"${libs.versions.packageName.get()}\"")
16+
}
917
}
1018

1119
ksp {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.ninecraft.booket.splash
2+
3+
import android.content.Context
4+
import android.content.Intent
5+
import android.net.Uri
6+
import androidx.compose.runtime.Composable
7+
import androidx.compose.ui.platform.LocalContext
8+
import com.skydoves.compose.effects.RememberedEffect
9+
import androidx.core.net.toUri
10+
11+
@Composable
12+
internal fun HandleSplashSideEffects(
13+
state: SplashUiState,
14+
eventSink: (SplashUiEvent) -> Unit,
15+
) {
16+
val context = LocalContext.current
17+
18+
RememberedEffect(state.sideEffect) {
19+
when (state.sideEffect) {
20+
is SplashSideEffect.NavigateToPlayStore -> {
21+
openPlayStore(context)
22+
}
23+
null -> {}
24+
}
25+
26+
if (state.sideEffect != null) {
27+
eventSink(SplashUiEvent.InitSideEffect)
28+
}
29+
}
30+
}
31+
32+
private fun openPlayStore(context: Context) {
33+
val intent = Intent(Intent.ACTION_VIEW, "market://details?id=${context.packageName}".toUri())
34+
context.startActivity(intent)
35+
}

feature/splash/src/main/kotlin/com/ninecraft/booket/splash/SplashPresenter.kt

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.compose.runtime.setValue
99
import com.ninecraft.booket.core.common.constants.ErrorScope
1010
import com.ninecraft.booket.core.common.utils.postErrorDialog
1111
import com.ninecraft.booket.core.data.api.repository.AuthRepository
12+
import com.ninecraft.booket.core.data.api.repository.RemoteConfigRepository
1213
import com.ninecraft.booket.core.data.api.repository.UserRepository
1314
import com.ninecraft.booket.core.model.AutoLoginState
1415
import com.ninecraft.booket.core.model.OnboardingState
@@ -17,7 +18,7 @@ import com.ninecraft.booket.feature.screens.HomeScreen
1718
import com.ninecraft.booket.feature.screens.LoginScreen
1819
import com.ninecraft.booket.feature.screens.OnboardingScreen
1920
import com.ninecraft.booket.feature.screens.SplashScreen
20-
import com.skydoves.compose.effects.RememberedEffect
21+
import com.orhanobut.logger.Logger
2122
import com.slack.circuit.codegen.annotations.CircuitInject
2223
import com.slack.circuit.retained.collectAsRetainedState
2324
import com.slack.circuit.retained.rememberRetained
@@ -34,14 +35,16 @@ class SplashPresenter @AssistedInject constructor(
3435
@Assisted private val navigator: Navigator,
3536
private val userRepository: UserRepository,
3637
private val authRepository: AuthRepository,
38+
private val remoteConfigRepository: RemoteConfigRepository,
3739
) : Presenter<SplashUiState> {
3840

3941
@Composable
4042
override fun present(): SplashUiState {
4143
val scope = rememberCoroutineScope()
4244
val onboardingState by userRepository.onboardingState.collectAsRetainedState(initial = OnboardingState.IDLE)
4345
val autoLoginState by authRepository.autoLoginState.collectAsRetainedState(initial = AutoLoginState.IDLE)
44-
var isSplashTimeCompleted by rememberRetained { mutableStateOf(false) }
46+
var isForceUpdateDialogVisible by rememberRetained { mutableStateOf(false) }
47+
var sideEffect by rememberRetained { mutableStateOf<SplashSideEffect?>(null) }
4548

4649
fun checkTermsAgreement() {
4750
scope.launch {
@@ -64,42 +67,67 @@ class SplashPresenter @AssistedInject constructor(
6467
}
6568
}
6669

67-
LaunchedEffect(Unit) {
68-
delay(1000L)
69-
isSplashTimeCompleted = true
70-
}
71-
72-
RememberedEffect(onboardingState, autoLoginState, isSplashTimeCompleted) {
73-
if (!isSplashTimeCompleted) return@RememberedEffect
74-
70+
fun proceedToNextScreen() {
7571
when (onboardingState) {
7672
OnboardingState.NOT_COMPLETED -> {
7773
navigator.resetRoot(OnboardingScreen)
7874
}
79-
8075
OnboardingState.COMPLETED -> {
8176
when (autoLoginState) {
8277
AutoLoginState.LOGGED_IN -> {
8378
checkTermsAgreement()
8479
}
85-
8680
AutoLoginState.NOT_LOGGED_IN -> {
8781
navigator.resetRoot(LoginScreen)
8882
}
89-
9083
AutoLoginState.IDLE -> {
9184
// 자동 로그인 상태를 기다리는 중
9285
}
9386
}
9487
}
95-
9688
OnboardingState.IDLE -> {
9789
// 온보딩 상태를 기다리는 중
9890
}
9991
}
10092
}
10193

102-
return SplashUiState
94+
fun handleEvent(event: SplashUiEvent) {
95+
when (event) {
96+
SplashUiEvent.OnUpdateButtonClick -> {
97+
sideEffect = SplashSideEffect.NavigateToPlayStore
98+
}
99+
SplashUiEvent.InitSideEffect -> {
100+
sideEffect = null
101+
}
102+
}
103+
}
104+
105+
LaunchedEffect(onboardingState, autoLoginState) {
106+
delay(1000L)
107+
108+
if (onboardingState == OnboardingState.IDLE || autoLoginState == AutoLoginState.IDLE) {
109+
return@LaunchedEffect
110+
}
111+
112+
remoteConfigRepository.shouldUpdate()
113+
.onSuccess { shouldUpdate ->
114+
if (shouldUpdate) {
115+
isForceUpdateDialogVisible = true
116+
} else {
117+
proceedToNextScreen()
118+
}
119+
}
120+
.onFailure { exception ->
121+
Logger.e("${exception.message}")
122+
proceedToNextScreen()
123+
}
124+
}
125+
126+
return SplashUiState(
127+
isForceUpdateDialogVisible = isForceUpdateDialogVisible,
128+
sideEffect = sideEffect,
129+
eventSink = ::handleEvent
130+
)
103131
}
104132

105133
@CircuitInject(SplashScreen::class, ActivityRetainedComponent::class)

0 commit comments

Comments
 (0)