From 7ac5be1c08c559854e0f30c860286a408e06b1cf Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 17 Oct 2025 03:59:54 +0900 Subject: [PATCH 1/8] =?UTF-8?q?[BOOK-355]=20feat:=20Notification=20Datasto?= =?UTF-8?q?re=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/datasource/NotificationDataSource.kt | 8 +++++ .../DefaultNotificationDataSource.kt | 32 +++++++++++++++++++ .../core/datastore/impl/di/DataStoreModule.kt | 17 ++++++++++ .../datastore/impl/di/DataStoreQualifier.kt | 4 +++ 4 files changed, 61 insertions(+) create mode 100644 core/datastore/api/src/main/kotlin/com/ninecraft/booket/core/datastore/api/datasource/NotificationDataSource.kt create mode 100644 core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt diff --git a/core/datastore/api/src/main/kotlin/com/ninecraft/booket/core/datastore/api/datasource/NotificationDataSource.kt b/core/datastore/api/src/main/kotlin/com/ninecraft/booket/core/datastore/api/datasource/NotificationDataSource.kt new file mode 100644 index 00000000..6b0d1483 --- /dev/null +++ b/core/datastore/api/src/main/kotlin/com/ninecraft/booket/core/datastore/api/datasource/NotificationDataSource.kt @@ -0,0 +1,8 @@ +package com.ninecraft.booket.core.datastore.api.datasource + +import kotlinx.coroutines.flow.Flow + +interface NotificationDataSource { + val isNotificationEnabled: Flow + suspend fun setNotificationEnabled(isEnabled: Boolean) +} diff --git a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt new file mode 100644 index 00000000..a6ff06ea --- /dev/null +++ b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt @@ -0,0 +1,32 @@ +package com.ninecraft.booket.core.datastore.impl.datasource + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import com.ninecraft.booket.core.datastore.api.datasource.NotificationDataSource +import com.ninecraft.booket.core.datastore.impl.di.NotificationDataStore +import com.ninecraft.booket.core.datastore.impl.util.handleIOException +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class DefaultNotificationDataSource @Inject constructor( + @NotificationDataStore private val dataStore: DataStore, +) : NotificationDataSource { + override val isNotificationEnabled: Flow = dataStore.data + .handleIOException() + .map { prefs -> + prefs[NOTIFICATION_ENABLED] ?: false + } + + override suspend fun setNotificationEnabled(isEnabled: Boolean) { + dataStore.edit { prefs -> + prefs[NOTIFICATION_ENABLED] = isEnabled + } + } + + companion object Companion { + private val NOTIFICATION_ENABLED = booleanPreferencesKey("NOTIFICATION_ENABLED") + } +} diff --git a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreModule.kt b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreModule.kt index 08e11837..88a2c3cc 100644 --- a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreModule.kt +++ b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreModule.kt @@ -6,11 +6,13 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore import com.ninecraft.booket.core.datastore.api.datasource.BookRecentSearchDataSource import com.ninecraft.booket.core.datastore.api.datasource.LibraryRecentSearchDataSource +import com.ninecraft.booket.core.datastore.api.datasource.NotificationDataSource import com.ninecraft.booket.core.datastore.api.datasource.OnboardingDataSource import com.ninecraft.booket.core.datastore.api.datasource.TokenDataSource import com.ninecraft.booket.core.datastore.impl.datasource.DefaultLibraryRecentSearchDataSource import com.ninecraft.booket.core.datastore.impl.datasource.DefaultOnboardingDataSource import com.ninecraft.booket.core.datastore.impl.datasource.DefaultBookRecentSearchDataSource +import com.ninecraft.booket.core.datastore.impl.datasource.DefaultNotificationDataSource import com.ninecraft.booket.core.datastore.impl.datasource.DefaultTokenDataSource import dagger.Binds import dagger.Module @@ -27,11 +29,13 @@ object DataStoreModule { private const val BOOK_RECENT_SEARCH_DATASTORE_NAME = "BOOK_RECENT_SEARCH_DATASTORE" private const val LIBRARY_RECENT_SEARCH_DATASTORE_NAME = "LIBRARY_RECENT_SEARCH_DATASTORE" private const val ONBOARDING_DATASTORE_NAME = "ONBOARDING_DATASTORE" + private const val NOTIFICATION_DATASTORE_NAME = "NOTIFICATION_DATASTORE" private val Context.tokenDataStore by preferencesDataStore(name = TOKEN_DATASTORE_NAME) private val Context.bookRecentSearchDataStore by preferencesDataStore(name = BOOK_RECENT_SEARCH_DATASTORE_NAME) private val Context.libraryRecentSearchDataStore by preferencesDataStore(name = LIBRARY_RECENT_SEARCH_DATASTORE_NAME) private val Context.onboardingDataStore by preferencesDataStore(name = ONBOARDING_DATASTORE_NAME) + private val Context.notificationDataStore by preferencesDataStore(name = NOTIFICATION_DATASTORE_NAME) @TokenDataStore @Provides @@ -60,6 +64,13 @@ object DataStoreModule { fun provideOnboardingDataStore( @ApplicationContext context: Context, ): DataStore = context.onboardingDataStore + + @NotificationDataStore + @Provides + @Singleton + fun provideNotificationDataStore( + @ApplicationContext context: Context, + ): DataStore = context.notificationDataStore } @Module @@ -89,4 +100,10 @@ abstract class DataStoreBindModule { abstract fun bindOnboardingDataSource( defaultOnboardingDataSource: DefaultOnboardingDataSource, ): OnboardingDataSource + + @Binds + @Singleton + abstract fun bindNotificationDataSource( + defaultNotificationDataSource: DefaultNotificationDataSource, + ): NotificationDataSource } diff --git a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreQualifier.kt b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreQualifier.kt index 49262242..8a65ab8c 100644 --- a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreQualifier.kt +++ b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreQualifier.kt @@ -17,3 +17,7 @@ annotation class LibraryRecentSearchDataStore @Qualifier @Retention(AnnotationRetention.BINARY) annotation class OnboardingDataStore + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class NotificationDataStore From 86f97bcab4200abb8392d1022db5e520c95b0736 Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 17 Oct 2025 04:23:44 +0900 Subject: [PATCH 2/8] =?UTF-8?q?[BOOK-355]=20feat:=20=ED=99=88=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=97=90=20=EC=95=8C=EB=A6=BC=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 1 + feature/home/build.gradle.kts | 1 + .../ninecraft/booket/feature/home/HomeUi.kt | 20 +++++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7a93c3d6..5e60c23b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ + = Build.VERSION_CODES.TIRAMISU) { + val permission = android.Manifest.permission.POST_NOTIFICATIONS + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + ) { granted -> + if (!granted) { + Logger.d("Notification permission not granted") + } + } + + LaunchedEffect(Unit) { + permissionLauncher.launch(permission) + } + } + ReedScaffold( modifier = modifier.fillMaxSize(), bottomBar = { From 1190e03498ef7cb65d8dfb94cfac261eada02dcb Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 17 Oct 2025 11:08:00 +0900 Subject: [PATCH 3/8] =?UTF-8?q?[BOOK-355]=20feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=9A=94=EC=B2=AD=EC=97=90=EC=84=9C=20Gue?= =?UTF-8?q?st=20Mode=20=EB=B6=84=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../booket/feature/home/HomePresenter.kt | 6 +++- .../ninecraft/booket/feature/home/HomeUi.kt | 30 ++++++++++++------- .../booket/feature/home/HomeUiState.kt | 1 + 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt index 175b1804..3bfb3f5c 100644 --- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt +++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt @@ -11,9 +11,9 @@ import com.ninecraft.booket.core.data.api.repository.BookRepository import com.ninecraft.booket.core.model.RecentBookModel import com.ninecraft.booket.core.model.UserState import com.ninecraft.booket.feature.screens.BookDetailScreen +import com.ninecraft.booket.feature.screens.BookSearchScreen import com.ninecraft.booket.feature.screens.HomeScreen import com.ninecraft.booket.feature.screens.RecordScreen -import com.ninecraft.booket.feature.screens.BookSearchScreen import com.ninecraft.booket.feature.screens.SettingsScreen import com.skydoves.compose.effects.RememberedEffect import com.slack.circuit.codegen.annotations.CircuitInject @@ -89,6 +89,10 @@ class HomePresenter @AssistedInject constructor( restoreState = true, ) } + + is HomeUiEvent.OnNotificationPermissionResult -> { + // TODO: 서버 동기화, FCM 토큰 전송 + } } } diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt index 9f0b9dc6..3f729ff7 100644 --- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt +++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt @@ -1,5 +1,6 @@ package com.ninecraft.booket.feature.home +import android.content.pm.PackageManager import android.os.Build import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -25,8 +26,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat import com.ninecraft.booket.core.designsystem.DevicePreview import com.ninecraft.booket.core.designsystem.theme.HomeBg import com.ninecraft.booket.core.designsystem.theme.ReedTheme @@ -53,18 +56,25 @@ internal fun HomeUi( ) { HandleHomeSideEffects(state = state) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - val permission = android.Manifest.permission.POST_NOTIFICATIONS - val permissionLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestPermission(), - ) { granted -> - if (!granted) { - Logger.d("Notification permission not granted") - } - } + val context = LocalContext.current + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + ) { granted -> + state.eventSink(HomeUiEvent.OnNotificationPermissionResult(granted)) + } + if (!state.isGuestMode) { LaunchedEffect(Unit) { - permissionLauncher.launch(permission) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val permission = android.Manifest.permission.POST_NOTIFICATIONS + val isGranted = ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED + + if (!isGranted) { + permissionLauncher.launch(permission) + } + } else { + state.eventSink(HomeUiEvent.OnNotificationPermissionResult(true)) + } } } diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt index c90d001a..39377b53 100644 --- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt +++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt @@ -43,4 +43,5 @@ sealed interface HomeUiEvent : CircuitUiEvent { ) : HomeUiEvent data object OnRetryClick : HomeUiEvent data class OnTabSelected(val tab: MainTab) : HomeUiEvent + data class OnNotificationPermissionResult(val isGranted: Boolean) : HomeUiEvent } From 39b097d0ca748380afda7cb502d00b12ad1d533a Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 17 Oct 2025 11:08:20 +0900 Subject: [PATCH 4/8] =?UTF-8?q?[BOOK-355]=20feat:=20NOTIFICATION=5FENABLED?= =?UTF-8?q?=20default=EA=B0=92=20true=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../datastore/impl/datasource/DefaultNotificationDataSource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt index a6ff06ea..24db08c0 100644 --- a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt +++ b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt @@ -17,7 +17,7 @@ class DefaultNotificationDataSource @Inject constructor( override val isNotificationEnabled: Flow = dataStore.data .handleIOException() .map { prefs -> - prefs[NOTIFICATION_ENABLED] ?: false + prefs[NOTIFICATION_ENABLED] ?: true } override suspend fun setNotificationEnabled(isEnabled: Boolean) { From 40695489f6894ba5ab4ac69d7e5630ec5707a99e Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 17 Oct 2025 14:47:20 +0900 Subject: [PATCH 5/8] =?UTF-8?q?[BOOK-355]=20feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=ED=86=A0=EA=B8=80=20=EC=83=81=ED=83=9C=EB=A5=BC=20dataStore?= =?UTF-8?q?=EC=97=90=20=EB=8F=99=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/api/repository/UserRepository.kt | 4 + .../impl/repository/DefaultUserRepository.kt | 8 ++ .../notification/NotificationPresenter.kt | 15 ++-- .../settings/notification/NotificationUi.kt | 89 +++++++++++++++---- 4 files changed, 92 insertions(+), 24 deletions(-) diff --git a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt index 049c19b3..00a34c9a 100644 --- a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt +++ b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt @@ -13,4 +13,8 @@ interface UserRepository { val onboardingState: Flow suspend fun setOnboardingCompleted(isCompleted: Boolean) + + val isNotificationEnabled: Flow + + suspend fun setNotificationEnabled(isEnabled: Boolean) } diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt index f9cf7741..14531cfe 100644 --- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt @@ -3,6 +3,7 @@ package com.ninecraft.booket.core.data.impl.repository import com.ninecraft.booket.core.common.utils.runSuspendCatching import com.ninecraft.booket.core.data.api.repository.UserRepository import com.ninecraft.booket.core.data.impl.mapper.toModel +import com.ninecraft.booket.core.datastore.api.datasource.NotificationDataSource import com.ninecraft.booket.core.datastore.api.datasource.OnboardingDataSource import com.ninecraft.booket.core.network.request.TermsAgreementRequest import com.ninecraft.booket.core.network.service.ReedService @@ -11,6 +12,7 @@ import javax.inject.Inject internal class DefaultUserRepository @Inject constructor( private val service: ReedService, private val onboardingDataSource: OnboardingDataSource, + private val notificationDataSource: NotificationDataSource, ) : UserRepository { override suspend fun agreeTerms(termsAgreed: Boolean) = runSuspendCatching { service.agreeTerms(TermsAgreementRequest(termsAgreed)).toModel() @@ -25,4 +27,10 @@ internal class DefaultUserRepository @Inject constructor( override suspend fun setOnboardingCompleted(isCompleted: Boolean) { onboardingDataSource.setOnboardingCompleted(isCompleted) } + + override val isNotificationEnabled = notificationDataSource.isNotificationEnabled + + override suspend fun setNotificationEnabled(isEnabled: Boolean) { + notificationDataSource.setNotificationEnabled(isEnabled) + } } diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationPresenter.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationPresenter.kt index 6b868357..706b8488 100644 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationPresenter.kt +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationPresenter.kt @@ -2,24 +2,27 @@ package com.ninecraft.booket.feature.settings.notification import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue +import androidx.compose.runtime.rememberCoroutineScope +import com.ninecraft.booket.core.data.api.repository.UserRepository import com.ninecraft.booket.feature.screens.NotificationScreen import com.slack.circuit.codegen.annotations.CircuitInject -import com.slack.circuit.retained.rememberRetained +import com.slack.circuit.retained.collectAsRetainedState import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.presenter.Presenter import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.components.ActivityRetainedComponent +import kotlinx.coroutines.launch class NotificationPresenter @AssistedInject constructor( @Assisted val navigator: Navigator, + private val userRepository: UserRepository, ) : Presenter { @Composable override fun present(): NotificationUiState { - var isNotificationEnabled by rememberRetained { mutableStateOf(false) } + val scope = rememberCoroutineScope() + val isNotificationEnabled by userRepository.isNotificationEnabled.collectAsRetainedState(initial = false) fun handleEvent(event: NotificationUiEvent) { when (event) { @@ -28,7 +31,9 @@ class NotificationPresenter @AssistedInject constructor( } is NotificationUiEvent.OnNotificationToggle -> { - isNotificationEnabled = event.enabled + scope.launch { + userRepository.setNotificationEnabled(event.enabled) + } } } } diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUi.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUi.kt index 3c1d475a..1b8c6ef0 100644 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUi.kt +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUi.kt @@ -1,10 +1,13 @@ package com.ninecraft.booket.feature.settings.notification +import android.content.Context import android.content.Intent -import android.net.Uri +import android.content.pm.PackageManager +import android.os.Build import android.provider.Settings import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -18,12 +21,19 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner import com.ninecraft.booket.core.common.extensions.noRippleClickable import com.ninecraft.booket.core.designsystem.DevicePreview import com.ninecraft.booket.core.designsystem.theme.ReedTheme @@ -43,6 +53,33 @@ internal fun NotificationUi( state: NotificationUiState, modifier: Modifier = Modifier, ) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + val isGranted by produceState( + initialValue = checkNotificationPermission(context), + key1 = lifecycleOwner, + ) { + // 포그라운드 복귀 시 OS 권한 동기화 + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + value = checkNotificationPermission(context) + } + } + } + lifecycleOwner.lifecycle.addObserver(observer) + awaitDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } + + val settingsLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + ) { _ -> } + + val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } + ReedScaffold( modifier = modifier.fillMaxSize(), containerColor = White, @@ -59,15 +96,24 @@ internal fun NotificationUi( state.eventSink(NotificationUiEvent.OnBackClick) }, ) + if (!isGranted) { + NotificationGuideItem( + onClick = { + settingsLauncher.launch(intent) + }, + ) + } Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) - NotificationGuideItem() - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) ToggleItem( title = stringResource(R.string.notification_toggle_title), description = stringResource(R.string.notification_toggle_description), - isChecked = state.isNotificationEnabled, + isChecked = isGranted && state.isNotificationEnabled, onCheckedChange = { enabled -> - state.eventSink(NotificationUiEvent.OnNotificationToggle(enabled)) + if (isGranted) { + state.eventSink(NotificationUiEvent.OnNotificationToggle(enabled)) + } else { + settingsLauncher.launch(intent) + } }, ) } @@ -75,26 +121,22 @@ internal fun NotificationUi( } @Composable -internal fun NotificationGuideItem() { - val context = LocalContext.current - val settingsLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult(), - ) { _ -> } - +internal fun NotificationGuideItem( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { Row( - modifier = Modifier - .padding(horizontal = ReedTheme.spacing.spacing5) + modifier = modifier + .padding( + vertical = ReedTheme.spacing.spacing2, + horizontal = ReedTheme.spacing.spacing5, + ) .fillMaxWidth() .background( color = ReedTheme.colors.baseSecondary, shape = RoundedCornerShape(ReedTheme.radius.md), ) - .noRippleClickable { - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.fromParts("package", context.packageName, null) - } - settingsLauncher.launch(intent) - } + .noRippleClickable { onClick() } .padding( vertical = ReedTheme.spacing.spacing6, horizontal = ReedTheme.spacing.spacing5, @@ -122,6 +164,15 @@ internal fun NotificationGuideItem() { } } +private fun checkNotificationPermission(context: Context): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED + } else { + true + } +} + +@RequiresApi(Build.VERSION_CODES.TIRAMISU) @DevicePreview @Composable private fun NotificationUiPreview() { From 94aaa4cbf1a1cf009ee206fb9fc5bed201cc073f Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 17 Oct 2025 14:50:26 +0900 Subject: [PATCH 6/8] =?UTF-8?q?[BOOK-355]=20feat:=20Guest=20Mode=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=84=A4=EC=A0=95=20=EC=95=8C=EB=A6=BC=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20=EB=B9=84=EB=85=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../booket/feature/settings/SettingsUi.kt | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt index cc14a049..d2b13b71 100644 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt @@ -98,19 +98,21 @@ internal fun SettingsUi( ) }, ) - SettingItem( - title = stringResource(R.string.settings_notification), - onItemClick = { - state.eventSink(SettingsUiEvent.OnNotificationClick) - }, - action = { - Icon( - imageVector = ImageVector.vectorResource(id = designR.drawable.ic_chevron_right), - contentDescription = "Right Chevron Icon", - tint = Color.Unspecified, - ) - }, - ) + if (!state.isGuestMode) { + SettingItem( + title = stringResource(R.string.settings_notification), + onItemClick = { + state.eventSink(SettingsUiEvent.OnNotificationClick) + }, + action = { + Icon( + imageVector = ImageVector.vectorResource(id = designR.drawable.ic_chevron_right), + contentDescription = "Right Chevron Icon", + tint = Color.Unspecified, + ) + }, + ) + } SettingItem( title = stringResource(R.string.settings_terms_of_service), onItemClick = { From e9969fd36c72178f115f97927784df6b738a43d3 Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 17 Oct 2025 15:10:02 +0900 Subject: [PATCH 7/8] [BOOK-355] chore: code style check success --- .../src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt | 1 - .../booket/feature/settings/notification/NotificationUi.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt index 3f729ff7..8219946c 100644 --- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt +++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt @@ -43,7 +43,6 @@ import com.ninecraft.booket.feature.home.component.HomeHeader import com.ninecraft.booket.feature.screens.HomeScreen import com.ninecraft.booket.feature.screens.component.MainBottomBar import com.ninecraft.booket.feature.screens.component.MainTab -import com.orhanobut.logger.Logger import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent import kotlinx.collections.immutable.toImmutableList diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUi.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUi.kt index 1b8c6ef0..72963bdb 100644 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUi.kt +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUi.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.ui.Alignment From c0a83f15edd4884753648f04d314963bb16f0081 Mon Sep 17 00:00:00 2001 From: seoyoon Date: Fri, 17 Oct 2025 18:02:36 +0900 Subject: [PATCH 8/8] =?UTF-8?q?[BOOK-355]=20fix:=20Android=2013=20?= =?UTF-8?q?=EB=AF=B8=EB=A7=8C=EC=97=90=EC=84=9C=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20=EC=95=8C=EB=A6=BC=20=EB=B9=84=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94=20=EC=8B=9C=20=EB=8F=99=EA=B8=B0=ED=99=94=20=EC=95=88?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../settings/notification/NotificationUi.kt | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUi.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUi.kt index 72963bdb..39377518 100644 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUi.kt +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUi.kt @@ -2,7 +2,6 @@ package com.ninecraft.booket.feature.settings.notification import android.content.Context import android.content.Intent -import android.content.pm.PackageManager import android.os.Build import android.provider.Settings import androidx.activity.compose.rememberLauncherForActivityResult @@ -29,7 +28,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource -import androidx.core.content.ContextCompat +import androidx.core.app.NotificationManagerCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner @@ -62,9 +61,7 @@ internal fun NotificationUi( // 포그라운드 복귀 시 OS 권한 동기화 val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - value = checkNotificationPermission(context) - } + value = checkNotificationPermission(context) } } lifecycleOwner.lifecycle.addObserver(observer) @@ -164,11 +161,8 @@ internal fun NotificationGuideItem( } private fun checkNotificationPermission(context: Context): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - ContextCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED - } else { - true - } + val notificationManager = NotificationManagerCompat.from(context) + return notificationManager.areNotificationsEnabled() } @RequiresApi(Build.VERSION_CODES.TIRAMISU)