Skip to content

Commit 4069548

Browse files
committed
[BOOK-355] feat: 알림 토글 상태를 dataStore에 동기화
1 parent 39b097d commit 4069548

File tree

4 files changed

+92
-24
lines changed

4 files changed

+92
-24
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,8 @@ interface UserRepository {
1313
val onboardingState: Flow<OnboardingState>
1414

1515
suspend fun setOnboardingCompleted(isCompleted: Boolean)
16+
17+
val isNotificationEnabled: Flow<Boolean>
18+
19+
suspend fun setNotificationEnabled(isEnabled: Boolean)
1620
}

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

Lines changed: 8 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.ninecraft.booket.core.common.utils.runSuspendCatching
44
import com.ninecraft.booket.core.data.api.repository.UserRepository
55
import com.ninecraft.booket.core.data.impl.mapper.toModel
6+
import com.ninecraft.booket.core.datastore.api.datasource.NotificationDataSource
67
import com.ninecraft.booket.core.datastore.api.datasource.OnboardingDataSource
78
import com.ninecraft.booket.core.network.request.TermsAgreementRequest
89
import com.ninecraft.booket.core.network.service.ReedService
@@ -11,6 +12,7 @@ import javax.inject.Inject
1112
internal class DefaultUserRepository @Inject constructor(
1213
private val service: ReedService,
1314
private val onboardingDataSource: OnboardingDataSource,
15+
private val notificationDataSource: NotificationDataSource,
1416
) : UserRepository {
1517
override suspend fun agreeTerms(termsAgreed: Boolean) = runSuspendCatching {
1618
service.agreeTerms(TermsAgreementRequest(termsAgreed)).toModel()
@@ -25,4 +27,10 @@ internal class DefaultUserRepository @Inject constructor(
2527
override suspend fun setOnboardingCompleted(isCompleted: Boolean) {
2628
onboardingDataSource.setOnboardingCompleted(isCompleted)
2729
}
30+
31+
override val isNotificationEnabled = notificationDataSource.isNotificationEnabled
32+
33+
override suspend fun setNotificationEnabled(isEnabled: Boolean) {
34+
notificationDataSource.setNotificationEnabled(isEnabled)
35+
}
2836
}

feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationPresenter.kt

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,27 @@ package com.ninecraft.booket.feature.settings.notification
22

33
import androidx.compose.runtime.Composable
44
import androidx.compose.runtime.getValue
5-
import androidx.compose.runtime.mutableStateOf
6-
import androidx.compose.runtime.setValue
5+
import androidx.compose.runtime.rememberCoroutineScope
6+
import com.ninecraft.booket.core.data.api.repository.UserRepository
77
import com.ninecraft.booket.feature.screens.NotificationScreen
88
import com.slack.circuit.codegen.annotations.CircuitInject
9-
import com.slack.circuit.retained.rememberRetained
9+
import com.slack.circuit.retained.collectAsRetainedState
1010
import com.slack.circuit.runtime.Navigator
1111
import com.slack.circuit.runtime.presenter.Presenter
1212
import dagger.assisted.Assisted
1313
import dagger.assisted.AssistedFactory
1414
import dagger.assisted.AssistedInject
1515
import dagger.hilt.android.components.ActivityRetainedComponent
16+
import kotlinx.coroutines.launch
1617

1718
class NotificationPresenter @AssistedInject constructor(
1819
@Assisted val navigator: Navigator,
20+
private val userRepository: UserRepository,
1921
) : Presenter<NotificationUiState> {
2022
@Composable
2123
override fun present(): NotificationUiState {
22-
var isNotificationEnabled by rememberRetained { mutableStateOf(false) }
24+
val scope = rememberCoroutineScope()
25+
val isNotificationEnabled by userRepository.isNotificationEnabled.collectAsRetainedState(initial = false)
2326

2427
fun handleEvent(event: NotificationUiEvent) {
2528
when (event) {
@@ -28,7 +31,9 @@ class NotificationPresenter @AssistedInject constructor(
2831
}
2932

3033
is NotificationUiEvent.OnNotificationToggle -> {
31-
isNotificationEnabled = event.enabled
34+
scope.launch {
35+
userRepository.setNotificationEnabled(event.enabled)
36+
}
3237
}
3338
}
3439
}

feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUi.kt

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package com.ninecraft.booket.feature.settings.notification
22

3+
import android.content.Context
34
import android.content.Intent
4-
import android.net.Uri
5+
import android.content.pm.PackageManager
6+
import android.os.Build
57
import android.provider.Settings
68
import androidx.activity.compose.rememberLauncherForActivityResult
79
import androidx.activity.result.contract.ActivityResultContracts
10+
import androidx.annotation.RequiresApi
811
import androidx.compose.foundation.background
912
import androidx.compose.foundation.layout.Arrangement
1013
import androidx.compose.foundation.layout.Column
@@ -18,12 +21,19 @@ import androidx.compose.foundation.shape.RoundedCornerShape
1821
import androidx.compose.material3.Icon
1922
import androidx.compose.material3.Text
2023
import androidx.compose.runtime.Composable
24+
import androidx.compose.runtime.LaunchedEffect
25+
import androidx.compose.runtime.getValue
26+
import androidx.compose.runtime.produceState
2127
import androidx.compose.ui.Alignment
2228
import androidx.compose.ui.Modifier
2329
import androidx.compose.ui.graphics.vector.ImageVector
2430
import androidx.compose.ui.platform.LocalContext
2531
import androidx.compose.ui.res.stringResource
2632
import androidx.compose.ui.res.vectorResource
33+
import androidx.core.content.ContextCompat
34+
import androidx.lifecycle.Lifecycle
35+
import androidx.lifecycle.LifecycleEventObserver
36+
import androidx.lifecycle.compose.LocalLifecycleOwner
2737
import com.ninecraft.booket.core.common.extensions.noRippleClickable
2838
import com.ninecraft.booket.core.designsystem.DevicePreview
2939
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
@@ -43,6 +53,33 @@ internal fun NotificationUi(
4353
state: NotificationUiState,
4454
modifier: Modifier = Modifier,
4555
) {
56+
val context = LocalContext.current
57+
val lifecycleOwner = LocalLifecycleOwner.current
58+
59+
val isGranted by produceState(
60+
initialValue = checkNotificationPermission(context),
61+
key1 = lifecycleOwner,
62+
) {
63+
// 포그라운드 복귀 시 OS 권한 동기화
64+
val observer = LifecycleEventObserver { _, event ->
65+
if (event == Lifecycle.Event.ON_RESUME) {
66+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
67+
value = checkNotificationPermission(context)
68+
}
69+
}
70+
}
71+
lifecycleOwner.lifecycle.addObserver(observer)
72+
awaitDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
73+
}
74+
75+
val settingsLauncher = rememberLauncherForActivityResult(
76+
contract = ActivityResultContracts.StartActivityForResult(),
77+
) { _ -> }
78+
79+
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
80+
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
81+
}
82+
4683
ReedScaffold(
4784
modifier = modifier.fillMaxSize(),
4885
containerColor = White,
@@ -59,42 +96,47 @@ internal fun NotificationUi(
5996
state.eventSink(NotificationUiEvent.OnBackClick)
6097
},
6198
)
99+
if (!isGranted) {
100+
NotificationGuideItem(
101+
onClick = {
102+
settingsLauncher.launch(intent)
103+
},
104+
)
105+
}
62106
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2))
63-
NotificationGuideItem()
64-
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4))
65107
ToggleItem(
66108
title = stringResource(R.string.notification_toggle_title),
67109
description = stringResource(R.string.notification_toggle_description),
68-
isChecked = state.isNotificationEnabled,
110+
isChecked = isGranted && state.isNotificationEnabled,
69111
onCheckedChange = { enabled ->
70-
state.eventSink(NotificationUiEvent.OnNotificationToggle(enabled))
112+
if (isGranted) {
113+
state.eventSink(NotificationUiEvent.OnNotificationToggle(enabled))
114+
} else {
115+
settingsLauncher.launch(intent)
116+
}
71117
},
72118
)
73119
}
74120
}
75121
}
76122

