Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -3,6 +3,7 @@ import com.ninecraft.booket.convention.ApplicationConstants
import com.ninecraft.booket.convention.Plugins
import com.ninecraft.booket.convention.applyPlugins
import com.ninecraft.booket.convention.configureAndroid
import com.ninecraft.booket.convention.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
Expand All @@ -19,9 +20,9 @@ internal class AndroidApplicationConventionPlugin : Plugin<Project> {
configureAndroid(this)

defaultConfig {
targetSdk = ApplicationConstants.TARGET_SDK
versionName = ApplicationConstants.VERSION_NAME
versionCode = ApplicationConstants.VERSION_CODE
targetSdk = libs.versions.targetSdk.get().toInt()
versionName = libs.versions.versionName.get()
versionCode = libs.versions.versionCode.get().toInt()
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import com.android.build.gradle.LibraryExtension
import com.ninecraft.booket.convention.Plugins
import com.ninecraft.booket.convention.applyPlugins
import com.ninecraft.booket.convention.configureAndroid
import com.ninecraft.booket.convention.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import com.ninecraft.booket.convention.ApplicationConstants

internal class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
Expand All @@ -19,7 +19,7 @@ internal class AndroidLibraryConventionPlugin : Plugin<Project> {
configureAndroid(this)

defaultConfig.apply {
targetSdk = ApplicationConstants.TARGET_SDK
targetSdk = libs.versions.targetSdk.get().toInt()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension

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

defaultConfig {
minSdk = ApplicationConstants.MIN_SDK
minSdk = libs.versions.minSdk.get().toInt()
}

compileOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@ package com.ninecraft.booket.convention
import org.gradle.api.JavaVersion

internal object ApplicationConstants {
const val MIN_SDK = 28
const val TARGET_SDK = 35
const val COMPILE_SDK = 35
const val VERSION_CODE = 3
const val VERSION_NAME = "1.0.0"
const val JAVA_VERSION_INT = 17
val javaVersion = JavaVersion.VERSION_17
}
8 changes: 8 additions & 0 deletions core/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ plugins {

android {
namespace = "com.ninecraft.booket.core.common"

buildFeatures {
buildConfig = true
}

defaultConfig {
buildConfigField("String", "PACKAGE_NAME", "\"${libs.versions.packageName.get()}\"")
}
}

dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package com.ninecraft.booket.core.common.extensions

import android.content.ContentValues
import android.content.Context
import android.content.Intent
import androidx.core.net.toUri
import com.ninecraft.booket.core.common.BuildConfig
import android.graphics.Bitmap
import android.os.Build
import android.os.Environment
Expand Down Expand Up @@ -41,7 +44,8 @@ fun Context.saveImageToGallery(bitmap: ImageBitmap) {
}
}

val imageUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
val imageUri =
contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
imageUri?.let { uri ->
contentResolver.openOutputStream(uri)?.use { outputStream ->
bitmap.asAndroidBitmap().compress(Bitmap.CompressFormat.PNG, 100, outputStream)
Expand All @@ -57,3 +61,9 @@ fun Context.saveImageToGallery(bitmap: ImageBitmap) {
Logger.e("Failed to save image to gallery: ${e.message}")
}
}

fun Context.openPlayStore() {
val intent =
Intent(Intent.ACTION_VIEW, "market://details?id=${BuildConfig.PACKAGE_NAME}".toUri())
startActivity(intent)
}
Comment on lines +65 to +69
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Play 스토어 미탑재 기기/에뮬레이터에서 ActivityNotFoundException로 크래시 가능 — 핸들러 확인, 웹 URL 폴백, NEW_TASK 플래그 추가 필요

market 스킴을 처리할 앱이 없는 환경(일부 AOSP 기기/중국 ROM/에뮬레이터 등)에서 startActivity가 예외를 던질 수 있습니다. 또한 비-Activity Context에서 호출될 수 있는 확장 함수 특성상 NEW_TASK 플래그가 필요합니다. 아래와 같이 안전하게 처리해 주세요.

-fun Context.openPlayStore() {
-    val intent =
-        Intent(Intent.ACTION_VIEW, "market://details?id=${BuildConfig.PACKAGE_NAME}".toUri())
-    startActivity(intent)
-}
+fun Context.openPlayStore() {
+    val marketUri = "market://details?id=${BuildConfig.PACKAGE_NAME}".toUri()
+    val marketIntent = Intent(Intent.ACTION_VIEW, marketUri).apply {
+        if (this@openPlayStore !is Activity) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+    }
+
+    val pm = packageManager
+    if (marketIntent.resolveActivity(pm) != null) {
+        startActivity(marketIntent)
+        return
+    }
+
+    // Fallback: 웹 URL
+    val webUri = "https://play.google.com/store/apps/details?id=${BuildConfig.PACKAGE_NAME}".toUri()
+    val webIntent = Intent(Intent.ACTION_VIEW, webUri).apply {
+        if (this@openPlayStore !is Activity) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+    }
+    try {
+        startActivity(webIntent)
+    } catch (e: ActivityNotFoundException) {
+        Logger.w("No handler for Play Store intents: ${e.message}")
+    }
+}

위 변경에 따라 필요한 import(파일 상단)에 아래를 추가해 주세요.

+import android.app.Activity
+import android.content.ActivityNotFoundException
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun Context.openPlayStore() {
val intent =
Intent(Intent.ACTION_VIEW, "market://details?id=${BuildConfig.PACKAGE_NAME}".toUri())
startActivity(intent)
}
// Add at the top of the file:
import android.app.Activity
import android.content.ActivityNotFoundException
fun Context.openPlayStore() {
val marketUri = "market://details?id=${BuildConfig.PACKAGE_NAME}".toUri()
val marketIntent = Intent(Intent.ACTION_VIEW, marketUri).apply {
if (this@openPlayStore !is Activity) {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
}
val pm = packageManager
if (marketIntent.resolveActivity(pm) != null) {
startActivity(marketIntent)
return
}
// Fallback to web URL if no market handler is available
val webUri = "https://play.google.com/store/apps/details?id=${BuildConfig.PACKAGE_NAME}".toUri()
val webIntent = Intent(Intent.ACTION_VIEW, webUri).apply {
if (this@openPlayStore !is Activity) {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
}
try {
startActivity(webIntent)
} catch (e: ActivityNotFoundException) {
Logger.w("No handler for Play Store intents: ${e.message}")
}
}
🤖 Prompt for AI Agents
In
core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Context.kt
around lines 65–69, wrap the startActivity call in a try/catch for
android.content.ActivityNotFoundException and, when caught, fall back to opening
the Play Store web URL
(https://play.google.com/store/apps/details?id=<PACKAGE_NAME>), and ensure the
Intent uses FLAG_ACTIVITY_NEW_TASK so non-Activity Contexts can launch it; also
add the needed imports (android.content.ActivityNotFoundException and
Intent.FLAG_ACTIVITY_NEW_TASK or android.content.Intent) at the file top.

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.ninecraft.booket.core.common.util

import com.orhanobut.logger.Logger

/**
* 두 버전을 비교하는 함수
*
* @param version1 첫 번째 버전 (예: "1.2.3")
* @param version2 두 번째 버전 (예: "1.1.0")
* @return 양수면 version1 > version2, 음수면 version1 < version2, 0이면 같음
*
* 버전 형식: "메이저.마이너.패치" (예: 1.2.3)
* 비교 순서: 메이저 → 마이너 → 패치 버전 순으로 비교
*/
fun compareVersions(version1: String, version2: String): Int {
Logger.d("compareVersions: version1: $version1, version2: $version2")

if (!Regex("""^\d+\.\d+\.\d+$""").matches(version1)) return 0
if (!Regex("""^\d+\.\d+\.\d+$""").matches(version2)) return 0

val v1 = version1.split('.').map { it.toInt() }
val v2 = version2.split('.').map { it.toInt() }

// 메이저 버전 비교
if (v1[0] != v2[0]) return v1[0] - v2[0]

// 마이너 버전 비교
if (v1[1] != v2[1]) return v1[1] - v2[1]

// 패치 버전 비교
return v1[2] - v2[2]
}

/**
* 현재 앱 버전이 최소 요구 버전보다 낮은지 확인하는 함수
*
* @param currentVersion 현재 앱의 버전 (예: "1.0.0")
* @param minVersion 최소 요구 버전 (Firebase Remote Config에서 가져온 값)
* @return true면 강제 업데이트 필요 (현재 버전 < 최소 요구 버전), false면 업데이트 불필요
*/
fun isUpdateRequired(currentVersion: String, minVersion: String): Boolean {
return compareVersions(currentVersion, minVersion) < 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import java.net.UnknownHostException
fun handleException(
exception: Throwable,
onError: (String) -> Unit,
onLoginRequired: () -> Unit,
onLoginRequired: () -> Unit = {},
) {
when {
exception is HttpException && exception.code() == 401 -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.ninecraft.booket.core.data.api.repository

interface RemoteConfigRepository {
suspend fun getLatestVersion(): Result<String>
suspend fun shouldUpdate(): Result<Boolean>
}
10 changes: 10 additions & 0 deletions core/data/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ plugins {

android {
namespace = "com.ninecraft.booket.core.data.impl"

buildFeatures {
buildConfig = true
}

defaultConfig {
buildConfigField("String", "APP_VERSION", "\"${libs.versions.versionName.get()}\"")
}
}

dependencies {
Expand All @@ -18,6 +26,8 @@ dependencies {
projects.core.model,
projects.core.network,

platform(libs.firebase.bom),
libs.firebase.remote.config,
libs.logger,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.ninecraft.booket.core.data.impl.di

import com.google.firebase.Firebase
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.remoteConfig
import com.google.firebase.remoteconfig.remoteConfigSettings
import com.ninecraft.booket.core.data.impl.BuildConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@InstallIn(SingletonComponent::class)
@Module
internal object FirebaseModule {
@Singleton
@Provides
fun provideRemoteConfig(): FirebaseRemoteConfig {
return Firebase.remoteConfig.apply {
val configSettings by lazy {
remoteConfigSettings {
minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0 else 60
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

minimumFetchIntervalInSeconds 타입(Int→Long) 불일치 가능성 — L 접미사 필요

minimumFetchIntervalInSeconds는 Long 타입입니다. 현재 0/60은 Int 리터럴이라 컴파일 에러가 발생할 수 있습니다. Long 리터럴로 수정해주세요.

-                    minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0 else 60
+                    minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0L else 60L
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0 else 60
minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0L else 60L
🤖 Prompt for AI Agents
In
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/FirebaseModule.kt
around line 23, the assigned literals for minimumFetchIntervalInSeconds are Ints
but the parameter expects a Long; change the literals to Long (add the L suffix)
so use 0L when DEBUG and 60L otherwise (e.g., minimumFetchIntervalInSeconds = if
(BuildConfig.DEBUG) 0L else 60L).

}
}
setConfigSettingsAsync(configSettings)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package com.ninecraft.booket.core.data.impl.di
import com.ninecraft.booket.core.data.api.repository.AuthRepository
import com.ninecraft.booket.core.data.api.repository.BookRepository
import com.ninecraft.booket.core.data.api.repository.RecordRepository
import com.ninecraft.booket.core.data.api.repository.RemoteConfigRepository
import com.ninecraft.booket.core.data.api.repository.UserRepository
import com.ninecraft.booket.core.data.impl.repository.DefaultAuthRepository
import com.ninecraft.booket.core.data.impl.repository.DefaultBookRepository
import com.ninecraft.booket.core.data.impl.repository.DefaultRecordRepository
import com.ninecraft.booket.core.data.impl.repository.DefaultRemoteConfigRepository
import com.ninecraft.booket.core.data.impl.repository.DefaultUserRepository
import dagger.Binds
import dagger.Module
Expand All @@ -33,4 +35,8 @@ internal abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindRecordRepository(defaultRecordRepository: DefaultRecordRepository): RecordRepository

@Binds
@Singleton
abstract fun bindRemoteConfigRepository(defaultRemoteConfigRepository: DefaultRemoteConfigRepository): RemoteConfigRepository
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.ninecraft.booket.core.data.impl.repository

import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.get
import com.ninecraft.booket.core.common.util.isUpdateRequired
import com.ninecraft.booket.core.data.api.repository.RemoteConfigRepository
import com.ninecraft.booket.core.data.impl.BuildConfig
import com.orhanobut.logger.Logger
import kotlinx.coroutines.suspendCancellableCoroutine
import javax.inject.Inject
import kotlin.coroutines.resume

class DefaultRemoteConfigRepository @Inject constructor(
private val remoteConfig: FirebaseRemoteConfig,
) : RemoteConfigRepository {
override suspend fun getLatestVersion(): Result<String> = suspendCancellableCoroutine { continuation ->
remoteConfig.fetchAndActivate().addOnCompleteListener { task ->
if (task.isSuccessful) {
val latestVersion = remoteConfig[KEY_LATEST_VERSION].asString()
Logger.d("LatestVersion: $latestVersion")
continuation.resume(Result.success(latestVersion))
} else {
Logger.e(task.exception, "getLatestVersion failed")
continuation.resume(Result.failure(task.exception ?: Exception("Unknown error")))
}
}
}
Comment on lines +16 to +27
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

코루틴 취소 시 resume 중복 호출 가능성 — 안전 가드와 캐시 폴백 추가 필요

suspendCancellableCoroutine + addOnCompleteListener 조합에서는 코루틴이 취소된 뒤에도 리스너가 호출될 수 있어, continuation.resume을 호출하면 IllegalStateException이 발생할 수 있습니다. 또한 fetch 실패 시 활성화된 기존 값으로의 폴백이 있으면 UX가 좋아집니다.

다음과 같이 isActive 가드와 캐시 폴백을 추가해 주세요:

-    override suspend fun getLatestVersion(): Result<String> = suspendCancellableCoroutine { continuation ->
-        remoteConfig.fetchAndActivate().addOnCompleteListener { task ->
-            if (task.isSuccessful) {
-                val latestVersion = remoteConfig[KEY_LATEST_VERSION].asString()
-                Logger.d("LatestVersion: $latestVersion")
-                continuation.resume(Result.success(latestVersion))
-            } else {
-                Logger.e(task.exception, "getLatestVersion failed")
-                continuation.resume(Result.failure(task.exception ?: Exception("Unknown error")))
-            }
-        }
-    }
+    override suspend fun getLatestVersion(): Result<String> = suspendCancellableCoroutine { continuation ->
+        remoteConfig.fetchAndActivate().addOnCompleteListener { task ->
+            if (!continuation.isActive) return@addOnCompleteListener
+            if (task.isSuccessful) {
+                val latestVersion = remoteConfig[KEY_LATEST_VERSION]
+                    .asString()
+                    .takeIf { it.isNotBlank() } ?: "Unknown"
+                Logger.d("LatestVersion: $latestVersion")
+                continuation.resume(Result.success(latestVersion))
+            } else {
+                Logger.e(task.exception, "getLatestVersion failed")
+                // fetch 실패 시에도 마지막 활성화된 값을 시도
+                val cached = remoteConfig[KEY_LATEST_VERSION].asString()
+                if (cached.isNotBlank()) {
+                    continuation.resume(Result.success(cached))
+                } else {
+                    continuation.resume(Result.failure(task.exception ?: Exception("Unknown error")))
+                }
+            }
+        }
+    }

추가로, 필요하다면 continuation.invokeOnCancellation { /* no-op */ }를 등록해 의도를 더 분명히 할 수 있습니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override suspend fun getLatestVersion(): Result<String> = suspendCancellableCoroutine { continuation ->
remoteConfig.fetchAndActivate().addOnCompleteListener { task ->
if (task.isSuccessful) {
val latestVersion = remoteConfig[KEY_LATEST_VERSION].asString()
Logger.d("LatestVersion: $latestVersion")
continuation.resume(Result.success(latestVersion))
} else {
Logger.e(task.exception, "getLatestVersion failed")
continuation.resume(Result.failure(task.exception ?: Exception("Unknown error")))
}
}
}
override suspend fun getLatestVersion(): Result<String> = suspendCancellableCoroutine { continuation ->
remoteConfig.fetchAndActivate().addOnCompleteListener { task ->
if (!continuation.isActive) return@addOnCompleteListener
if (task.isSuccessful) {
val latestVersion = remoteConfig[KEY_LATEST_VERSION]
.asString()
.takeIf { it.isNotBlank() } ?: "Unknown"
Logger.d("LatestVersion: $latestVersion")
continuation.resume(Result.success(latestVersion))
} else {
Logger.e(task.exception, "getLatestVersion failed")
// fetch 실패 시에도 마지막 활성화된 값을 시도
val cached = remoteConfig[KEY_LATEST_VERSION].asString()
if (cached.isNotBlank()) {
continuation.resume(Result.success(cached))
} else {
continuation.resume(Result.failure(task.exception ?: Exception("Unknown error")))
}
}
}
}
🤖 Prompt for AI Agents
In
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultRemoteConfigRepository.kt
around lines 14 to 25, the coroutine continuation may be resumed after
cancellation which can throw IllegalStateException and there is no fallback to
an activated cached value on fetch failure; guard resume calls with
continuation.isActive checks (only call continuation.resume(...) when isActive),
register continuation.invokeOnCancellation { /* no-op */ } to make intent
explicit, and on fetch failure try to read the currently activated cached value
(e.g. remoteConfig[KEY_LATEST_VERSION].asString()) and resume with
Result.success(fallback) if non-empty, otherwise resume with
Result.failure(task.exception ?: Exception("Unknown error")); ensure only one
resume path executes by checking isActive before each resume.


override suspend fun shouldUpdate(): Result<Boolean> = suspendCancellableCoroutine { continuation ->
remoteConfig.fetchAndActivate().addOnCompleteListener { task ->
if (task.isSuccessful) {
val minVersion = remoteConfig[KEY_MIN_VERSION].asString()
val currentVersion = BuildConfig.APP_VERSION
continuation.resume(Result.success(isUpdateRequired(currentVersion, minVersion)))
} else {
Logger.e(task.exception, "shouldUpdate: getMinVersion failed")
continuation.resume(Result.failure(task.exception ?: Exception("Unknown error")))
}
}
}

companion object {
private const val KEY_LATEST_VERSION = "LatestVersion"
private const val KEY_MIN_VERSION = "MinVersion"
}
}
2 changes: 1 addition & 1 deletion core/ui/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="no_more_result">더 이상 결과가 없습니다</string>
<string name="retry">다시 시도</string>
<string name="retry">다시 시도하기</string>
<string name="network_error_message">네트워크 연결이 불안정합니다.\n인터넷 연결을 확인해주세요</string>
<string name="server_error_message">알 수 없는 문제가 발생했어요.\n다시 시도해주세요</string>
</resources>
1 change: 0 additions & 1 deletion feature/home/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,4 @@
<string name="book_card_record">기록하기</string>
<string name="book_card_unit_count">개</string>
<string name="home_error_title">책 정보를 가져오는데 실패했어요</string>
<string name="home_retry">다시 시도</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package com.ninecraft.booket.feature.settings
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import com.ninecraft.booket.core.common.extensions.openPlayStore
import com.skydoves.compose.effects.RememberedEffect

@Composable
internal fun HandleSettingsSideEffects(
state: SettingsUiState,
eventSink: (SettingsUiEvent) -> Unit,
) {
val context = LocalContext.current

Expand All @@ -16,7 +18,16 @@ internal fun HandleSettingsSideEffects(
is SettingsSideEffect.ShowToast -> {
Toast.makeText(context, state.sideEffect.message, Toast.LENGTH_SHORT).show()
}
null -> {}

is SettingsSideEffect.NavigateToPlayStore -> {
context.openPlayStore()
}

else -> {}
}

if (state.sideEffect != null) {
eventSink(SettingsUiEvent.InitSideEffect)
}
}
}
Loading
Loading