diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6eabb63b..e7abd75a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,6 +11,7 @@
+
= dataStore.data
.catch { emit(emptyPreferences()) }
@@ -61,9 +63,9 @@ class UserPreferences @Inject constructor(
.map { it[Keys.FORTUNE_ID] }
.distinctUntilChanged()
- val fortuneDateFlow: Flow = dataStore.data
+ val fortuneDateEpochFlow: Flow = dataStore.data
.catch { emit(emptyPreferences()) }
- .map { it[Keys.FORTUNE_DATE] }
+ .map { it[Keys.FORTUNE_DATE_EPOCH] }
.distinctUntilChanged()
val fortuneImageIdFlow: Flow = dataStore.data
@@ -79,18 +81,17 @@ class UserPreferences @Inject constructor(
val hasUnseenFortuneFlow: Flow = dataStore.data
.catch { emit(emptyPreferences()) }
.map { pref ->
- pref[Keys.FORTUNE_DATE] == today() &&
- pref[Keys.FORTUNE_ID] != null &&
- (pref[Keys.FORTUNE_SEEN] != true)
+ val isToday = pref[Keys.FORTUNE_DATE_EPOCH] == todayEpoch()
+ isToday && (pref[Keys.FORTUNE_ID] != null) && (pref[Keys.FORTUNE_SEEN] != true)
}
.distinctUntilChanged()
val shouldShowFortuneToolTipFlow: Flow = dataStore.data
.catch { emit(emptyPreferences()) }
.map { pref ->
- val hasTodayFortune = pref[Keys.FORTUNE_DATE] == today() && pref[Keys.FORTUNE_ID] != null
- val tooltipNotShown = pref[Keys.FORTUNE_TOOLTIP_SHOWN] ?: false
- hasTodayFortune && !tooltipNotShown
+ val hasTodayFortune = (pref[Keys.FORTUNE_DATE_EPOCH] == todayEpoch()) && (pref[Keys.FORTUNE_ID] != null)
+ val tooltipShown = pref[Keys.FORTUNE_TOOLTIP_SHOWN] ?: false
+ hasTodayFortune && !tooltipShown
}
.distinctUntilChanged()
@@ -108,27 +109,31 @@ class UserPreferences @Inject constructor(
.catch { emit(emptyPreferences()) }
.map { pref ->
val flag = pref[Keys.FIRST_ALARM_DISMISSED_TODAY] ?: false
- val date = pref[Keys.FIRST_ALARM_DISMISSED_DATE]
- flag && date == today()
+ val isToday = pref[Keys.FIRST_ALARM_DISMISSED_DATE_EPOCH] == todayEpoch()
+ flag && isToday
}
.distinctUntilChanged()
+ val updateNoticeDontShowVersionFlow: Flow = dataStore.data
+ .catch { emit(emptyPreferences()) }
+ .map { it[Keys.UPDATE_NOTICE_DONT_SHOW_VERSION] }
+ .distinctUntilChanged()
+
+ val updateNoticeLastShownDateEpochFlow: Flow = dataStore.data
+ .catch { emit(emptyPreferences()) }
+ .map { it[Keys.UPDATE_NOTICE_LAST_SHOWN_DATE_EPOCH] }
+ .distinctUntilChanged()
+
suspend fun saveUserId(userId: Long) {
- dataStore.edit { pref ->
- pref[Keys.USER_ID] = userId
- }
+ dataStore.edit { it[Keys.USER_ID] = userId }
}
suspend fun saveUserName(userName: String) {
- dataStore.edit { pref ->
- pref[Keys.USER_NAME] = userName
- }
+ dataStore.edit { it[Keys.USER_NAME] = userName }
}
suspend fun setOnboardingCompleted() {
- dataStore.edit { pref ->
- pref[Keys.ONBOARDING_COMPLETED] = true
- }
+ dataStore.edit { it[Keys.ONBOARDING_COMPLETED] = true }
}
suspend fun markFortuneCreating() {
@@ -140,10 +145,12 @@ class UserPreferences @Inject constructor(
suspend fun markFortuneCreated(fortuneId: Long) {
dataStore.edit { pref ->
- val isNewForToday = pref[Keys.FORTUNE_ID] != fortuneId || pref[Keys.FORTUNE_DATE] != today()
+ val today = todayEpoch()
+ val prevDate = pref[Keys.FORTUNE_DATE_EPOCH]
+ val isNewForToday = (pref[Keys.FORTUNE_ID] != fortuneId) || (prevDate != today)
pref[Keys.FORTUNE_ID] = fortuneId
- pref[Keys.FORTUNE_DATE] = today()
+ pref[Keys.FORTUNE_DATE_EPOCH] = today
pref[Keys.FORTUNE_CREATING] = false
pref[Keys.FORTUNE_FAILED] = false
@@ -162,44 +169,46 @@ class UserPreferences @Inject constructor(
}
suspend fun markFortuneSeen() {
- dataStore.edit { pref ->
- pref[Keys.FORTUNE_SEEN] = true
- }
+ dataStore.edit { it[Keys.FORTUNE_SEEN] = true }
}
suspend fun markFortuneTooltipShown() {
- dataStore.edit { pref ->
- pref[Keys.FORTUNE_TOOLTIP_SHOWN] = true
- }
+ dataStore.edit { it[Keys.FORTUNE_TOOLTIP_SHOWN] = true }
}
suspend fun saveFortuneImageId(imageResId: Int) {
- dataStore.edit { pref ->
- pref[Keys.FORTUNE_IMAGE_ID] = imageResId
- }
+ dataStore.edit { it[Keys.FORTUNE_IMAGE_ID] = imageResId }
}
suspend fun saveFortuneScore(score: Int) {
- dataStore.edit { pref ->
- pref[Keys.FORTUNE_SCORE] = score
- }
+ dataStore.edit { it[Keys.FORTUNE_SCORE] = score }
}
suspend fun markFirstAlarmDismissedToday() {
dataStore.edit { pref ->
pref[Keys.FIRST_ALARM_DISMISSED_TODAY] = true
- pref[Keys.FIRST_ALARM_DISMISSED_DATE] = today()
+ pref[Keys.FIRST_ALARM_DISMISSED_DATE_EPOCH] = todayEpoch()
+ }
+ }
+
+ suspend fun markUpdateNoticeDontShow(version: String) {
+ dataStore.edit { it[Keys.UPDATE_NOTICE_DONT_SHOW_VERSION] = version }
+ }
+
+ suspend fun markUpdateNoticeShownToday() {
+ dataStore.edit { pref ->
+ pref[Keys.UPDATE_NOTICE_LAST_SHOWN_DATE_EPOCH] = todayEpoch()
}
}
suspend fun clearUserData() {
- dataStore.edit { pref -> pref.clear() }
+ dataStore.edit { it.clear() }
}
suspend fun clearFortuneData() {
dataStore.edit { pref ->
pref.remove(Keys.FORTUNE_ID)
- pref.remove(Keys.FORTUNE_DATE)
+ pref.remove(Keys.FORTUNE_DATE_EPOCH)
pref.remove(Keys.FORTUNE_IMAGE_ID)
pref.remove(Keys.FORTUNE_SCORE)
pref.remove(Keys.FORTUNE_SEEN)
diff --git a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt
index 853420c6..4e519ddb 100644
--- a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt
+++ b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt
@@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.Flow
interface FortuneLocalDataSource {
val fortuneIdFlow: Flow
- val fortuneDateFlow: Flow
+ val fortuneDateEpochFlow: Flow
val fortuneImageIdFlow: Flow
val fortuneScoreFlow: Flow
val hasUnseenFortuneFlow: Flow
diff --git a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt
index 0a14cb17..b8ab799f 100644
--- a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt
+++ b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt
@@ -5,7 +5,6 @@ import com.yapp.domain.model.FortuneCreateStatus
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import java.time.LocalDate
-import java.time.format.DateTimeFormatter
import javax.inject.Inject
class FortuneLocalDataSourceImpl @Inject constructor(
@@ -13,7 +12,7 @@ class FortuneLocalDataSourceImpl @Inject constructor(
) : FortuneLocalDataSource {
override val fortuneIdFlow = userPreferences.fortuneIdFlow
- override val fortuneDateFlow = userPreferences.fortuneDateFlow
+ override val fortuneDateEpochFlow = userPreferences.fortuneDateEpochFlow
override val fortuneImageIdFlow = userPreferences.fortuneImageIdFlow
override val fortuneScoreFlow = userPreferences.fortuneScoreFlow
override val hasUnseenFortuneFlow = userPreferences.hasUnseenFortuneFlow
@@ -22,19 +21,19 @@ class FortuneLocalDataSourceImpl @Inject constructor(
override val fortuneCreateStatusFlow = combine(
userPreferences.fortuneIdFlow,
- userPreferences.fortuneDateFlow,
+ userPreferences.fortuneDateEpochFlow,
userPreferences.isFortuneCreatingFlow,
userPreferences.isFortuneFailedFlow,
) { fortuneId, fortuneDate, isCreating, isFailed ->
when {
isFailed -> FortuneCreateStatus.Failure
isCreating -> FortuneCreateStatus.Creating
- fortuneId != null && fortuneDate == today() -> FortuneCreateStatus.Success(fortuneId)
+ fortuneId != null && fortuneDate == todayEpoch() -> FortuneCreateStatus.Success(fortuneId)
else -> FortuneCreateStatus.Idle
}
}.distinctUntilChanged()
- private fun today(): String = LocalDate.now().format(DateTimeFormatter.ISO_DATE)
+ private fun todayEpoch(): Long = LocalDate.now().toEpochDay()
override suspend fun markFortuneCreating() {
userPreferences.markFortuneCreating()
diff --git a/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSource.kt b/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSource.kt
index 37b4fc5a..3ad851df 100644
--- a/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSource.kt
+++ b/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSource.kt
@@ -6,9 +6,13 @@ interface UserLocalDataSource {
val userIdFlow: Flow
val userNameFlow: Flow
val onboardingCompletedFlow: Flow
+ val updateNoticeDontShowVersionFlow: Flow
+ val updateNoticeLastShownDateEpochFlow: Flow
suspend fun saveUserId(userId: Long)
suspend fun saveUserName(userName: String)
suspend fun setOnboardingCompleted()
+ suspend fun markUpdateNoticeDontShow(version: String)
+ suspend fun markUpdateNoticeShownToday()
suspend fun clearUserData()
}
diff --git a/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSourceImpl.kt b/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSourceImpl.kt
index 7e7d4324..187a7a59 100644
--- a/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSourceImpl.kt
+++ b/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSourceImpl.kt
@@ -11,6 +11,8 @@ class UserLocalDataSourceImpl @Inject constructor(
override val userIdFlow: Flow = userPreferences.userIdFlow
override val userNameFlow: Flow = userPreferences.userNameFlow
override val onboardingCompletedFlow: Flow = userPreferences.onboardingCompletedFlow
+ override val updateNoticeDontShowVersionFlow: Flow = userPreferences.updateNoticeDontShowVersionFlow
+ override val updateNoticeLastShownDateEpochFlow: Flow = userPreferences.updateNoticeLastShownDateEpochFlow
override suspend fun saveUserId(userId: Long) {
userPreferences.saveUserId(userId)
@@ -24,6 +26,14 @@ class UserLocalDataSourceImpl @Inject constructor(
userPreferences.setOnboardingCompleted()
}
+ override suspend fun markUpdateNoticeDontShow(version: String) {
+ userPreferences.markUpdateNoticeDontShow(version)
+ }
+
+ override suspend fun markUpdateNoticeShownToday() {
+ userPreferences.markUpdateNoticeShownToday()
+ }
+
override suspend fun clearUserData() {
userPreferences.clearUserData()
}
diff --git a/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt b/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt
index 8afe824e..1c761ba6 100644
--- a/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt
+++ b/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt
@@ -15,7 +15,7 @@ class FortuneRepositoryImpl @Inject constructor(
) : FortuneRepository {
override val fortuneIdFlow: Flow = fortuneLocalDataSource.fortuneIdFlow
- override val fortuneDateFlow: Flow = fortuneLocalDataSource.fortuneDateFlow
+ override val fortuneDateEpochFlow: Flow = fortuneLocalDataSource.fortuneDateEpochFlow
override val fortuneImageIdFlow: Flow = fortuneLocalDataSource.fortuneImageIdFlow
override val fortuneScoreFlow: Flow = fortuneLocalDataSource.fortuneScoreFlow
override val hasUnseenFortuneFlow: Flow = fortuneLocalDataSource.hasUnseenFortuneFlow
diff --git a/data/src/main/java/com/yapp/data/repositoryimpl/UserInfoRepositoryImpl.kt b/data/src/main/java/com/yapp/data/repositoryimpl/UserInfoRepositoryImpl.kt
index d96ca6be..818e232d 100644
--- a/data/src/main/java/com/yapp/data/repositoryimpl/UserInfoRepositoryImpl.kt
+++ b/data/src/main/java/com/yapp/data/repositoryimpl/UserInfoRepositoryImpl.kt
@@ -17,10 +17,14 @@ class UserInfoRepositoryImpl @Inject constructor(
override val userIdFlow: Flow = userLocalDataSource.userIdFlow
override val userNameFlow: Flow = userLocalDataSource.userNameFlow
override val onboardingCompletedFlow: Flow = userLocalDataSource.onboardingCompletedFlow
+ override val updateNoticeDontShowVersionFlow: Flow = userLocalDataSource.updateNoticeDontShowVersionFlow
+ override val updateNoticeLastShownDateEpochFlow: Flow = userLocalDataSource.updateNoticeLastShownDateEpochFlow
override suspend fun saveUserId(userId: Long) = userLocalDataSource.saveUserId(userId)
override suspend fun saveUserName(userName: String) = userLocalDataSource.saveUserName(userName)
override suspend fun setOnboardingCompleted() = userLocalDataSource.setOnboardingCompleted()
+ override suspend fun markUpdateNoticeDontShow(version: String) = userLocalDataSource.markUpdateNoticeDontShow(version)
+ override suspend fun markUpdateNoticeShownToday() = userLocalDataSource.markUpdateNoticeShownToday()
override suspend fun clearUserData() = userLocalDataSource.clearUserData()
override suspend fun getUserInfo(userId: Long): Result {
diff --git a/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt b/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt
index 29024ae6..372fd5fe 100644
--- a/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt
+++ b/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt
@@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.Flow
interface FortuneRepository {
val fortuneIdFlow: Flow
- val fortuneDateFlow: Flow
+ val fortuneDateEpochFlow: Flow
val fortuneImageIdFlow: Flow
val fortuneScoreFlow: Flow
val hasUnseenFortuneFlow: Flow
diff --git a/domain/src/main/java/com/yapp/domain/repository/UserInfoRepository.kt b/domain/src/main/java/com/yapp/domain/repository/UserInfoRepository.kt
index bda28291..a9df412e 100644
--- a/domain/src/main/java/com/yapp/domain/repository/UserInfoRepository.kt
+++ b/domain/src/main/java/com/yapp/domain/repository/UserInfoRepository.kt
@@ -8,10 +8,14 @@ interface UserInfoRepository {
val userIdFlow: Flow
val userNameFlow: Flow
val onboardingCompletedFlow: Flow
+ val updateNoticeDontShowVersionFlow: Flow
+ val updateNoticeLastShownDateEpochFlow: Flow
suspend fun saveUserId(userId: Long)
suspend fun saveUserName(userName: String)
suspend fun setOnboardingCompleted()
+ suspend fun markUpdateNoticeDontShow(version: String)
+ suspend fun markUpdateNoticeShownToday()
suspend fun clearUserData()
suspend fun getUserInfo(userId: Long): Result
diff --git a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/snooze/AlarmSnoozeTimerViewModel.kt b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/snooze/AlarmSnoozeTimerViewModel.kt
index 1f0e6d1b..05b6c798 100644
--- a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/snooze/AlarmSnoozeTimerViewModel.kt
+++ b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/snooze/AlarmSnoozeTimerViewModel.kt
@@ -17,7 +17,6 @@ import org.orbitmvi.orbit.viewmodel.container
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
-import java.time.format.DateTimeFormatter
import javax.inject.Inject
import kotlin.math.max
@@ -45,8 +44,8 @@ class AlarmSnoozeTimerViewModel @Inject constructor(
}
private fun fetchIsFirstMission() = intent {
- val fortuneDate = fortuneRepository.fortuneDateFlow.firstOrNull()
- val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE)
+ val fortuneDate = fortuneRepository.fortuneDateEpochFlow.firstOrNull()
+ val todayDate = LocalDate.now().toEpochDay()
val isFirstMission = fortuneDate != todayDate
reduce {
diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts
index 9ef0f667..0b90300c 100644
--- a/feature/home/build.gradle.kts
+++ b/feature/home/build.gradle.kts
@@ -19,4 +19,5 @@ dependencies {
implementation(libs.orbit.viewmodel)
implementation(libs.androidx.material.android)
implementation(libs.androidx.annotation)
+ implementation(libs.coil.compose)
}
diff --git a/feature/home/src/main/AndroidManifest.xml b/feature/home/src/main/AndroidManifest.xml
index 8bdb7e14..1c6dcf4e 100644
--- a/feature/home/src/main/AndroidManifest.xml
+++ b/feature/home/src/main/AndroidManifest.xml
@@ -1,4 +1,4 @@
-
+
diff --git a/feature/home/src/main/java/com/yapp/home/HomeContract.kt b/feature/home/src/main/java/com/yapp/home/HomeContract.kt
index dee0ecef..e4d15d19 100644
--- a/feature/home/src/main/java/com/yapp/home/HomeContract.kt
+++ b/feature/home/src/main/java/com/yapp/home/HomeContract.kt
@@ -18,6 +18,7 @@ sealed class HomeContract {
val isDeleteDialogVisible: Boolean = false,
val isNoActivatedAlarmDialogVisible: Boolean = false,
val isNoDailyFortuneDialogVisible: Boolean = false,
+ val isUpdateNoticeVisible: Boolean = false,
val hasNewFortune: Boolean = false,
val isToolTipVisible: Boolean = false,
val pendingAlarmToggle: Pair? = null,
@@ -58,6 +59,8 @@ sealed class HomeContract {
data object ShowNoDailyFortuneDialog : Action()
data object HideNoDailyFortuneDialog : Action()
data object HideToolTip : Action()
+ data object OnClickDontShowAgain : Action()
+ data object HideUpdateNotice : Action()
data object RollbackPendingAlarmToggle : Action()
data object ConfirmDeletion : Action()
data class DeleteSingleAlarm(val alarmId: Long) : Action()
diff --git a/feature/home/src/main/java/com/yapp/home/HomeScreen.kt b/feature/home/src/main/java/com/yapp/home/HomeScreen.kt
index f69d7683..10d74e52 100644
--- a/feature/home/src/main/java/com/yapp/home/HomeScreen.kt
+++ b/feature/home/src/main/java/com/yapp/home/HomeScreen.kt
@@ -75,6 +75,7 @@ import com.yapp.domain.model.Alarm
import com.yapp.home.alarm.component.AlarmListItem
import com.yapp.home.alarm.component.AlarmListItemMenu
import com.yapp.home.component.bottomsheet.AlarmListBottomSheet
+import com.yapp.home.component.bottomsheet.UpdateNoticeBottomSheet
import com.yapp.ui.component.dialog.OrbitDialog
import com.yapp.ui.component.lottie.LottieAnimation
import com.yapp.ui.component.snackbar.showCustomSnackBar
@@ -245,6 +246,17 @@ fun HomeScreen(
},
)
}
+
+ if (state.isUpdateNoticeVisible) {
+ UpdateNoticeBottomSheet(
+ onDontShowAgain = {
+ processAction(HomeContract.Action.OnClickDontShowAgain)
+ },
+ onClose = {
+ processAction(HomeContract.Action.HideUpdateNotice)
+ },
+ )
+ }
}
@Composable
diff --git a/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt b/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt
index 1834e476..57b74428 100644
--- a/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt
+++ b/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt
@@ -1,5 +1,8 @@
package com.yapp.home
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
import android.util.Log
import androidx.lifecycle.ViewModel
import com.yapp.common.util.ResourceProvider
@@ -9,6 +12,7 @@ import com.yapp.domain.repository.UserInfoRepository
import com.yapp.domain.usecase.AlarmUseCase
import com.yapp.home.util.AlarmDateTimeFormatter
import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
import feature.home.R
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
@@ -21,8 +25,8 @@ import org.orbitmvi.orbit.syntax.simple.reduce
import org.orbitmvi.orbit.syntax.simple.repeatOnSubscription
import org.orbitmvi.orbit.viewmodel.container
import java.time.LocalDate
-import java.time.format.DateTimeFormatter
import javax.inject.Inject
+import javax.inject.Named
@HiltViewModel
class HomeViewModel @Inject constructor(
@@ -31,6 +35,8 @@ class HomeViewModel @Inject constructor(
private val alarmDateTimeFormatter: AlarmDateTimeFormatter,
private val fortuneRepository: FortuneRepository,
private val userInfoRepository: UserInfoRepository,
+ @Named("appVersion") private val appVersion: String,
+ @ApplicationContext private val context: Context,
) : ViewModel(), ContainerHost {
override val container: Container = container(
@@ -41,6 +47,7 @@ class HomeViewModel @Inject constructor(
loadAllAlarms()
loadDailyFortuneState()
loadUserName()
+ loadUpdateNoticeVisibility()
}
}
}
@@ -63,6 +70,8 @@ class HomeViewModel @Inject constructor(
HomeContract.Action.ShowNoDailyFortuneDialog -> showNoDailyFortuneDialog()
HomeContract.Action.HideNoDailyFortuneDialog -> hideNoDailyFortuneDialog()
HomeContract.Action.HideToolTip -> hideToolTip()
+ HomeContract.Action.HideUpdateNotice -> hideUpdateNotice()
+ HomeContract.Action.OnClickDontShowAgain -> setUpdateNoticeDontShowVersion()
HomeContract.Action.RollbackPendingAlarmToggle -> rollbackAlarmActivation()
HomeContract.Action.ConfirmDeletion -> confirmDeletion()
is HomeContract.Action.DeleteSingleAlarm -> deleteSingleAlarm(action.alarmId)
@@ -332,8 +341,8 @@ class HomeViewModel @Inject constructor(
}
private fun loadDailyFortune() = intent {
- val fortuneDate = fortuneRepository.fortuneDateFlow.firstOrNull()
- val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE)
+ val fortuneDate = fortuneRepository.fortuneDateEpochFlow.firstOrNull()
+ val todayDate = LocalDate.now().toEpochDay()
if (fortuneDate != todayDate) {
processAction(HomeContract.Action.ShowNoDailyFortuneDialog)
@@ -344,10 +353,10 @@ class HomeViewModel @Inject constructor(
}
private fun loadDailyFortuneState() = intent {
- val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE)
+ val todayDate = LocalDate.now().toEpochDay()
combine(
- fortuneRepository.fortuneDateFlow,
+ fortuneRepository.fortuneDateEpochFlow,
fortuneRepository.fortuneScoreFlow,
fortuneRepository.shouldShowFortuneToolTipFlow,
) { fortuneDate, fortuneScore, shouldShowTooltip ->
@@ -366,6 +375,39 @@ class HomeViewModel @Inject constructor(
}
}
+ private fun loadUpdateNoticeVisibility() = intent {
+ if (!isOnlineNow()) {
+ reduce { state.copy(isUpdateNoticeVisible = false) }
+ return@intent
+ }
+
+ val dontShowVersion =
+ userInfoRepository.updateNoticeDontShowVersionFlow.firstOrNull()
+ val lastShownDate =
+ userInfoRepository.updateNoticeLastShownDateEpochFlow.firstOrNull()
+
+ val today = LocalDate.now().toEpochDay()
+
+ val shouldShow = when {
+ dontShowVersion != null && dontShowVersion == appVersion -> false
+ lastShownDate != null && lastShownDate == today -> false
+ else -> true
+ }
+
+ if (shouldShow) userInfoRepository.markUpdateNoticeShownToday()
+
+ reduce { state.copy(isUpdateNoticeVisible = shouldShow) }
+ }
+
+ private fun setUpdateNoticeDontShowVersion() = intent {
+ userInfoRepository.markUpdateNoticeDontShow(appVersion)
+ reduce { state.copy(isUpdateNoticeVisible = false) }
+ }
+
+ private fun hideUpdateNotice() = intent {
+ reduce { state.copy(isUpdateNoticeVisible = false) }
+ }
+
private fun loadUserName() = intent {
userInfoRepository.userNameFlow.first { userName ->
reduce { state.copy(name = userName ?: "") }
@@ -411,4 +453,13 @@ class HomeViewModel @Inject constructor(
reduce { state.copy(sortOrder = sortOrder) }
hideDropDownMenu()
}
+
+ private fun isOnlineNow(): Boolean {
+ val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ val network = cm.activeNetwork ?: return false
+ val caps = cm.getNetworkCapabilities(network) ?: return false
+
+ return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
+ caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+ }
}
diff --git a/feature/home/src/main/java/com/yapp/home/component/bottomsheet/UpdateNoticeBottomSheet.kt b/feature/home/src/main/java/com/yapp/home/component/bottomsheet/UpdateNoticeBottomSheet.kt
new file mode 100644
index 00000000..63ffb413
--- /dev/null
+++ b/feature/home/src/main/java/com/yapp/home/component/bottomsheet/UpdateNoticeBottomSheet.kt
@@ -0,0 +1,145 @@
+package com.yapp.home.component.bottomsheet
+
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import com.yapp.designsystem.theme.OrbitTheme
+import feature.home.R
+
+private fun resolveVersionName(ctx: android.content.Context): String {
+ return runCatching {
+ val pm = ctx.packageManager
+ val packageName = ctx.packageName
+ val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
+ } else {
+ @Suppress("DEPRECATION")
+ pm.getPackageInfo(packageName, 0)
+ }
+ info.versionName ?: ""
+ }.getOrDefault("")
+}
+
+private fun bannerUrl(versionName: String): String =
+ "https://www.orbitalarm.net/images/aos/$versionName/update-banner.png"
+
+@Composable
+internal fun UpdateNoticeBottomSheet(
+ onDontShowAgain: () -> Unit,
+ onClose: () -> Unit,
+) {
+ val context = LocalContext.current
+ val isPreview = LocalInspectionMode.current
+
+ val versionName = remember(isPreview) {
+ if (isPreview) "preview" else resolveVersionName(context)
+ }
+ val imageUrl = remember(versionName) { bannerUrl(versionName.ifEmpty { "unknown" }) }
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color(0xFF17191F).copy(alpha = 0.85f))
+ .clickable(onClick = onClose),
+ contentAlignment = Alignment.BottomCenter,
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ color = OrbitTheme.colors.gray_900,
+ shape = RoundedCornerShape(topStart = 30.dp, topEnd = 30.dp),
+ )
+ .clip(RoundedCornerShape(topStart = 30.dp, topEnd = 30.dp)),
+ ) {
+ if (isPreview) {
+ // 프리뷰용 박스
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(1f)
+ .background(color = OrbitTheme.colors.white),
+ )
+ } else {
+ AsyncImage(
+ model = imageUrl,
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(1f),
+ )
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp, bottom = 20.dp, start = 20.dp, end = 20.dp),
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .clickable(onClick = onDontShowAgain)
+ .padding(vertical = 14.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = stringResource(id = R.string.update_notice_bottom_sheet_dont_show_again),
+ style = OrbitTheme.typography.body1SemiBold,
+ color = OrbitTheme.colors.white,
+ )
+ }
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .clickable(onClick = onClose)
+ .padding(vertical = 14.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = stringResource(id = R.string.update_notice_bottom_sheet_close),
+ style = OrbitTheme.typography.body1SemiBold,
+ color = OrbitTheme.colors.white,
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun UpdateNoticeBottomSheetPreview() {
+ OrbitTheme {
+ UpdateNoticeBottomSheet(
+ onDontShowAgain = {},
+ onClose = {},
+ )
+ }
+}
diff --git a/feature/home/src/main/res/values/strings.xml b/feature/home/src/main/res/values/strings.xml
index 682d14b9..b2266b16 100644
--- a/feature/home/src/main/res/values/strings.xml
+++ b/feature/home/src/main/res/values/strings.xml
@@ -124,4 +124,7 @@
%1$d시간 %2$d분 후에 울려요
%d분 후에 울려요
곧 울려요
+
+ 다시 보지 않기
+ 닫기
diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt
index eeccad37..b5e1c45d 100644
--- a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt
+++ b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt
@@ -133,20 +133,27 @@ class MissionViewModel @Inject constructor(
private fun completeMission(type: String) = intent {
performHapticSuccess()
logMissionSuccess(type)
- if (state.missionMode == MissionMode.REAL) {
- val fortuneCreateStaus = fortuneRepository.fortuneCreateStatusFlow.first()
-
- when (fortuneCreateStaus) {
- is FortuneCreateStatus.Creating, is FortuneCreateStatus.Success -> {
- postSideEffect(MissionContract.SideEffect.NavigateToFortune)
- }
- FortuneCreateStatus.Failure, FortuneCreateStatus.Idle -> {
- postSideEffect(MissionContract.SideEffect.NavigateBack)
- }
- }
- } else {
+
+ if (state.missionMode != MissionMode.REAL) {
postSideEffect(MissionContract.SideEffect.NavigateBack)
+ return@intent
}
+
+ val fortuneCreateStatus = fortuneRepository.fortuneCreateStatusFlow.first()
+ val hasUnseenFortune = fortuneRepository.hasUnseenFortuneFlow.first()
+
+ val shouldOpenFortune = (
+ fortuneCreateStatus is FortuneCreateStatus.Creating ||
+ fortuneCreateStatus is FortuneCreateStatus.Success && hasUnseenFortune
+ )
+
+ postSideEffect(
+ if (shouldOpenFortune) {
+ MissionContract.SideEffect.NavigateToFortune
+ } else {
+ MissionContract.SideEffect.NavigateBack
+ },
+ )
}
private fun logMissionSuccess(type: String) {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index db4817c9..2d1248b6 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -50,7 +50,7 @@ hilt-work = "1.2.0"
## Third Party
okhttp = "4.12.0"
retrofit = "2.11.0"
-coil = "2.4.0"
+coil = "2.7.0"
# Google Libraries Versions
google-service = "4.4.2"
diff --git a/project.dot.png b/project.dot.png
index 070d91b3..77a616a5 100644
Binary files a/project.dot.png and b/project.dot.png differ