77123
@Composable
78-
internal fun NotificationGuideItem() {
79-
val context = LocalContext.current
80-
val settingsLauncher = rememberLauncherForActivityResult(
81-
contract = ActivityResultContracts.StartActivityForResult(),
82-
) { _ -> }
83-
124+
internal fun NotificationGuideItem(
125+
onClick: () -> Unit,
126+
modifier: Modifier = Modifier,
127+
) {
84128
Row(
85-
modifier = Modifier
86-
.padding(horizontal = ReedTheme.spacing.spacing5)
129+
modifier = modifier
130+
.padding(
131+
vertical = ReedTheme.spacing.spacing2,
132+
horizontal = ReedTheme.spacing.spacing5,
133+
)
87134
.fillMaxWidth()
88135
.background(
89136
color = ReedTheme.colors.baseSecondary,
90137
shape = RoundedCornerShape(ReedTheme.radius.md),
91138
)
92-
.noRippleClickable {
93-
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
94-
data = Uri.fromParts("package", context.packageName, null)
95-
}
96-
settingsLauncher.launch(intent)
97-
}
139+
.noRippleClickable { onClick() }
98140
.padding(
99141
vertical = ReedTheme.spacing.spacing6,
100142
horizontal = ReedTheme.spacing.spacing5,
@@ -122,6 +164,15 @@ internal fun NotificationGuideItem() {
122164
}
123165
}
124166

167+
private fun checkNotificationPermission(context: Context): Boolean {
168+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
169+
ContextCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
170+
} else {
171+
true
172+
}
173+
}
174+
175+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
125176
@DevicePreview
126177
@Composable
127178
private fun NotificationUiPreview() {

0 commit comments

Comments
 (0)