diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml
index 1f1a7173..3e073b75 100644
--- a/.github/workflows/android_ci.yml
+++ b/.github/workflows/android_ci.yml
@@ -7,13 +7,6 @@ on:
- '**/*.kt'
- 'build.gradle'
- 'app/**'
- push:
- branches:
- - 'release/**'
- paths:
- - '**/*.kt'
- - 'build.gradle'
- - 'app/**'
jobs:
build:
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b955d234..6c350ede 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -8,10 +8,11 @@ plugins {
android {
namespace = "com.yapp.orbit"
+ compileSdk = 35
defaultConfig {
- versionCode = 5
- versionName = "1.0.3"
+ versionCode = 6
+ versionName = "1.1.3"
targetSdk = 35
}
@@ -53,4 +54,6 @@ dependencies {
implementation(libs.firebase.crashlytics)
implementation(libs.play.services.ads)
implementation(libs.kotlin.reflect)
+ implementation(libs.hilt.worker)
+ implementation(libs.androidx.work.runtime)
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 780b61bb..e7abd75a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,6 +11,7 @@
+
-
+
@@ -80,5 +79,15 @@
+
+
+
+
diff --git a/app/src/main/java/com/yapp/orbit/OrbitApplication.kt b/app/src/main/java/com/yapp/orbit/OrbitApplication.kt
index 7391cf6e..b06538f8 100644
--- a/app/src/main/java/com/yapp/orbit/OrbitApplication.kt
+++ b/app/src/main/java/com/yapp/orbit/OrbitApplication.kt
@@ -1,13 +1,24 @@
package com.yapp.orbit
import android.app.Application
+import androidx.hilt.work.HiltWorkerFactory
+import androidx.work.Configuration
import com.google.android.gms.ads.MobileAds
import dagger.hilt.android.HiltAndroidApp
+import javax.inject.Inject
@HiltAndroidApp
-class OrbitApplication : Application() {
+class OrbitApplication() : Application(), Configuration.Provider {
+
+ @Inject lateinit var workerFactory: HiltWorkerFactory
+
override fun onCreate() {
super.onCreate()
MobileAds.initialize(this)
}
+
+ override val workManagerConfiguration: Configuration
+ get() = Configuration.Builder()
+ .setWorkerFactory(workerFactory)
+ .build()
}
diff --git a/app/src/main/java/com/yapp/orbit/OrbitNavHost.kt b/app/src/main/java/com/yapp/orbit/OrbitNavHost.kt
index 69bc23ac..5dd9ce4b 100644
--- a/app/src/main/java/com/yapp/orbit/OrbitNavHost.kt
+++ b/app/src/main/java/com/yapp/orbit/OrbitNavHost.kt
@@ -6,24 +6,16 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.zIndex
import androidx.navigation.compose.NavHost
import com.yapp.common.navigation.OrbitNavigator
import com.yapp.common.navigation.rememberOrbitNavigator
@@ -34,9 +26,9 @@ import com.yapp.mission.missionScreen
import com.yapp.onboarding.onboardingNavGraph
import com.yapp.setting.settingNavGraph
import com.yapp.splash.splashScreen
-import com.yapp.ui.component.bottomsheet.OrbitBottomSheetLayout
import com.yapp.ui.component.bottomsheet.OrbitBottomSheetState
import com.yapp.ui.component.bottomsheet.rememberOrbitBottomSheetState
+import com.yapp.ui.component.navigation.NavigationBarScrim
import com.yapp.ui.component.snackbar.CustomSnackBarVisuals
import com.yapp.ui.component.snackbar.OrbitSnackBar
import com.yapp.webview.webViewScreen
@@ -51,18 +43,16 @@ internal fun OrbitNavHost(
val snackBarHostState = remember { SnackbarHostState() }
Box {
- OrbitBottomSheetLayout(sheetState = bottomSheetState) {
- Scaffold(
- modifier = modifier,
- snackbarHost = { OrbitSnackBarHost(snackBarHostState) },
- containerColor = OrbitTheme.colors.gray_900,
- ) {
- OrbitNavigationGraph(
- navigator = navigator,
- bottomSheetState = bottomSheetState,
- snackBarHostState = snackBarHostState,
- )
- }
+ Scaffold(
+ modifier = modifier,
+ snackbarHost = { OrbitSnackBarHost(snackBarHostState) },
+ containerColor = OrbitTheme.colors.gray_900,
+ ) {
+ OrbitNavigationGraph(
+ navigator = navigator,
+ bottomSheetState = bottomSheetState,
+ snackBarHostState = snackBarHostState,
+ )
}
NavigationBarScrim()
@@ -76,6 +66,7 @@ private fun OrbitNavigationGraph(
snackBarHostState: SnackbarHostState,
) {
NavHost(
+ modifier = Modifier.navigationBarsPadding(),
navController = navigator.navController,
startDestination = navigator.startDestination,
) {
@@ -89,18 +80,6 @@ private fun OrbitNavigationGraph(
}
}
-@Composable
-private fun BoxScope.NavigationBarScrim() {
- Box(
- modifier = Modifier
- .align(Alignment.BottomCenter)
- .fillMaxWidth()
- .windowInsetsBottomHeight(WindowInsets.navigationBars)
- .background(Color.Black)
- .zIndex(1f),
- )
-}
-
@Composable
private fun OrbitSnackBarHost(
snackBarHostState: SnackbarHostState,
diff --git a/app/src/main/java/com/yapp/orbit/di/AppVersionModule.kt b/app/src/main/java/com/yapp/orbit/di/AppVersionModule.kt
new file mode 100644
index 00000000..6297ecc3
--- /dev/null
+++ b/app/src/main/java/com/yapp/orbit/di/AppVersionModule.kt
@@ -0,0 +1,18 @@
+package com.yapp.orbit.di
+
+import com.yapp.orbit.BuildConfig
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Named
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object AppVersionModule {
+ @Provides
+ @Singleton
+ @Named("appVersion")
+ fun provideAppVersion(): String = BuildConfig.VERSION_NAME
+}
diff --git a/build-logic/src/main/java/com/yapp/convention/HiltAndroid.kt b/build-logic/src/main/java/com/yapp/convention/HiltAndroid.kt
index e266704f..15db09ff 100644
--- a/build-logic/src/main/java/com/yapp/convention/HiltAndroid.kt
+++ b/build-logic/src/main/java/com/yapp/convention/HiltAndroid.kt
@@ -14,7 +14,9 @@ internal fun Project.configureHiltAndroid() {
dependencies {
"implementation"(libs.findLibrary("hilt.android").get())
"ksp"(libs.findLibrary("hilt.android.compiler").get())
+ "ksp"(libs.findLibrary("androidx-hilt-compiler").get())
"implementation"(libs.findLibrary("hilt-navigation-compose").get())
+ "implementation"(libs.findLibrary("hilt-worker").get())
}
}
diff --git a/build-logic/src/main/java/orbit.android.feature.gradle.kts b/build-logic/src/main/java/orbit.android.feature.gradle.kts
index aa5803c4..47d5c072 100644
--- a/build-logic/src/main/java/orbit.android.feature.gradle.kts
+++ b/build-logic/src/main/java/orbit.android.feature.gradle.kts
@@ -1,4 +1,3 @@
-import com.yapp.convention.configureHiltAndroid
import com.yapp.convention.libs
plugins {
@@ -6,8 +5,6 @@ plugins {
id("orbit.android.compose")
}
-configureHiltAndroid()
-
dependencies {
implementation(project(":core:designsystem"))
implementation(project(":core:ui"))
diff --git a/core/alarm/src/main/java/com/yapp/alarm/AndroidAlarmScheduler.kt b/core/alarm/src/main/java/com/yapp/alarm/AndroidAlarmScheduler.kt
index 2691e78b..49c4581a 100644
--- a/core/alarm/src/main/java/com/yapp/alarm/AndroidAlarmScheduler.kt
+++ b/core/alarm/src/main/java/com/yapp/alarm/AndroidAlarmScheduler.kt
@@ -2,6 +2,7 @@ package com.yapp.alarm
import android.app.AlarmManager
import android.app.Application
+import android.util.Log
import com.yapp.alarm.pendingIntent.schedule.createAlarmReceiverPendingIntentForSchedule
import com.yapp.alarm.pendingIntent.schedule.createAlarmReceiverPendingIntentForUnSchedule
import com.yapp.domain.model.Alarm
@@ -16,6 +17,15 @@ class AndroidAlarmScheduler @Inject constructor(
private val alarmTimeCalculator: AlarmTimeCalculator,
) : AlarmScheduler {
+ private fun logSchedule(tag: String, alarm: Alarm, triggerMillis: Long, extra: String = "") {
+ Log.d("ScheduleTrace", "scheduleAlarm Called", Throwable())
+ Log.d(
+ "AlarmSchedule",
+ "[$tag] id=${alarm.id}, repeatDays=${alarm.repeatDays}, " +
+ "time=${java.time.Instant.ofEpochMilli(triggerMillis)} $extra",
+ )
+ }
+
override fun scheduleAlarm(alarm: Alarm) {
val selectedDays = alarm.repeatDays.toAlarmDays()
@@ -31,7 +41,7 @@ class AndroidAlarmScheduler @Inject constructor(
private fun setRepeatingAlarm(day: AlarmDay, alarm: Alarm) {
val triggerMillis = alarmTimeCalculator.calculateNextRepeatingTimeMillis(alarm, day)
val pendingIntent = createAlarmReceiverPendingIntentForSchedule(app, alarm, day)
-
+ logSchedule("REPEAT", alarm, triggerMillis, "day=$day")
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerMillis,
@@ -42,7 +52,7 @@ class AndroidAlarmScheduler @Inject constructor(
private fun setNonRepeatingAlarm(alarm: Alarm) {
val triggerMillis = alarmTimeCalculator.calculateNonRepeatingTimeMillis(alarm)
val pendingIntent = createAlarmReceiverPendingIntentForSchedule(app, alarm)
-
+ logSchedule("NON_REPEAT", alarm, triggerMillis)
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerMillis,
@@ -53,7 +63,7 @@ class AndroidAlarmScheduler @Inject constructor(
fun rescheduleUpcomingWeeklyAlarm(alarm: Alarm, day: AlarmDay) {
val triggerMillis = alarmTimeCalculator.calculateNextWeeklyRescheduledTimeMillis(alarm, day)
val pendingIntent = createAlarmReceiverPendingIntentForSchedule(app, alarm, day)
-
+ logSchedule("RESCHEDULE_WEEKLY", alarm, triggerMillis, "day=$day")
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerMillis,
diff --git a/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmInteractionActivityReceiver.kt b/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmInteractionActivityReceiver.kt
index d7f38c3e..029072a4 100644
--- a/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmInteractionActivityReceiver.kt
+++ b/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmInteractionActivityReceiver.kt
@@ -3,18 +3,18 @@ package com.yapp.alarm.receivers
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
-import android.util.Log
import androidx.activity.ComponentActivity
import androidx.core.net.toUri
import com.yapp.alarm.AlarmConstants
+import com.yapp.domain.model.FortuneCreateStatus
+import com.yapp.domain.model.MissionType
import com.yapp.domain.repository.FortuneRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
-import java.time.LocalDate
-import java.time.format.DateTimeFormatter
+import kotlinx.coroutines.withContext
import javax.inject.Inject
@AndroidEntryPoint
@@ -31,29 +31,69 @@ class AlarmInteractionActivityReceiver(private val activity: ComponentActivity)
if (!isSnoozed) {
val notificationId = intent.getLongExtra(AlarmConstants.EXTRA_NOTIFICATION_ID, -1L)
- val missionType = intent.getIntExtra(AlarmConstants.EXTRA_MISSION_TYPE, -1)
+ val missionTypeRaw = intent.getIntExtra(AlarmConstants.EXTRA_MISSION_TYPE, -1)
val missionCount = intent.getIntExtra(AlarmConstants.EXTRA_MISSION_COUNT, -1)
- if (notificationId == -1L || missionType == -1 || missionCount == -1) {
- Log.e("AlarmInteraction", "필수 값 누락")
- return
- }
+ val missionType = MissionType.fromInt(missionTypeRaw)
+
+ val hasValidMissionData = (
+ notificationId != -1L &&
+ missionType != MissionType.NONE &&
+ missionCount != -1
+ )
+
+ val pending = goAsync()
+ CoroutineScope(Dispatchers.Main).launch {
+ try {
+ if (!hasValidMissionData) {
+ val (fortuneCreateStatus, hasUnseenFortune) = withContext(Dispatchers.IO) {
+ val status = fortuneRepository.fortuneCreateStatusFlow.first()
+ val unseen = fortuneRepository.hasUnseenFortuneFlow.first()
+ status to unseen
+ }
- CoroutineScope(Dispatchers.IO).launch {
- val fortuneDate = fortuneRepository.fortuneDateFlow.firstOrNull()
- val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE)
-
- if (fortuneDate != todayDate) {
- context?.let {
- val uriString =
- "orbitapp://mission?notificationId=$notificationId&missionType=$missionType&missionCount=$missionCount"
- val missionIntent =
- Intent(Intent.ACTION_VIEW, uriString.toUri()).apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- setPackage(context.packageName)
+ when (fortuneCreateStatus) {
+ is FortuneCreateStatus.Creating -> {
+ context?.let { ctx ->
+ val uri = "orbitapp://fortune".toUri()
+ val fortuneIntent = Intent(Intent.ACTION_VIEW, uri).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ setPackage(ctx.packageName)
+ }
+ ctx.startActivity(fortuneIntent)
+ }
}
- it.startActivity(missionIntent)
+
+ is FortuneCreateStatus.Success -> {
+ if (hasUnseenFortune) {
+ context?.let { ctx ->
+ val uri = "orbitapp://fortune".toUri()
+ val fortuneIntent =
+ Intent(Intent.ACTION_VIEW, uri).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ setPackage(ctx.packageName)
+ }
+ ctx.startActivity(fortuneIntent)
+ }
+ }
+ }
+
+ FortuneCreateStatus.Failure, FortuneCreateStatus.Idle -> { }
+ }
+ } else {
+ context?.let { ctx ->
+ val uriString =
+ "orbitapp://mission?notificationId=$notificationId&missionType=${missionType.value}&missionCount=$missionCount"
+ val missionIntent =
+ Intent(Intent.ACTION_VIEW, uriString.toUri()).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ setPackage(ctx.packageName)
+ }
+ ctx.startActivity(missionIntent)
+ }
}
+ } finally {
+ pending.finish()
}
}
}
diff --git a/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmReceiver.kt b/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmReceiver.kt
index edd18466..011dadef 100644
--- a/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmReceiver.kt
+++ b/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmReceiver.kt
@@ -12,6 +12,7 @@ import com.yapp.alarm.services.AlarmService
import com.yapp.analytics.AnalyticsEvent
import com.yapp.analytics.AnalyticsHelper
import com.yapp.domain.model.Alarm
+import com.yapp.domain.model.toAlarmDay
import com.yapp.domain.model.toTimeString
import com.yapp.domain.repository.FortuneRepository
import com.yapp.domain.usecase.AlarmUseCase
@@ -19,8 +20,8 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
+import java.time.LocalDate
import java.time.LocalDateTime
import javax.inject.Inject
@@ -105,17 +106,31 @@ class AlarmReceiver : BroadcastReceiver() {
androidAlarmScheduler.cancelSnoozedAlarm(notificationId)
context.stopService(alarmServiceIntent)
- sendBroadCastToCloseAlarmInteractionActivity(
- context = context,
- notificationId = notificationId,
- missionType = missionType,
- missionCount = missionCount,
- )
-
CoroutineScope(Dispatchers.IO).launch {
- val alarms = alarmUseCase.getAllAlarms().first().sortedBy { it.isAlarmActive }
- val isFirstAlarm = alarms.firstOrNull()?.id == notificationId
+ val alarms = alarmUseCase.getAllAlarms().first()
+
+ val isSnoozeId = notificationId >= AlarmConstants.SNOOZE_ID_OFFSET
+
+ fun Alarm.ringsToday(): Boolean {
+ if (repeatDays == 0) return true
+
+ val todayAlarmDay = LocalDate.now().dayOfWeek.toAlarmDay()
+ return (repeatDays and todayAlarmDay.bitValue) != 0
+ }
+ val earliestIdToday: Long? = alarms
+ .asSequence()
+ .filter { (it.isAlarmActive || it.id == notificationId) && it.ringsToday() }
+ .sortedWith(compareBy({ it.hour }, { it.minute }, { it.second }))
+ .firstOrNull()
+ ?.id
+
+ val isEarliestAlarmDismissedToday =
+ !isSnoozeId && (earliestIdToday == notificationId)
+
+ if (isEarliestAlarmDismissedToday) fortuneRepository.markFirstAlarmDismissedToday()
+
+ val isFirstAlarm = earliestIdToday == notificationId
analyticsHelper.logEvent(
AnalyticsEvent(
type = "alarm_dismiss",
@@ -126,12 +141,12 @@ class AlarmReceiver : BroadcastReceiver() {
),
)
- val existingId = fortuneRepository.firstDismissedAlarmIdFlow.firstOrNull()
- if (existingId == null) {
- fortuneRepository.saveFirstDismissedAlarmId(notificationId)
- } else if (existingId != notificationId) {
- fortuneRepository.clearDismissedAlarmId()
- }
+ sendBroadCastToCloseAlarmInteractionActivity(
+ context = context,
+ notificationId = notificationId,
+ missionType = missionType,
+ missionCount = missionCount,
+ )
}
Toast.makeText(context, "알람이 해제되었어요", Toast.LENGTH_SHORT).show()
diff --git a/core/alarm/src/main/java/com/yapp/alarm/receivers/RescheduleAlarmReceiver.kt b/core/alarm/src/main/java/com/yapp/alarm/receivers/RescheduleAlarmReceiver.kt
index 9d21144d..bdd47fe8 100644
--- a/core/alarm/src/main/java/com/yapp/alarm/receivers/RescheduleAlarmReceiver.kt
+++ b/core/alarm/src/main/java/com/yapp/alarm/receivers/RescheduleAlarmReceiver.kt
@@ -8,6 +8,8 @@ import com.yapp.domain.usecase.AlarmUseCase
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -25,16 +27,20 @@ class RescheduleAlarmReceiver : BroadcastReceiver() {
intent ?: return
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
- rescheduleAlarm()
+ val pending = goAsync()
+ rescheduleAlarm(pending)
}
}
- private fun rescheduleAlarm() {
- CoroutineScope(Dispatchers.IO).launch {
- alarmUseCase.getAllAlarms().collect { alarms ->
- alarms.forEach { alarm ->
- androidAlarmScheduler.scheduleAlarm(alarm)
- }
+ private fun rescheduleAlarm(pendingResult: PendingResult) {
+ CoroutineScope(Dispatchers.IO + SupervisorJob()).launch {
+ try {
+ val alarms = alarmUseCase.getAllAlarms().first()
+ alarms
+ .filter { it.isAlarmActive }
+ .forEach { alarm -> androidAlarmScheduler.scheduleAlarm(alarm) }
+ } finally {
+ pendingResult.finish()
}
}
}
diff --git a/core/alarm/src/main/java/com/yapp/alarm/scheduler/PostFortuneTaskScheduler.kt b/core/alarm/src/main/java/com/yapp/alarm/scheduler/PostFortuneTaskScheduler.kt
new file mode 100644
index 00000000..e16e8dd4
--- /dev/null
+++ b/core/alarm/src/main/java/com/yapp/alarm/scheduler/PostFortuneTaskScheduler.kt
@@ -0,0 +1,5 @@
+package com.yapp.alarm.scheduler
+
+interface PostFortuneTaskScheduler {
+ fun enqueueOnceForToday()
+}
diff --git a/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt b/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt
index cbea504e..56141679 100644
--- a/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt
+++ b/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt
@@ -23,9 +23,10 @@ import com.yapp.alarm.pendingIntent.interaction.createAlarmAlertPendingIntent
import com.yapp.alarm.pendingIntent.interaction.createAlarmDismissPendingIntent
import com.yapp.alarm.pendingIntent.interaction.createAlarmSnoozePendingIntent
import com.yapp.alarm.pendingIntent.interaction.createNavigateToMissionPendingIntent
+import com.yapp.alarm.scheduler.PostFortuneTaskScheduler
import com.yapp.domain.model.Alarm
import com.yapp.domain.model.AlarmDay
-import com.yapp.domain.repository.FortuneRepository
+import com.yapp.domain.model.MissionType
import com.yapp.domain.usecase.AlarmUseCase
import com.yapp.media.sound.SoundPlayer
import dagger.hilt.android.AndroidEntryPoint
@@ -33,10 +34,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
-import java.time.LocalDate
-import java.time.format.DateTimeFormatter
import javax.inject.Inject
@AndroidEntryPoint
@@ -54,7 +52,7 @@ class AlarmService : Service() {
lateinit var androidAlarmScheduler: AndroidAlarmScheduler
@Inject
- lateinit var fortuneRepository: FortuneRepository
+ lateinit var postFortuneTaskScheduler: PostFortuneTaskScheduler
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@@ -81,7 +79,7 @@ class AlarmService : Service() {
super.onDestroy()
}
- private suspend fun handleIntent(intent: Intent) {
+ private fun handleIntent(intent: Intent) {
val alarm: Alarm? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(AlarmConstants.EXTRA_ALARM, Alarm::class.java)
} else {
@@ -113,7 +111,7 @@ class AlarmService : Service() {
false -> {
startForeground(
notificationId.toInt(),
- createNotification(alarm, shouldNavigateToMission()),
+ createNotification(alarm, shouldNavigateToMission(alarm.missionType)),
)
if (alarm.isVibrationEnabled) startVibration()
if (alarm.isSoundEnabled) startSound(alarm.soundUri, alarm.soundVolume)
@@ -123,12 +121,14 @@ class AlarmService : Service() {
if (isOneTimeAlarm) {
turnOffAlarm(alarmId = notificationId)
}
+
+ postFortuneTaskScheduler.enqueueOnceForToday()
}
- private suspend fun shouldNavigateToMission(): Boolean {
- val fortuneDate = fortuneRepository.fortuneDateFlow.firstOrNull()
- val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE)
- return fortuneDate != todayDate
+ private fun shouldNavigateToMission(
+ missionType: MissionType,
+ ): Boolean {
+ return missionType != MissionType.NONE
}
private fun createNotification(alarm: Alarm, shouldNavigateToMission: Boolean): Notification {
diff --git a/core/common/src/main/java/com/yapp/common/navigation/route/MissionRoute.kt b/core/common/src/main/java/com/yapp/common/navigation/route/MissionRoute.kt
index 2ffd29d6..111c7788 100644
--- a/core/common/src/main/java/com/yapp/common/navigation/route/MissionRoute.kt
+++ b/core/common/src/main/java/com/yapp/common/navigation/route/MissionRoute.kt
@@ -8,8 +8,4 @@ data class MissionRoute(
val missionType: String,
val missionCount: String,
val missionMode: String = MissionMode.REAL.name,
-) {
- companion object {
- const val route = "mission"
- }
-}
+)
diff --git a/core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt b/core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt
index 35298556..c9325870 100644
--- a/core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt
+++ b/core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt
@@ -1,6 +1,5 @@
package com.yapp.datastore
-import android.util.Log
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
@@ -14,7 +13,6 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import java.time.LocalDate
-import java.time.format.DateTimeFormatter
import javax.inject.Inject
import javax.inject.Singleton
@@ -26,15 +24,25 @@ class UserPreferences @Inject constructor(
val USER_ID = longPreferencesKey("user_id")
val USER_NAME = stringPreferencesKey("user_name")
val ONBOARDING_COMPLETED = booleanPreferencesKey("onboarding_completed")
+
val FORTUNE_ID = longPreferencesKey("fortune_id")
- val FORTUNE_DATE = stringPreferencesKey("fortune_date")
+ val FORTUNE_DATE_EPOCH = longPreferencesKey("fortune_date_epoch")
val FORTUNE_IMAGE_ID = intPreferencesKey("fortune_image_id")
val FORTUNE_SCORE = intPreferencesKey("fortune_score")
- val FORTUNE_CHECKED = booleanPreferencesKey("fortune_checked")
- val FIRST_DISMISSED_ALARM_ID = longPreferencesKey("first_dismissed_alarm_id")
- val DISMISSED_DATE = stringPreferencesKey("dismissed_date")
+ val FORTUNE_SEEN = booleanPreferencesKey("fortune_seen")
+ val FORTUNE_TOOLTIP_SHOWN = booleanPreferencesKey("fortune_tooltip_shown")
+ val FORTUNE_CREATING = booleanPreferencesKey("fortune_creating")
+ val FORTUNE_FAILED = booleanPreferencesKey("fortune_failed")
+
+ val FIRST_ALARM_DISMISSED_TODAY = booleanPreferencesKey("first_alarm_dismissed_today")
+ val FIRST_ALARM_DISMISSED_DATE_EPOCH = longPreferencesKey("first_alarm_dismissed_date_epoch")
+
+ val UPDATE_NOTICE_DONT_SHOW_VERSION = stringPreferencesKey("update_notice_dont_show_version")
+ val UPDATE_NOTICE_LAST_SHOWN_DATE_EPOCH = longPreferencesKey("update_notice_last_shown_date_epoch")
}
+ private fun todayEpoch(): Long = LocalDate.now().toEpochDay()
+
val userIdFlow: Flow = dataStore.data
.catch { emit(emptyPreferences()) }
.map { it[Keys.USER_ID] }
@@ -55,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
@@ -70,108 +78,143 @@ class UserPreferences @Inject constructor(
.map { it[Keys.FORTUNE_SCORE] }
.distinctUntilChanged()
- val hasNewFortuneFlow: Flow = dataStore.data
+ val hasUnseenFortuneFlow: Flow = dataStore.data
.catch { emit(emptyPreferences()) }
- .map { preferences ->
- val savedDate = preferences[Keys.FORTUNE_DATE]
- val isChecked = preferences[Keys.FORTUNE_CHECKED] ?: true
- val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE)
- savedDate == todayDate && !isChecked
+ .map { pref ->
+ val isToday = pref[Keys.FORTUNE_DATE_EPOCH] == todayEpoch()
+ isToday && (pref[Keys.FORTUNE_ID] != null) && (pref[Keys.FORTUNE_SEEN] != true)
}
.distinctUntilChanged()
- val firstDismissedAlarmIdFlow: Flow = dataStore.data
+ val shouldShowFortuneToolTipFlow: Flow = dataStore.data
.catch { emit(emptyPreferences()) }
- .map { preferences ->
- val savedDate = preferences[Keys.DISMISSED_DATE]
- val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE)
-
- if (savedDate == todayDate) {
- preferences[Keys.FIRST_DISMISSED_ALARM_ID]
- } else {
- null
- }
+ .map { pref ->
+ val hasTodayFortune = (pref[Keys.FORTUNE_DATE_EPOCH] == todayEpoch()) && (pref[Keys.FORTUNE_ID] != null)
+ val tooltipShown = pref[Keys.FORTUNE_TOOLTIP_SHOWN] ?: false
+ hasTodayFortune && !tooltipShown
}
.distinctUntilChanged()
- suspend fun saveUserId(userId: Long) {
- dataStore.edit { preferences ->
- preferences[Keys.USER_ID] = userId
+ val isFortuneCreatingFlow: Flow = dataStore.data
+ .catch { emit(emptyPreferences()) }
+ .map { it[Keys.FORTUNE_CREATING] ?: false }
+ .distinctUntilChanged()
+
+ val isFortuneFailedFlow: Flow = dataStore.data
+ .catch { emit(emptyPreferences()) }
+ .map { it[Keys.FORTUNE_FAILED] ?: false }
+ .distinctUntilChanged()
+
+ val isFirstAlarmDismissedTodayFlow: Flow = dataStore.data
+ .catch { emit(emptyPreferences()) }
+ .map { pref ->
+ val flag = pref[Keys.FIRST_ALARM_DISMISSED_TODAY] ?: false
+ 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 { it[Keys.USER_ID] = userId }
}
suspend fun saveUserName(userName: String) {
- dataStore.edit { preferences ->
- preferences[Keys.USER_NAME] = userName
+ dataStore.edit { it[Keys.USER_NAME] = userName }
+ }
+
+ suspend fun setOnboardingCompleted() {
+ dataStore.edit { it[Keys.ONBOARDING_COMPLETED] = true }
+ }
+
+ suspend fun markFortuneCreating() {
+ dataStore.edit { pref ->
+ pref[Keys.FORTUNE_CREATING] = true
+ pref[Keys.FORTUNE_FAILED] = false
}
}
- suspend fun saveFortuneId(fortuneId: Long) {
- val currentDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE)
- dataStore.edit { preferences ->
- preferences[Keys.FORTUNE_ID] = fortuneId
- preferences[Keys.FORTUNE_DATE] = currentDate
- preferences[Keys.FORTUNE_CHECKED] = false
+ suspend fun markFortuneCreated(fortuneId: Long) {
+ dataStore.edit { pref ->
+ 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_EPOCH] = today
+ pref[Keys.FORTUNE_CREATING] = false
+ pref[Keys.FORTUNE_FAILED] = false
+
+ if (isNewForToday) {
+ pref[Keys.FORTUNE_SEEN] = false
+ pref[Keys.FORTUNE_TOOLTIP_SHOWN] = false
+ }
}
}
- suspend fun markFortuneAsChecked() {
- dataStore.edit { preferences ->
- preferences[Keys.FORTUNE_CHECKED] = true
+ suspend fun markFortuneFailed() {
+ dataStore.edit { pref ->
+ pref[Keys.FORTUNE_CREATING] = false
+ pref[Keys.FORTUNE_FAILED] = true
}
}
+ suspend fun markFortuneSeen() {
+ dataStore.edit { it[Keys.FORTUNE_SEEN] = true }
+ }
+
+ suspend fun markFortuneTooltipShown() {
+ dataStore.edit { it[Keys.FORTUNE_TOOLTIP_SHOWN] = true }
+ }
+
suspend fun saveFortuneImageId(imageResId: Int) {
- dataStore.edit { preferences ->
- preferences[Keys.FORTUNE_IMAGE_ID] = imageResId
- }
+ dataStore.edit { it[Keys.FORTUNE_IMAGE_ID] = imageResId }
}
suspend fun saveFortuneScore(score: Int) {
- dataStore.edit { preferences ->
- preferences[Keys.FORTUNE_SCORE] = score
- }
+ dataStore.edit { it[Keys.FORTUNE_SCORE] = score }
}
- suspend fun saveFirstDismissedAlarmId(alarmId: Long) {
- dataStore.edit { preferences ->
- val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE)
- if (preferences[Keys.FIRST_DISMISSED_ALARM_ID] == null) {
- preferences[Keys.FIRST_DISMISSED_ALARM_ID] = alarmId
- preferences[Keys.DISMISSED_DATE] = todayDate
- Log.d("UserPreferences", "첫 해제된 알람 ID 저장 완료: $alarmId (날짜: $todayDate)")
- } else {
- Log.d("UserPreferences", "이미 첫 알람 해제 ID가 저장되어 있음)")
- }
+ suspend fun markFirstAlarmDismissedToday() {
+ dataStore.edit { pref ->
+ pref[Keys.FIRST_ALARM_DISMISSED_TODAY] = true
+ pref[Keys.FIRST_ALARM_DISMISSED_DATE_EPOCH] = todayEpoch()
}
}
- suspend fun setOnboardingCompleted() {
- dataStore.edit { preferences ->
- preferences[Keys.ONBOARDING_COMPLETED] = true
- }
+ suspend fun markUpdateNoticeDontShow(version: String) {
+ dataStore.edit { it[Keys.UPDATE_NOTICE_DONT_SHOW_VERSION] = version }
}
- suspend fun clearDismissedAlarmId() {
- dataStore.edit { preferences ->
- preferences.remove(Keys.FIRST_DISMISSED_ALARM_ID)
- preferences.remove(Keys.DISMISSED_DATE)
+ suspend fun markUpdateNoticeShownToday() {
+ dataStore.edit { pref ->
+ pref[Keys.UPDATE_NOTICE_LAST_SHOWN_DATE_EPOCH] = todayEpoch()
}
}
suspend fun clearUserData() {
- dataStore.edit { preferences ->
- preferences.clear()
- }
+ dataStore.edit { it.clear() }
}
- suspend fun clearFortuneId() {
- dataStore.edit { preferences ->
- preferences.remove(Keys.FORTUNE_ID)
- preferences.remove(Keys.FORTUNE_DATE)
- preferences.remove(Keys.FORTUNE_IMAGE_ID)
- preferences.remove(Keys.FORTUNE_SCORE)
- preferences.remove(Keys.FORTUNE_CHECKED)
+ suspend fun clearFortuneData() {
+ dataStore.edit { pref ->
+ pref.remove(Keys.FORTUNE_ID)
+ pref.remove(Keys.FORTUNE_DATE_EPOCH)
+ pref.remove(Keys.FORTUNE_IMAGE_ID)
+ pref.remove(Keys.FORTUNE_SCORE)
+ pref.remove(Keys.FORTUNE_SEEN)
+ pref.remove(Keys.FORTUNE_TOOLTIP_SHOWN)
+ pref.remove(Keys.FORTUNE_CREATING)
+ pref.remove(Keys.FORTUNE_FAILED)
}
}
}
diff --git a/core/designsystem/src/main/res/drawable-xhdpi/ic_100_buble.png b/core/designsystem/src/main/res/drawable-xhdpi/ic_100_buble.png
deleted file mode 100644
index d30beec6..00000000
Binary files a/core/designsystem/src/main/res/drawable-xhdpi/ic_100_buble.png and /dev/null differ
diff --git a/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_delivering_speech_bubble.png b/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_delivering_speech_bubble.png
new file mode 100644
index 00000000..c75d2db3
Binary files /dev/null and b/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_delivering_speech_bubble.png differ
diff --git a/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_waiting_speech_bubble.png b/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_waiting_speech_bubble.png
new file mode 100644
index 00000000..a119387d
Binary files /dev/null and b/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_waiting_speech_bubble.png differ
diff --git a/core/designsystem/src/main/res/drawable-xxhdpi/ic_100_buble.png b/core/designsystem/src/main/res/drawable-xxhdpi/ic_100_buble.png
deleted file mode 100644
index 469a7b3f..00000000
Binary files a/core/designsystem/src/main/res/drawable-xxhdpi/ic_100_buble.png and /dev/null differ
diff --git a/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_delivering_speech_bubble.png b/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_delivering_speech_bubble.png
new file mode 100644
index 00000000..318d11d6
Binary files /dev/null and b/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_delivering_speech_bubble.png differ
diff --git a/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_waiting_speech_bubble.png b/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_waiting_speech_bubble.png
new file mode 100644
index 00000000..83b665f4
Binary files /dev/null and b/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_waiting_speech_bubble.png differ
diff --git a/core/designsystem/src/main/res/drawable/ic_100_buble.png b/core/designsystem/src/main/res/drawable/ic_100_buble.png
deleted file mode 100644
index e69978c4..00000000
Binary files a/core/designsystem/src/main/res/drawable/ic_100_buble.png and /dev/null differ
diff --git a/core/designsystem/src/main/res/raw/fortune_loading.json b/core/designsystem/src/main/res/raw/fortune_loading.json
new file mode 100644
index 00000000..cb570812
--- /dev/null
+++ b/core/designsystem/src/main/res/raw/fortune_loading.json
@@ -0,0 +1 @@
+{"nm":"컴포지션 2","h":500,"w":700,"meta":{"g":"@lottiefiles/toolkit-js 0.66.4","tc":"#202f44"},"layers":[{"ty":2,"nm":"Group 1948760243.png","sr":1,"st":1,"op":45,"ip":0,"ln":"2856","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[151.5,137]},"s":{"a":0,"k":[61.6,61.6,101.65]},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[130.5,159,0],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.139,178.955,0],"t":15},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[233.8,193.529,0],"t":31},{"s":[285.485,192.267,0],"t":45}]},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[17.4],"t":0},{"s":[32.4],"t":45}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"refId":"1","ind":1},{"ty":2,"nm":"Group 1948760248-1.png","sr":1,"st":0,"op":45,"ip":0,"ln":"2855","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[22,121.064]},"s":{"a":0,"k":[70.27,70.27,89.655]},"p":{"a":0,"k":[386,148,0]},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[-9.304],"t":0},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":45},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[10],"t":48},{"s":[0],"t":51}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"refId":"2","ind":2},{"ty":2,"nm":"Vector 27856.png","sr":1,"st":0,"op":45,"ip":0,"ln":"2854","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[93.94,9.052]},"s":{"a":0,"k":[75.628,72.572,100]},"p":{"a":0,"k":[310.5,242.5,0]},"r":{"a":0,"k":2},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"refId":"3","ind":3},{"ty":2,"nm":"Group 1948760248.png","sr":1,"st":0,"op":45,"ip":0,"ln":"2853","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[132.5,232]},"s":{"a":0,"k":[76.4,76.4,76.4]},"p":{"a":0,"k":[370,256,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"refId":"4","ind":4}],"v":"5.7.0","fr":30,"op":45,"ip":0,"assets":[{"id":"1","e":1,"w":303,"h":274,"p":"data:image/webp;base64,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","u":""},{"id":"2","e":1,"w":100,"h":148,"p":"data:image/webp;base64,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","u":""},{"id":"3","e":1,"w":154,"h":124,"p":"data:image/webp;base64,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","u":""},{"id":"4","e":1,"w":265,"h":464,"p":"data:image/webp;base64,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","u":""}]}
\ No newline at end of file
diff --git a/core/network/src/main/java/com/yapp/network/di/NetworkModule.kt b/core/network/src/main/java/com/yapp/network/di/NetworkModule.kt
index 10276dfb..25dfb118 100644
--- a/core/network/src/main/java/com/yapp/network/di/NetworkModule.kt
+++ b/core/network/src/main/java/com/yapp/network/di/NetworkModule.kt
@@ -6,7 +6,6 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
-import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
@@ -39,21 +38,7 @@ object NetworkModule {
@Provides
@Singleton
- @Auth
- fun provideAuthOkHttpClient(
- loggingInterceptor: HttpLoggingInterceptor,
- authInterceptor: Interceptor,
- ): OkHttpClient =
- OkHttpClient.Builder()
- .retryOnConnectionFailure(true)
- .addInterceptor(loggingInterceptor)
- .addInterceptor(authInterceptor)
- .build()
-
- @Provides
- @Singleton
- @NoneAuth
- fun provideNoneAuthOkHttpClient(
+ fun provideHttpClient(
loggingInterceptor: HttpLoggingInterceptor,
): OkHttpClient =
OkHttpClient.Builder()
@@ -66,18 +51,8 @@ object NetworkModule {
@Provides
@Singleton
- @Auth
- fun provideAuthRetrofit(@Auth okHttpClient: OkHttpClient, buildConfigFieldProvider: BuildConfigFieldProvider): Retrofit = Retrofit.Builder()
- .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
- .baseUrl(buildConfigFieldProvider.get().baseUrl)
- .client(okHttpClient)
- .build()
-
- @Provides
- @Singleton
- @NoneAuth
- fun provideNoneAuthRetrofit(
- @NoneAuth okHttpClient: OkHttpClient,
+ fun provideRetrofit(
+ okHttpClient: OkHttpClient,
buildConfigFieldProvider: BuildConfigFieldProvider,
json: Json,
): Retrofit =
@@ -86,14 +61,4 @@ object NetworkModule {
.baseUrl(buildConfigFieldProvider.get().baseUrl)
.client(okHttpClient)
.build()
-
- @Provides
- @Singleton
- @S3
- fun provideS3Retrofit(@NoneAuth okHttpClient: OkHttpClient, buildConfigFieldProvider: BuildConfigFieldProvider): Retrofit =
- Retrofit.Builder()
- .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
- .baseUrl(buildConfigFieldProvider.get().baseUrl)
- .client(okHttpClient)
- .build()
}
diff --git a/core/network/src/main/java/com/yapp/network/di/Qualifier.kt b/core/network/src/main/java/com/yapp/network/di/Qualifier.kt
deleted file mode 100644
index 68817100..00000000
--- a/core/network/src/main/java/com/yapp/network/di/Qualifier.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.yapp.network.di
-
-import javax.inject.Qualifier
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-annotation class NoneAuth
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-annotation class Auth
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-annotation class S3
diff --git a/core/ui/src/main/java/com/yapp/ui/component/navigation/NavigationBarScrim.kt b/core/ui/src/main/java/com/yapp/ui/component/navigation/NavigationBarScrim.kt
new file mode 100644
index 00000000..c0bcb5a7
--- /dev/null
+++ b/core/ui/src/main/java/com/yapp/ui/component/navigation/NavigationBarScrim.kt
@@ -0,0 +1,26 @@
+package com.yapp.ui.component.navigation
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.windowInsetsBottomHeight
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.zIndex
+
+@Composable
+fun BoxScope.NavigationBarScrim() {
+ Box(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .fillMaxWidth()
+ .windowInsetsBottomHeight(WindowInsets.navigationBars)
+ .background(Color.Black)
+ .zIndex(1f),
+ )
+}
diff --git a/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSource.kt b/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSource.kt
index eb9fb350..8ff592d2 100644
--- a/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSource.kt
+++ b/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSource.kt
@@ -5,8 +5,6 @@ import com.yapp.domain.model.Alarm
import kotlinx.coroutines.flow.Flow
interface AlarmLocalDataSource {
- val firstDismissedAlarmIdFlow: Flow
-
fun getAllAlarms(): Flow>
fun getAlarmsByTime(hour: Int, minute: Int): Flow>
suspend fun insertAlarm(alarm: AlarmEntity): Long
@@ -14,6 +12,4 @@ interface AlarmLocalDataSource {
suspend fun updateAlarmActive(id: Long, active: Boolean): Int
suspend fun getAlarm(id: Long): Alarm?
suspend fun deleteAlarm(id: Long): Int
- suspend fun saveFirstDismissedAlarmId(alarmId: Long)
- suspend fun clearDismissedAlarmId()
}
diff --git a/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSourceImpl.kt b/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSourceImpl.kt
index 7c7425b2..03fecd55 100644
--- a/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSourceImpl.kt
+++ b/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSourceImpl.kt
@@ -3,7 +3,6 @@ package com.yapp.data.local.datasource
import com.yapp.database.AlarmDao
import com.yapp.database.AlarmEntity
import com.yapp.database.toDomain
-import com.yapp.datastore.UserPreferences
import com.yapp.domain.model.Alarm
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@@ -11,10 +10,7 @@ import javax.inject.Inject
class AlarmLocalDataSourceImpl @Inject constructor(
private val alarmDao: AlarmDao,
- private val userPreferences: UserPreferences,
) : AlarmLocalDataSource {
- override val firstDismissedAlarmIdFlow: Flow = userPreferences.firstDismissedAlarmIdFlow
-
override fun getAllAlarms(): Flow> {
return alarmDao.getAllAlarms()
.map { alarmEntities -> alarmEntities.map { it.toDomain() } }
@@ -45,12 +41,4 @@ class AlarmLocalDataSourceImpl @Inject constructor(
override suspend fun deleteAlarm(id: Long): Int {
return alarmDao.deleteAlarm(id)
}
-
- override suspend fun saveFirstDismissedAlarmId(alarmId: Long) {
- userPreferences.saveFirstDismissedAlarmId(alarmId)
- }
-
- override suspend fun clearDismissedAlarmId() {
- userPreferences.clearDismissedAlarmId()
- }
}
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 149234f7..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
@@ -1,20 +1,27 @@
package com.yapp.data.local.datasource
+import com.yapp.domain.model.FortuneCreateStatus
import kotlinx.coroutines.flow.Flow
interface FortuneLocalDataSource {
val fortuneIdFlow: Flow
- val fortuneDateFlow: Flow
+ val fortuneDateEpochFlow: Flow
val fortuneImageIdFlow: Flow
val fortuneScoreFlow: Flow
- val hasNewFortuneFlow: Flow
- val firstDismissedAlarmIdFlow: Flow
+ val hasUnseenFortuneFlow: Flow
+ val shouldShowFortuneToolTipFlow: Flow
+ val isFirstAlarmDismissedTodayFlow: Flow
- suspend fun saveFortuneId(fortuneId: Long)
- suspend fun markFortuneAsChecked()
+ val fortuneCreateStatusFlow: Flow
+
+ suspend fun markFortuneCreating()
+ suspend fun markFortuneCreated(fortuneId: Long)
+ suspend fun markFortuneFailed()
+ suspend fun markFortuneSeen()
+ suspend fun markFortuneTooltipShown()
suspend fun saveFortuneImageId(imageResId: Int)
suspend fun saveFortuneScore(score: Int)
- suspend fun saveFirstDismissedAlarmId(alarmId: Long)
- suspend fun clearDismissedAlarmId()
- suspend fun clearFortuneId()
+ suspend fun markFirstAlarmDismissedToday()
+
+ suspend fun clearFortuneData()
}
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 6dbe10f9..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
@@ -1,6 +1,10 @@
package com.yapp.data.local.datasource
import com.yapp.datastore.UserPreferences
+import com.yapp.domain.model.FortuneCreateStatus
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import java.time.LocalDate
import javax.inject.Inject
class FortuneLocalDataSourceImpl @Inject constructor(
@@ -8,18 +12,47 @@ 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 hasNewFortuneFlow = userPreferences.hasNewFortuneFlow
- override val firstDismissedAlarmIdFlow = userPreferences.firstDismissedAlarmIdFlow
+ override val hasUnseenFortuneFlow = userPreferences.hasUnseenFortuneFlow
+ override val shouldShowFortuneToolTipFlow = userPreferences.shouldShowFortuneToolTipFlow
+ override val isFirstAlarmDismissedTodayFlow = userPreferences.isFirstAlarmDismissedTodayFlow
- override suspend fun saveFortuneId(fortuneId: Long) {
- userPreferences.saveFortuneId(fortuneId)
+ override val fortuneCreateStatusFlow = combine(
+ userPreferences.fortuneIdFlow,
+ userPreferences.fortuneDateEpochFlow,
+ userPreferences.isFortuneCreatingFlow,
+ userPreferences.isFortuneFailedFlow,
+ ) { fortuneId, fortuneDate, isCreating, isFailed ->
+ when {
+ isFailed -> FortuneCreateStatus.Failure
+ isCreating -> FortuneCreateStatus.Creating
+ fortuneId != null && fortuneDate == todayEpoch() -> FortuneCreateStatus.Success(fortuneId)
+ else -> FortuneCreateStatus.Idle
+ }
+ }.distinctUntilChanged()
+
+ private fun todayEpoch(): Long = LocalDate.now().toEpochDay()
+
+ override suspend fun markFortuneCreating() {
+ userPreferences.markFortuneCreating()
+ }
+
+ override suspend fun markFortuneCreated(fortuneId: Long) {
+ userPreferences.markFortuneCreated(fortuneId)
+ }
+
+ override suspend fun markFortuneFailed() {
+ userPreferences.markFortuneFailed()
+ }
+
+ override suspend fun markFortuneSeen() {
+ userPreferences.markFortuneSeen()
}
- override suspend fun markFortuneAsChecked() {
- userPreferences.markFortuneAsChecked()
+ override suspend fun markFortuneTooltipShown() {
+ userPreferences.markFortuneTooltipShown()
}
override suspend fun saveFortuneImageId(imageResId: Int) {
@@ -30,15 +63,11 @@ class FortuneLocalDataSourceImpl @Inject constructor(
userPreferences.saveFortuneScore(score)
}
- override suspend fun saveFirstDismissedAlarmId(alarmId: Long) {
- userPreferences.saveFirstDismissedAlarmId(alarmId)
- }
-
- override suspend fun clearDismissedAlarmId() {
- userPreferences.clearDismissedAlarmId()
+ override suspend fun markFirstAlarmDismissedToday() {
+ userPreferences.markFirstAlarmDismissedToday()
}
- override suspend fun clearFortuneId() {
- userPreferences.clearFortuneId()
+ override suspend fun clearFortuneData() {
+ userPreferences.clearFortuneData()
}
}
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/remote/di/ServiceModule.kt b/data/src/main/java/com/yapp/data/remote/di/ServiceModule.kt
index 458db14b..be0e97f5 100644
--- a/data/src/main/java/com/yapp/data/remote/di/ServiceModule.kt
+++ b/data/src/main/java/com/yapp/data/remote/di/ServiceModule.kt
@@ -1,7 +1,6 @@
package com.yapp.data.remote.di
import com.yapp.data.remote.service.ApiService
-import com.yapp.network.di.NoneAuth
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -14,6 +13,6 @@ import javax.inject.Singleton
object ServiceModule {
@Provides
@Singleton
- fun providesApiService(@NoneAuth retrofit: Retrofit): ApiService =
+ fun providesApiService(retrofit: Retrofit): ApiService =
retrofit.create(ApiService::class.java)
}
diff --git a/data/src/main/java/com/yapp/data/repositoryimpl/AlarmRepositoryImpl.kt b/data/src/main/java/com/yapp/data/repositoryimpl/AlarmRepositoryImpl.kt
index a1c72135..50385b4f 100644
--- a/data/src/main/java/com/yapp/data/repositoryimpl/AlarmRepositoryImpl.kt
+++ b/data/src/main/java/com/yapp/data/repositoryimpl/AlarmRepositoryImpl.kt
@@ -16,8 +16,6 @@ class AlarmRepositoryImpl @Inject constructor(
private val ringtoneManagerHelper: RingtoneManagerHelper,
private val soundPlayer: SoundPlayer,
) : AlarmRepository {
- override val firstDismissedAlarmIdFlow: Flow = alarmLocalDataSource.firstDismissedAlarmIdFlow
-
override suspend fun getAlarmSounds(): Result> = runCatching {
ringtoneManagerHelper.getAlarmSounds().map { (title, uri) ->
AlarmSound(title, uri)
@@ -93,12 +91,4 @@ class AlarmRepositoryImpl @Inject constructor(
throw Exception("No rows deleted")
}
}
-
- override suspend fun saveFirstDismissedAlarmId(alarmId: Long) {
- alarmLocalDataSource.saveFirstDismissedAlarmId(alarmId)
- }
-
- override suspend fun clearDismissedAlarmId() {
- alarmLocalDataSource.clearDismissedAlarmId()
- }
}
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 d3abb0e9..1c761ba6 100644
--- a/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt
+++ b/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt
@@ -4,6 +4,7 @@ import com.yapp.data.local.datasource.FortuneLocalDataSource
import com.yapp.data.remote.datasource.FortuneDataSource
import com.yapp.data.remote.dto.response.toDomain
import com.yapp.domain.model.Fortune
+import com.yapp.domain.model.FortuneCreateStatus
import com.yapp.domain.repository.FortuneRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
@@ -12,32 +13,35 @@ class FortuneRepositoryImpl @Inject constructor(
private val fortuneLocalDataSource: FortuneLocalDataSource,
private val fortuneRemoteDataSource: FortuneDataSource,
) : 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 hasNewFortuneFlow: Flow = fortuneLocalDataSource.hasNewFortuneFlow
- override val firstDismissedAlarmIdFlow: Flow = fortuneLocalDataSource.firstDismissedAlarmIdFlow
+ override val hasUnseenFortuneFlow: Flow = fortuneLocalDataSource.hasUnseenFortuneFlow
+ override val shouldShowFortuneToolTipFlow: Flow = fortuneLocalDataSource.shouldShowFortuneToolTipFlow
+ override val isFirstAlarmDismissedTodayFlow: Flow = fortuneLocalDataSource.isFirstAlarmDismissedTodayFlow
+
+ override val fortuneCreateStatusFlow: Flow = fortuneLocalDataSource.fortuneCreateStatusFlow
- override suspend fun saveFortuneId(fortuneId: Long) = fortuneLocalDataSource.saveFortuneId(fortuneId)
- override suspend fun markFortuneAsChecked() = fortuneLocalDataSource.markFortuneAsChecked()
+ override suspend fun markFortuneAsCreating() = fortuneLocalDataSource.markFortuneCreating()
+ override suspend fun markFortuneAsCreated(fortuneId: Long) = fortuneLocalDataSource.markFortuneCreated(fortuneId)
+ override suspend fun markFortuneAsFailed() = fortuneLocalDataSource.markFortuneFailed()
+ override suspend fun markFortuneSeen() = fortuneLocalDataSource.markFortuneSeen()
+ override suspend fun markFortuneTooltipShown() = fortuneLocalDataSource.markFortuneTooltipShown()
override suspend fun saveFortuneImageId(imageResId: Int) = fortuneLocalDataSource.saveFortuneImageId(imageResId)
override suspend fun saveFortuneScore(score: Int) = fortuneLocalDataSource.saveFortuneScore(score)
- override suspend fun saveFirstDismissedAlarmId(alarmId: Long) = fortuneLocalDataSource.saveFirstDismissedAlarmId(alarmId)
- override suspend fun clearDismissedAlarmId() = fortuneLocalDataSource.clearDismissedAlarmId()
- override suspend fun clearFortuneId() = fortuneLocalDataSource.clearFortuneId()
+ override suspend fun markFirstAlarmDismissedToday() = fortuneLocalDataSource.markFirstAlarmDismissedToday()
+
+ override suspend fun clearFortuneData() = fortuneLocalDataSource.clearFortuneData()
override suspend fun postFortune(userId: Long): Result {
return fortuneRemoteDataSource.postFortune(userId)
- .mapCatching { fortuneResponse ->
- fortuneResponse.toDomain()
- }
+ .mapCatching { it.toDomain() }
}
override suspend fun getFortune(fortuneId: Long): Result {
return fortuneRemoteDataSource.getFortune(fortuneId)
- .mapCatching { fortuneResponse ->
- fortuneResponse.toDomain()
- }
+ .mapCatching { it.toDomain() }
}
}
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/model/AlarmDay.kt b/domain/src/main/java/com/yapp/domain/model/AlarmDay.kt
index beaead69..7f349ed5 100644
--- a/domain/src/main/java/com/yapp/domain/model/AlarmDay.kt
+++ b/domain/src/main/java/com/yapp/domain/model/AlarmDay.kt
@@ -1,5 +1,7 @@
package com.yapp.domain.model
+import java.time.DayOfWeek
+
enum class AlarmDay(val bitValue: Int) {
SUN(0b0000001), // 1
MON(0b0000010), // 2
@@ -11,8 +13,13 @@ enum class AlarmDay(val bitValue: Int) {
;
}
-fun AlarmDay.toDayOfWeek(): java.time.DayOfWeek {
- return java.time.DayOfWeek.of(((this.ordinal + 6) % 7) + 1)
+fun AlarmDay.toDayOfWeek(): DayOfWeek {
+ return DayOfWeek.of(((this.ordinal + 6) % 7) + 1)
+}
+
+fun DayOfWeek.toAlarmDay(): AlarmDay {
+ val index = (this.value % 7)
+ return AlarmDay.entries[index]
}
fun Set.toRepeatDays(): Int {
diff --git a/domain/src/main/java/com/yapp/domain/model/FortuneCreateStatus.kt b/domain/src/main/java/com/yapp/domain/model/FortuneCreateStatus.kt
new file mode 100644
index 00000000..27ae9ad0
--- /dev/null
+++ b/domain/src/main/java/com/yapp/domain/model/FortuneCreateStatus.kt
@@ -0,0 +1,8 @@
+package com.yapp.domain.model
+
+sealed class FortuneCreateStatus {
+ data object Idle : FortuneCreateStatus()
+ data object Creating : FortuneCreateStatus()
+ data class Success(val fortuneId: Long) : FortuneCreateStatus()
+ data object Failure : FortuneCreateStatus()
+}
diff --git a/domain/src/main/java/com/yapp/domain/model/MissionMode.kt b/domain/src/main/java/com/yapp/domain/model/MissionMode.kt
new file mode 100644
index 00000000..16009fc2
--- /dev/null
+++ b/domain/src/main/java/com/yapp/domain/model/MissionMode.kt
@@ -0,0 +1,13 @@
+package com.yapp.domain.model
+
+enum class MissionMode {
+ REAL,
+ PREVIEW,
+ ;
+
+ companion object {
+ fun fromRaw(raw: String?): MissionMode {
+ return raw?.let { entries.find { it.name == raw } } ?: REAL
+ }
+ }
+}
diff --git a/domain/src/main/java/com/yapp/domain/repository/AlarmRepository.kt b/domain/src/main/java/com/yapp/domain/repository/AlarmRepository.kt
index f7dac361..60123473 100644
--- a/domain/src/main/java/com/yapp/domain/repository/AlarmRepository.kt
+++ b/domain/src/main/java/com/yapp/domain/repository/AlarmRepository.kt
@@ -6,8 +6,6 @@ import com.yapp.domain.model.AlarmSound
import kotlinx.coroutines.flow.Flow
interface AlarmRepository {
- val firstDismissedAlarmIdFlow: Flow
-
suspend fun getAlarmSounds(): Result>
fun initializeSoundPlayer(uri: Uri)
fun playAlarmSound(volume: Int)
@@ -21,6 +19,4 @@ interface AlarmRepository {
suspend fun updateAlarmActive(id: Long, active: Boolean): Result
suspend fun getAlarm(id: Long): Result
suspend fun deleteAlarm(id: Long): Result
- suspend fun saveFirstDismissedAlarmId(alarmId: Long)
- suspend fun clearDismissedAlarmId()
}
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 23598e3c..372fd5fe 100644
--- a/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt
+++ b/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt
@@ -1,23 +1,31 @@
package com.yapp.domain.repository
import com.yapp.domain.model.Fortune
+import com.yapp.domain.model.FortuneCreateStatus
import kotlinx.coroutines.flow.Flow
interface FortuneRepository {
val fortuneIdFlow: Flow
- val fortuneDateFlow: Flow
+ val fortuneDateEpochFlow: Flow
val fortuneImageIdFlow: Flow
val fortuneScoreFlow: Flow
- val hasNewFortuneFlow: Flow
- val firstDismissedAlarmIdFlow: Flow
+ val hasUnseenFortuneFlow: Flow
+ val shouldShowFortuneToolTipFlow: Flow
+ val isFirstAlarmDismissedTodayFlow: Flow
- suspend fun saveFortuneId(fortuneId: Long)
- suspend fun markFortuneAsChecked()
+ val fortuneCreateStatusFlow: Flow
+
+ suspend fun markFortuneAsCreating()
+ suspend fun markFortuneAsCreated(fortuneId: Long)
+ suspend fun markFortuneAsFailed()
+ suspend fun markFortuneSeen()
+ suspend fun markFortuneTooltipShown()
suspend fun saveFortuneImageId(imageResId: Int)
suspend fun saveFortuneScore(score: Int)
- suspend fun saveFirstDismissedAlarmId(alarmId: Long)
- suspend fun clearDismissedAlarmId()
- suspend fun clearFortuneId()
+ suspend fun markFirstAlarmDismissedToday()
+
+ suspend fun clearFortuneData()
+
suspend fun postFortune(userId: Long): Result
suspend fun getFortune(fortuneId: Long): Result
}
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/AlarmInteractionActivity.kt b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/AlarmInteractionActivity.kt
index 92d52cee..e95a93b2 100644
--- a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/AlarmInteractionActivity.kt
+++ b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/AlarmInteractionActivity.kt
@@ -10,7 +10,10 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.ui.Modifier
import androidx.core.util.Consumer
import androidx.navigation.compose.NavHost
import com.yapp.alarm.AlarmConstants
@@ -18,6 +21,7 @@ import com.yapp.alarm.receivers.AlarmInteractionActivityReceiver
import com.yapp.common.navigation.rememberOrbitNavigator
import com.yapp.common.navigation.route.AlarmInteractionBaseRoute
import com.yapp.domain.model.Alarm
+import com.yapp.ui.component.navigation.NavigationBarScrim
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
@@ -45,14 +49,19 @@ class AlarmInteractionActivity : ComponentActivity() {
setContent {
val navigator = rememberOrbitNavigator()
- NavHost(
- navController = navigator.navController,
- startDestination = AlarmInteractionBaseRoute,
- ) {
- alarmInteractionNavGraph(
- navigator = navigator,
- alarm = alarm,
- )
+ Box {
+ NavHost(
+ modifier = Modifier.navigationBarsPadding(),
+ navController = navigator.navController,
+ startDestination = AlarmInteractionBaseRoute,
+ ) {
+ alarmInteractionNavGraph(
+ navigator = navigator,
+ alarm = alarm,
+ )
+ }
+
+ NavigationBarScrim()
}
DisposableEffect(this, navigator.navController) {
diff --git a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionContract.kt b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionContract.kt
index ea4d0b68..9eee8bad 100644
--- a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionContract.kt
+++ b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionContract.kt
@@ -14,7 +14,7 @@ class AlarmActionContract {
val snoozeEnabled: Boolean = true,
val snoozeInterval: Int = 5,
val snoozeCount: Int = 5,
- val isFirstMission: Boolean? = null,
+ val shouldShowMissionStart: Boolean? = null,
) : UiState
sealed class Action {
diff --git a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionScreen.kt b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionScreen.kt
index d58bdda1..8ff13cb9 100644
--- a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionScreen.kt
+++ b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionScreen.kt
@@ -84,7 +84,7 @@ internal fun AlarmActionScreen(
snoozeEnabled = state.snoozeEnabled,
snoozeInterval = state.snoozeInterval,
snoozeCount = state.snoozeCount,
- isFirstMission = state.isFirstMission,
+ isFirstMission = state.shouldShowMissionStart,
onSnoozeClick = { processAction(AlarmActionContract.Action.Snooze) },
onDismissClick = {
processAction(AlarmActionContract.Action.Dismiss)
diff --git a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionViewModel.kt b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionViewModel.kt
index a15a85c5..49182b15 100644
--- a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionViewModel.kt
+++ b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionViewModel.kt
@@ -6,10 +6,9 @@ import androidx.lifecycle.ViewModel
import com.yapp.alarm.pendingIntent.interaction.createAlarmDismissIntent
import com.yapp.alarm.pendingIntent.interaction.createAlarmSnoozeIntent
import com.yapp.domain.model.Alarm
-import com.yapp.domain.repository.FortuneRepository
+import com.yapp.domain.model.MissionType
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.firstOrNull
import org.orbitmvi.orbit.Container
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.syntax.simple.intent
@@ -18,7 +17,6 @@ import org.orbitmvi.orbit.syntax.simple.reduce
import org.orbitmvi.orbit.viewmodel.container
import java.time.LocalDate
import java.time.LocalTime
-import java.time.format.DateTimeFormatter
import java.time.format.TextStyle
import java.util.Locale
import javax.inject.Inject
@@ -26,14 +24,13 @@ import javax.inject.Inject
@HiltViewModel
class AlarmActionViewModel @Inject constructor(
private val app: Application,
- private val fortuneRepository: FortuneRepository,
savedStateHandle: SavedStateHandle,
) : ViewModel(), ContainerHost {
override val container: Container = container(
initialState = AlarmActionContract.State(),
) {
- fetchIsFirstMission()
+ fetchShouldShowMissionStart()
initializeAlarmState()
startClock()
}
@@ -58,13 +55,9 @@ class AlarmActionViewModel @Inject constructor(
}
}
- private fun fetchIsFirstMission() = intent {
- val fortuneDate = fortuneRepository.fortuneDateFlow.firstOrNull()
- val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE)
- val isFirstMission = fortuneDate != todayDate
-
+ private fun fetchShouldShowMissionStart() = intent {
reduce {
- state.copy(isFirstMission = isFirstMission)
+ state.copy(shouldShowMissionStart = (alarm?.missionType ?: MissionType.NONE) != MissionType.NONE)
}
}
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/fortune/build.gradle.kts b/feature/fortune/build.gradle.kts
index ae450155..543510e8 100644
--- a/feature/fortune/build.gradle.kts
+++ b/feature/fortune/build.gradle.kts
@@ -12,10 +12,14 @@ dependencies {
implementation(projects.core.ui)
implementation(projects.core.common)
implementation(projects.core.analytics)
+ implementation(projects.core.alarm)
implementation(libs.orbit.core)
implementation(libs.orbit.compose)
implementation(libs.orbit.viewmodel)
implementation(libs.coil.compose)
+ implementation(libs.androidx.work.runtime)
+ testImplementation(libs.androidx.work.testing)
+ androidTestImplementation(libs.androidx.work.testing)
implementation(projects.domain)
implementation(projects.core.media)
}
diff --git a/feature/fortune/src/main/java/com/yapp/fortune/FortuneNavGraph.kt b/feature/fortune/src/main/java/com/yapp/fortune/FortuneNavGraph.kt
index 5f1b5133..0a7b62ce 100644
--- a/feature/fortune/src/main/java/com/yapp/fortune/FortuneNavGraph.kt
+++ b/feature/fortune/src/main/java/com/yapp/fortune/FortuneNavGraph.kt
@@ -5,6 +5,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
+import androidx.navigation.navDeepLink
import androidx.navigation.navOptions
import androidx.navigation.navigation
import com.yapp.common.navigation.OrbitNavigator
@@ -19,7 +20,11 @@ fun NavGraphBuilder.fortuneNavGraph(
snackBarHostState: SnackbarHostState,
) {
navigation(startDestination = FortuneDestination.Fortune) {
- composable { backStackEntry ->
+ composable(
+ deepLinks = listOf(
+ navDeepLink { uriPattern = "orbitapp://fortune" },
+ ),
+ ) { backStackEntry ->
val viewModel = backStackEntry.sharedHiltViewModel(navigator.navController)
val coroutineScope = rememberCoroutineScope()
diff --git a/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt b/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt
index f5109a3b..1eb97abc 100644
--- a/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt
+++ b/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt
@@ -3,11 +3,13 @@ package com.yapp.fortune
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
-import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
@@ -15,6 +17,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@@ -34,6 +37,7 @@ import com.yapp.fortune.component.FortuneTopAppBar
import com.yapp.fortune.component.SlidingIndicator
import com.yapp.fortune.page.FortunePager
import com.yapp.ui.component.lottie.LottieAnimation
+import kotlinx.coroutines.delay
import java.math.BigDecimal
import java.math.RoundingMode
@@ -184,21 +188,49 @@ fun FortuneScreen(
@Composable
fun FortuneLoadingScreen() {
+ var isDelivering by remember { mutableStateOf(false) }
+
+ LaunchedEffect(Unit) {
+ while (true) {
+ delay(2000)
+ isDelivering = !isDelivering
+ }
+ }
+
Box(
modifier = Modifier
.fillMaxSize()
.background(OrbitTheme.colors.gray_900.copy(alpha = 0.7f)),
contentAlignment = Alignment.Center,
) {
- LottieAnimation(
- modifier = Modifier
- .size(70.dp),
- resId = core.designsystem.R.raw.star_loading,
- )
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(6.dp),
+ ) {
+ val imageRes = if (isDelivering) {
+ core.designsystem.R.drawable.ic_fortune_delivering_speech_bubble
+ } else {
+ core.designsystem.R.drawable.ic_fortune_waiting_speech_bubble
+ }
+ Image(
+ painter = painterResource(id = imageRes),
+ contentDescription = null,
+ )
+
+ LottieAnimation(
+ modifier = Modifier
+ .width(375.dp)
+ .height(267.dp),
+ resId = core.designsystem.R.raw.fortune_loading,
+ )
+ }
}
}
@Composable
@Preview
-fun FortuneRoutePreview() {
+private fun FortuneLoadingScreenPreview() {
+ OrbitTheme {
+ FortuneLoadingScreen()
+ }
}
diff --git a/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt b/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt
index 034c1590..4a83a561 100644
--- a/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt
+++ b/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt
@@ -4,11 +4,13 @@ import android.app.Application
import android.util.Log
import androidx.annotation.DrawableRes
import androidx.lifecycle.ViewModel
+import com.yapp.domain.model.FortuneCreateStatus
import com.yapp.domain.repository.FortuneRepository
import com.yapp.fortune.page.toFortunePages
import com.yapp.media.decoder.ImageUtils
import com.yapp.media.storage.ImageSaver
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import org.orbitmvi.orbit.Container
import org.orbitmvi.orbit.ContainerHost
@@ -16,8 +18,6 @@ import org.orbitmvi.orbit.syntax.simple.intent
import org.orbitmvi.orbit.syntax.simple.postSideEffect
import org.orbitmvi.orbit.syntax.simple.reduce
import org.orbitmvi.orbit.viewmodel.container
-import java.time.LocalDate
-import java.time.format.DateTimeFormatter
import javax.inject.Inject
@HiltViewModel
@@ -30,7 +30,7 @@ class FortuneViewModel @Inject constructor(
override val container: Container = container(
initialState = FortuneContract.State(),
) {
- loadFortune()
+ observeFortune()
}
fun processAction(action: FortuneContract.Action) {
@@ -50,14 +50,31 @@ class FortuneViewModel @Inject constructor(
}
}
- private fun loadFortune() = intent {
- val fortuneId = fortuneRepository.fortuneIdFlow.firstOrNull()
- val firstDismissedAlarmId = fortuneRepository.firstDismissedAlarmIdFlow.firstOrNull()
- val fortuneDate = fortuneRepository.fortuneDateFlow.firstOrNull()
- fortuneId?.let { fetchAndUpdateFortune(it, firstDismissedAlarmId, fortuneDate) }
+ private fun observeFortune() = intent {
+ fortuneRepository.fortuneCreateStatusFlow.collect { status ->
+ when (status) {
+ is FortuneCreateStatus.Creating -> {
+ reduce { state.copy(isLoading = true) }
+ }
+
+ is FortuneCreateStatus.Success -> {
+ fetchAndUpdateFortune(
+ fortuneId = status.fortuneId,
+ isFirstAlarmDismissedToday = fortuneRepository.isFirstAlarmDismissedTodayFlow.first(),
+ )
+ }
+
+ is FortuneCreateStatus.Failure, FortuneCreateStatus.Idle -> {
+ postSideEffect(FortuneContract.SideEffect.NavigateToHome)
+ }
+ }
+ }
}
- private fun fetchAndUpdateFortune(fortuneId: Long, firstDismissedAlarmId: Long?, fortuneDate: String?) = intent {
+ private fun fetchAndUpdateFortune(
+ fortuneId: Long,
+ isFirstAlarmDismissedToday: Boolean,
+ ) = intent {
reduce { state.copy(isLoading = true) }
fortuneRepository.getFortune(fortuneId).onSuccess { fortune ->
@@ -65,8 +82,9 @@ class FortuneViewModel @Inject constructor(
val imageId = savedImageId ?: getRandomImage()
val formattedTitle = fortune.dailyFortuneTitle.replace(",", ",\n").trim()
- val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE)
- val hasReward = (fortuneDate == todayDate) && (firstDismissedAlarmId != null)
+
+ fortuneRepository.markFortuneSeen()
+
reduce {
state.copy(
isLoading = false,
@@ -75,7 +93,7 @@ class FortuneViewModel @Inject constructor(
avgFortuneScore = fortune.avgFortuneScore,
fortunePages = fortune.toFortunePages(),
fortuneImageId = imageId,
- hasReward = hasReward,
+ hasReward = isFirstAlarmDismissedToday,
)
}
}.onFailure { error ->
diff --git a/feature/fortune/src/main/java/com/yapp/fortune/di/SchedulerModule.kt b/feature/fortune/src/main/java/com/yapp/fortune/di/SchedulerModule.kt
new file mode 100644
index 00000000..47fbd0b0
--- /dev/null
+++ b/feature/fortune/src/main/java/com/yapp/fortune/di/SchedulerModule.kt
@@ -0,0 +1,19 @@
+package com.yapp.fortune.di
+
+import com.yapp.alarm.scheduler.PostFortuneTaskScheduler
+import com.yapp.fortune.scheduler.WorkManagerPostFortuneTaskScheduler
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class SchedulerModule {
+ @Binds
+ @Singleton
+ abstract fun bindsPostFortuneTaskScheduler(
+ postFortuneTaskScheduler: WorkManagerPostFortuneTaskScheduler,
+ ): PostFortuneTaskScheduler
+}
diff --git a/feature/fortune/src/main/java/com/yapp/fortune/page/FortunePager.kt b/feature/fortune/src/main/java/com/yapp/fortune/page/FortunePager.kt
index 08b7cfc5..aa1dc78b 100644
--- a/feature/fortune/src/main/java/com/yapp/fortune/page/FortunePager.kt
+++ b/feature/fortune/src/main/java/com/yapp/fortune/page/FortunePager.kt
@@ -51,11 +51,13 @@ fun FortunePager(
val index = (page - 1).coerceIn(0, state.fortunePages.lastIndex)
FortunePageLayout(state.fortunePages[index])
}
+
5 -> FortuneCompletePage(
hasReward = state.hasReward,
onCompleteClick = onNextStep,
onNavigateToHome = onNavigateToHome,
)
+
else -> {}
}
}
diff --git a/feature/fortune/src/main/java/com/yapp/fortune/scheduler/WorkManagerPostFortuneTaskScheduler.kt b/feature/fortune/src/main/java/com/yapp/fortune/scheduler/WorkManagerPostFortuneTaskScheduler.kt
new file mode 100644
index 00000000..36e49c4f
--- /dev/null
+++ b/feature/fortune/src/main/java/com/yapp/fortune/scheduler/WorkManagerPostFortuneTaskScheduler.kt
@@ -0,0 +1,32 @@
+package com.yapp.fortune.scheduler
+
+import android.content.Context
+import androidx.work.BackoffPolicy
+import androidx.work.Constraints
+import androidx.work.ExistingWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import com.yapp.alarm.scheduler.PostFortuneTaskScheduler
+import com.yapp.fortune.worker.PostFortuneWorker
+import dagger.hilt.android.qualifiers.ApplicationContext
+import java.time.LocalDate
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+class WorkManagerPostFortuneTaskScheduler @Inject constructor(
+ @ApplicationContext private val context: Context,
+) : PostFortuneTaskScheduler {
+ override fun enqueueOnceForToday() {
+ val name = "post_fortune_${LocalDate.now()}"
+ val constraints = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+ val req = OneTimeWorkRequestBuilder()
+ .setConstraints(constraints)
+ .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, TimeUnit.SECONDS)
+ .build()
+ WorkManager.getInstance(context)
+ .enqueueUniqueWork(name, ExistingWorkPolicy.KEEP, req)
+ }
+}
diff --git a/feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt b/feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt
new file mode 100644
index 00000000..2dd14f72
--- /dev/null
+++ b/feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt
@@ -0,0 +1,63 @@
+package com.yapp.fortune.worker
+
+import android.content.Context
+import androidx.hilt.work.HiltWorker
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import com.yapp.domain.model.FortuneCreateStatus
+import com.yapp.domain.repository.FortuneRepository
+import com.yapp.domain.repository.UserInfoRepository
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.firstOrNull
+
+@HiltWorker
+class PostFortuneWorker @AssistedInject constructor(
+ @Assisted appContext: Context,
+ @Assisted params: WorkerParameters,
+ private val fortuneRepository: FortuneRepository,
+ private val userInfoRepository: UserInfoRepository,
+) : CoroutineWorker(appContext, params) {
+
+ override suspend fun doWork(): Result {
+ when (fortuneRepository.fortuneCreateStatusFlow.first()) {
+ is FortuneCreateStatus.Creating,
+ is FortuneCreateStatus.Success,
+ -> {
+ return Result.success()
+ }
+ FortuneCreateStatus.Failure,
+ FortuneCreateStatus.Idle,
+ -> {
+ val userId = userInfoRepository.userIdFlow.firstOrNull()
+ ?: run {
+ // 사용자 없으면 실패 상태 표시 후 실패 반환
+ fortuneRepository.markFortuneAsFailed()
+ return Result.failure()
+ }
+
+ return try {
+ fortuneRepository.markFortuneAsCreating()
+
+ val result = fortuneRepository.postFortune(userId)
+ result.fold(
+ onSuccess = { fortune ->
+ fortuneRepository.markFortuneAsCreated(fortune.id)
+ fortuneRepository.saveFortuneScore(fortune.avgFortuneScore)
+ Result.success()
+ },
+ onFailure = {
+ fortuneRepository.markFortuneAsFailed()
+ // WM 백오프 규칙에 따라 재시도
+ Result.retry()
+ },
+ )
+ } catch (_: Throwable) {
+ fortuneRepository.markFortuneAsFailed()
+ Result.retry()
+ }
+ }
+ }
+ }
+}
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 8c44e05a..10d74e52 100644
--- a/feature/home/src/main/java/com/yapp/home/HomeScreen.kt
+++ b/feature/home/src/main/java/com/yapp/home/HomeScreen.kt
@@ -1,5 +1,6 @@
package com.yapp.home
+import android.os.Build
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
@@ -74,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
@@ -129,8 +131,8 @@ fun HomeRoute(
}
HomeScreen(
- stateProvider = { state },
- eventDispatcher = viewModel::processAction,
+ state = state,
+ processAction = viewModel::processAction,
)
}
@@ -178,23 +180,21 @@ private suspend fun handleSideEffect(
@Composable
fun HomeScreen(
- stateProvider: () -> HomeContract.State,
- eventDispatcher: (HomeContract.Action) -> Unit,
+ state: HomeContract.State,
+ processAction: (HomeContract.Action) -> Unit,
) {
- val state = stateProvider()
-
if (state.initialLoading) {
HomeLoadingScreen()
} else if (state.alarms.isEmpty()) {
HomeAlarmEmptyScreen(
onSettingClick = {
- eventDispatcher(HomeContract.Action.NavigateToSetting)
+ processAction(HomeContract.Action.NavigateToSetting)
},
onMailClick = {
- eventDispatcher(HomeContract.Action.ShowDailyFortune)
+ processAction(HomeContract.Action.ShowDailyFortune)
},
onAddClick = {
- eventDispatcher(HomeContract.Action.NavigateToAlarmCreation)
+ processAction(HomeContract.Action.NavigateToAlarmCreation)
},
hasNewFortune = state.hasNewFortune,
isTooltipVisible = state.isToolTipVisible,
@@ -202,7 +202,7 @@ fun HomeScreen(
} else {
HomeContent(
state = state,
- eventDispatcher = eventDispatcher,
+ processAction = processAction,
)
}
@@ -213,10 +213,10 @@ fun HomeScreen(
confirmText = stringResource(id = R.string.alarm_delete_dialog_btn_delete),
cancelText = stringResource(id = R.string.alarm_delete_dialog_btn_cancel),
onConfirm = {
- eventDispatcher(HomeContract.Action.ConfirmDeletion)
+ processAction(HomeContract.Action.ConfirmDeletion)
},
onCancel = {
- eventDispatcher(HomeContract.Action.HideDeleteDialog)
+ processAction(HomeContract.Action.HideDeleteDialog)
},
)
}
@@ -228,10 +228,10 @@ fun HomeScreen(
confirmText = stringResource(id = R.string.no_active_alarm_dialog_btn_confirm),
cancelText = stringResource(id = R.string.no_active_alarm_dialog_btn_cancel),
onConfirm = {
- eventDispatcher(HomeContract.Action.HideNoActivatedAlarmDialog)
+ processAction(HomeContract.Action.HideNoActivatedAlarmDialog)
},
onCancel = {
- eventDispatcher(HomeContract.Action.RollbackPendingAlarmToggle)
+ processAction(HomeContract.Action.RollbackPendingAlarmToggle)
},
)
}
@@ -242,7 +242,18 @@ fun HomeScreen(
message = stringResource(id = R.string.no_daily_fortune_dialog_message),
confirmText = stringResource(id = R.string.no_daily_fortune_dialog_btn_confirm),
onConfirm = {
- eventDispatcher(HomeContract.Action.HideNoDailyFortuneDialog)
+ processAction(HomeContract.Action.HideNoDailyFortuneDialog)
+ },
+ )
+ }
+
+ if (state.isUpdateNoticeVisible) {
+ UpdateNoticeBottomSheet(
+ onDontShowAgain = {
+ processAction(HomeContract.Action.OnClickDontShowAgain)
+ },
+ onClose = {
+ processAction(HomeContract.Action.HideUpdateNotice)
},
)
}
@@ -279,7 +290,7 @@ private fun HomeLoadingScreen() {
@Composable
private fun HomeContent(
state: HomeContract.State,
- eventDispatcher: (HomeContract.Action) -> Unit,
+ processAction: (HomeContract.Action) -> Unit,
) {
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
@@ -291,7 +302,7 @@ private fun HomeContent(
LaunchedEffect(state.lastAddedAlarmIndex) {
state.lastAddedAlarmIndex?.let { index ->
listState.scrollToItem(index)
- eventDispatcher(HomeContract.Action.ResetLastAddedAlarmIndex)
+ processAction(HomeContract.Action.ResetLastAddedAlarmIndex)
}
}
@@ -301,7 +312,7 @@ private fun HomeContent(
interactionSource = remember { MutableInteractionSource() },
indication = null,
) {
- eventDispatcher(HomeContract.Action.HideToolTip)
+ processAction(HomeContract.Action.HideToolTip)
},
) {
if (state.activeItemMenu != null) {
@@ -312,7 +323,7 @@ private fun HomeContent(
interactionSource = remember { MutableInteractionSource() },
indication = null,
) {
- eventDispatcher(HomeContract.Action.HideItemMenu)
+ processAction(HomeContract.Action.HideItemMenu)
}
.zIndex(1f),
)
@@ -338,47 +349,50 @@ private fun HomeContent(
halfExpandedHeight = sheetHalfExpandHeight,
listState = listState,
onClickAlarm = { alarmId ->
- eventDispatcher(HomeContract.Action.EditAlarm(alarmId))
+ processAction(HomeContract.Action.EditAlarm(alarmId))
},
onLongPressAlarm = { alarmId, x, y ->
- eventDispatcher(HomeContract.Action.ShowItemMenu(alarmId, x, y))
+ processAction(HomeContract.Action.ShowItemMenu(alarmId, x, y))
},
onClickAdd = {
- eventDispatcher(HomeContract.Action.NavigateToAlarmCreation)
+ processAction(HomeContract.Action.NavigateToAlarmCreation)
},
onClickMore = {
if (state.dropdownMenuExpanded || state.sortDropDownMenuExpanded) {
- eventDispatcher(HomeContract.Action.HideDropDownMenu)
+ processAction(HomeContract.Action.HideDropDownMenu)
} else {
- eventDispatcher(HomeContract.Action.ShowDropDownMenu)
+ processAction(HomeContract.Action.ShowDropDownMenu)
}
},
onClickCheckAll = {
- eventDispatcher(HomeContract.Action.ToggleAllAlarmSelection)
+ processAction(HomeContract.Action.ToggleAllAlarmSelection)
},
onClickClose = {
- eventDispatcher(HomeContract.Action.ToggleMultiSelectionMode)
+ processAction(HomeContract.Action.ToggleMultiSelectionMode)
},
onClickEdit = {
- eventDispatcher(HomeContract.Action.ToggleMultiSelectionMode)
+ processAction(HomeContract.Action.ToggleMultiSelectionMode)
},
onClickSort = {
- eventDispatcher(HomeContract.Action.ShowSortDropDownMenu)
+ processAction(HomeContract.Action.ShowSortDropDownMenu)
},
onSetSortOrder = { sortOrder ->
- eventDispatcher(HomeContract.Action.SetSortOrder(sortOrder))
+ processAction(HomeContract.Action.SetSortOrder(sortOrder))
},
onDismissRequest = {
- eventDispatcher(HomeContract.Action.HideDropDownMenu)
+ processAction(HomeContract.Action.HideDropDownMenu)
},
onToggleSelect = { alarmId ->
- eventDispatcher(HomeContract.Action.ToggleAlarmSelection(alarmId))
+ processAction(HomeContract.Action.ToggleAlarmSelection(alarmId))
},
onToggleActive = { alarmId ->
- eventDispatcher(HomeContract.Action.ToggleAlarmActivation(alarmId))
+ processAction(HomeContract.Action.ToggleAlarmActivation(alarmId))
},
onSwipe = { alarmId ->
- eventDispatcher(HomeContract.Action.SwipeToDeleteAlarm(alarmId))
+ processAction(HomeContract.Action.SwipeToDeleteAlarm(alarmId))
+ },
+ onExpanded = {
+ processAction(HomeContract.Action.HideToolTip)
},
) {
Box(
@@ -397,7 +411,15 @@ private fun HomeContent(
.fillMaxWidth()
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
- sheetHalfExpandHeight = screenHeight - placeable.height.toDp() - statusBarHeight - navBarHeight
+ val contentHeight = placeable.height.toDp()
+
+ val offset = if (Build.VERSION.SDK_INT < 35) {
+ 0.dp
+ } else {
+ statusBarHeight + navBarHeight
+ }
+ sheetHalfExpandHeight = screenHeight - contentHeight - offset
+
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
@@ -420,8 +442,8 @@ private fun HomeContent(
}
HomeTopBar(
- onSettingClick = { eventDispatcher(HomeContract.Action.NavigateToSetting) },
- onMailClick = { eventDispatcher(HomeContract.Action.ShowDailyFortune) },
+ onSettingClick = { processAction(HomeContract.Action.NavigateToSetting) },
+ onMailClick = { processAction(HomeContract.Action.ShowDailyFortune) },
hasNewFortune = state.hasNewFortune,
isShowTooltip = state.isToolTipVisible,
)
@@ -438,7 +460,7 @@ private fun HomeContent(
.padding(bottom = 26.dp),
selectedAlarmCount = state.selectedAlarmIds.size,
onClick = {
- eventDispatcher(HomeContract.Action.ShowDeleteDialog)
+ processAction(HomeContract.Action.ShowDeleteDialog)
},
)
}
@@ -451,7 +473,7 @@ private fun HomeContent(
activeItemMenuPosition = state.activeItemMenuPosition,
selectedAlarmIds = state.selectedAlarmIds,
onDelete = { alarmId ->
- eventDispatcher(HomeContract.Action.DeleteSingleAlarm(alarmId))
+ processAction(HomeContract.Action.DeleteSingleAlarm(alarmId))
},
)
}
@@ -938,18 +960,16 @@ private fun AlarmWithMenu(
fun HomeScreenPreview() {
OrbitTheme {
HomeScreen(
- stateProvider = {
- HomeContract.State()
- .copy(
- initialLoading = false,
- alarms = listOf(
- Alarm(),
- ),
- activeItemMenu = 0L,
- activeItemMenuPosition = Pair(0f, 0f),
- )
- },
- eventDispatcher = {},
+ state = HomeContract.State()
+ .copy(
+ initialLoading = false,
+ alarms = listOf(
+ Alarm(),
+ ),
+ activeItemMenu = 0L,
+ activeItemMenuPosition = Pair(0f, 0f),
+ ),
+ processAction = {},
)
}
}
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 ae347cea..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,29 +341,29 @@ 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)
} else {
- fortuneRepository.markFortuneAsChecked()
+ fortuneRepository.markFortuneTooltipShown()
postSideEffect(HomeContract.SideEffect.NavigateToFortune)
}
}
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.hasNewFortuneFlow,
- ) { fortuneDate, fortuneScore, hasNewFortune ->
+ fortuneRepository.shouldShowFortuneToolTipFlow,
+ ) { fortuneDate, fortuneScore, shouldShowTooltip ->
val isTodayFortuneAvailable = fortuneDate == todayDate
val finalFortuneScore = if (isTodayFortuneAvailable) fortuneScore ?: -1 else -1
- Pair(finalFortuneScore, hasNewFortune)
+ Pair(finalFortuneScore, shouldShowTooltip)
}.collect { (finalFortuneScore, hasNewFortune) ->
reduce {
state.copy(
@@ -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/alarm/addedit/AlarmAddEditScreen.kt b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditScreen.kt
index ec6dcefe..9d726ae3 100644
--- a/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditScreen.kt
+++ b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditScreen.kt
@@ -61,6 +61,7 @@ import com.yapp.home.alarm.component.bottomsheet.AlarmMissionBottomSheet
import com.yapp.home.alarm.component.bottomsheet.AlarmSnoozeBottomSheet
import com.yapp.home.alarm.component.bottomsheet.AlarmSoundBottomSheet
import com.yapp.home.alarm.getLabelStringRes
+import com.yapp.ui.component.bottomsheet.OrbitBottomSheetLayout
import com.yapp.ui.component.bottomsheet.OrbitBottomSheetState
import com.yapp.ui.component.bottomsheet.rememberOrbitBottomSheetState
import com.yapp.ui.component.button.OrbitButton
@@ -282,50 +283,52 @@ fun AlarmAddEditContent(
}
}
- Column(
- modifier = Modifier.fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- AlarmAddEditTopBar(
- mode = state.mode,
- title = state.timeState.alarmMessage,
- onBack = { processAction(AlarmAddEditContract.Action.CheckUnsavedChangesBeforeExit) },
- onDelete = { processAction(AlarmAddEditContract.Action.ShowDeleteDialog) },
- )
- Box(
- modifier = Modifier.weight(1f),
- contentAlignment = Alignment.Center,
+ OrbitBottomSheetLayout(sheetState = bottomSheetState) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
) {
- OrbitPicker(
- initialTime = state.timeState.initialTime,
- ) { newTime ->
- processAction(AlarmAddEditContract.Action.SetAlarmTime(newTime))
+ AlarmAddEditTopBar(
+ mode = state.mode,
+ title = state.timeState.alarmMessage,
+ onBack = { processAction(AlarmAddEditContract.Action.CheckUnsavedChangesBeforeExit) },
+ onDelete = { processAction(AlarmAddEditContract.Action.ShowDeleteDialog) },
+ )
+ Box(
+ modifier = Modifier.weight(1f),
+ contentAlignment = Alignment.Center,
+ ) {
+ OrbitPicker(
+ initialTime = state.timeState.initialTime,
+ ) { newTime ->
+ processAction(AlarmAddEditContract.Action.SetAlarmTime(newTime))
+ }
}
+ AlarmAddEditSelectDaysSection(
+ modifier = Modifier.padding(horizontal = 20.dp),
+ daysSelectionState = state.daySelectionState,
+ holidayState = state.holidayState,
+ processAction = processAction,
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ AlarmAddEditSettingsSection(
+ modifier = Modifier.padding(horizontal = 20.dp),
+ state = state,
+ processAction = processAction,
+ )
+ Spacer(modifier = Modifier.height(24.dp))
+ OrbitButton(
+ label = stringResource(R.string.alarm_add_edit_save),
+ onClick = { processAction(AlarmAddEditContract.Action.SaveAlarm) },
+ enabled = true,
+ modifier = Modifier
+ .padding(
+ start = 20.dp,
+ end = 20.dp,
+ bottom = 12.dp,
+ ),
+ )
}
- AlarmAddEditSelectDaysSection(
- modifier = Modifier.padding(horizontal = 20.dp),
- daysSelectionState = state.daySelectionState,
- holidayState = state.holidayState,
- processAction = processAction,
- )
- Spacer(modifier = Modifier.height(12.dp))
- AlarmAddEditSettingsSection(
- modifier = Modifier.padding(horizontal = 20.dp),
- state = state,
- processAction = processAction,
- )
- Spacer(modifier = Modifier.height(24.dp))
- OrbitButton(
- label = stringResource(R.string.alarm_add_edit_save),
- onClick = { processAction(AlarmAddEditContract.Action.SaveAlarm) },
- enabled = true,
- modifier = Modifier
- .padding(
- start = 20.dp,
- end = 20.dp,
- bottom = 12.dp,
- ),
- )
}
if (state.isDeleteDialogVisible) {
diff --git a/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmMissionBottomSheet.kt b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmMissionBottomSheet.kt
index 0c84f840..99656263 100644
--- a/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmMissionBottomSheet.kt
+++ b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmMissionBottomSheet.kt
@@ -21,10 +21,13 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -57,6 +60,12 @@ private fun MissionType.displayData(): Pair = when (this) {
else -> throw IllegalStateException("Invalid mission type")
}
+val StepStackSaver: Saver>, out Any> =
+ listSaver(
+ save = { state -> state.value.map { it.name } },
+ restore = { restored -> mutableStateOf(restored.map { AlarmMissionSelectBottomSheetType.valueOf(it) }) },
+ )
+
@Composable
internal fun AlarmMissionBottomSheet(
missionState: AlarmAddEditContract.AlarmMissionState,
@@ -64,9 +73,15 @@ internal fun AlarmMissionBottomSheet(
onSaveMission: (MissionType, Int) -> Unit,
onPreviewMission: (MissionType, Int) -> Unit,
) {
- var stepStack by remember { mutableStateOf(listOf(AlarmMissionSelectBottomSheetType.MISSION_SETTING)) }
- var selectedMissionType by remember { mutableStateOf(missionState.missionType) }
- var selectedMissionCount by remember { mutableIntStateOf(missionState.missionCount) }
+ val initialMissionType = missionState.missionType
+ val initialMissionCount = missionState.missionCount
+
+ var stepStack by rememberSaveable(saver = StepStackSaver) {
+ mutableStateOf(listOf(AlarmMissionSelectBottomSheetType.MISSION_SETTING))
+ }
+
+ var currentSelectedMissionType by rememberSaveable { mutableStateOf(initialMissionType) }
+ var currentSelectedMissionCount by rememberSaveable { mutableIntStateOf(initialMissionCount) }
fun push(step: AlarmMissionSelectBottomSheetType) {
stepStack = stepStack + step
@@ -82,22 +97,22 @@ internal fun AlarmMissionBottomSheet(
when (currentStep) {
AlarmMissionSelectBottomSheetType.MISSION_SETTING -> {
- if (selectedMissionType == MissionType.NONE) {
+ if (currentSelectedMissionType == MissionType.NONE) {
MissionAddContent {
push(AlarmMissionSelectBottomSheetType.MISSION_SELECT)
}
} else {
MissionSettingContent(
- missionType = selectedMissionType,
- missionCount = selectedMissionCount,
+ missionType = currentSelectedMissionType,
+ missionCount = currentSelectedMissionCount,
onDetail = { push(AlarmMissionSelectBottomSheetType.MISSION_DETAIL) },
onDelete = {
- selectedMissionType = MissionType.NONE
- onSaveMission(selectedMissionType, selectedMissionCount)
+ currentSelectedMissionType = MissionType.NONE
+ onSaveMission(currentSelectedMissionType, currentSelectedMissionCount)
},
onChange = { push(AlarmMissionSelectBottomSheetType.MISSION_SELECT) },
onDone = {
- onSaveMission(selectedMissionType, selectedMissionCount)
+ onSaveMission(currentSelectedMissionType, currentSelectedMissionCount)
onDismiss()
},
)
@@ -107,11 +122,10 @@ internal fun AlarmMissionBottomSheet(
AlarmMissionSelectBottomSheetType.MISSION_SELECT -> {
MissionSelectContent(
onBack = { pop() },
- onClose = {
- onDismiss()
- },
- onSelect = { mission ->
- selectedMissionType = mission
+ onClose = { onDismiss() },
+ initialMission = currentSelectedMissionType,
+ onSelect = { selected ->
+ currentSelectedMissionType = selected
push(AlarmMissionSelectBottomSheetType.MISSION_DETAIL)
},
)
@@ -119,19 +133,17 @@ internal fun AlarmMissionBottomSheet(
AlarmMissionSelectBottomSheetType.MISSION_DETAIL -> {
MissionDetailContent(
- missionType = selectedMissionType,
- selectedMissionCount = selectedMissionCount,
- onCountChange = { selectedMissionCount = it },
+ missionType = currentSelectedMissionType,
+ selectedMissionCount = currentSelectedMissionCount,
+ onCountChange = { currentSelectedMissionCount = it },
onBack = { pop() },
- onClose = {
- onDismiss()
- },
+ onClose = { onDismiss() },
onSave = {
- onSaveMission(selectedMissionType, selectedMissionCount)
+ onSaveMission(currentSelectedMissionType, currentSelectedMissionCount)
onDismiss()
},
onPreview = {
- onPreviewMission(selectedMissionType, selectedMissionCount)
+ onPreviewMission(currentSelectedMissionType, currentSelectedMissionCount)
},
)
}
@@ -379,6 +391,7 @@ private fun MissionCountChip(
private fun MissionSelectContent(
onBack: () -> Unit,
onClose: () -> Unit,
+ initialMission: MissionType,
onSelect: (MissionType) -> Unit,
) {
Column(
@@ -390,7 +403,7 @@ private fun MissionSelectContent(
Spacer(modifier = Modifier.height(14.dp))
MissionSelectTopAppBar(
- title = stringResource(id = feature.home.R.string.mission_bottom_sheet_title),
+ title = stringResource(id = feature.home.R.string.mission_select_content_title),
onBack = onBack,
onClose = onClose,
)
@@ -400,12 +413,14 @@ private fun MissionSelectContent(
) {
MissionTypeItem(
missionType = MissionType.SHAKE,
+ selected = initialMission == MissionType.SHAKE,
onClick = {
onSelect(MissionType.SHAKE)
},
)
MissionTypeItem(
missionType = MissionType.TAP,
+ selected = initialMission == MissionType.TAP,
onClick = {
onSelect(MissionType.TAP)
},
@@ -417,6 +432,7 @@ private fun MissionSelectContent(
@Composable
private fun MissionTypeItem(
missionType: MissionType,
+ selected: Boolean,
onClick: () -> Unit,
) {
val (iconRes, titleRes) = missionType.displayData()
@@ -425,6 +441,7 @@ private fun MissionTypeItem(
Row(
modifier = Modifier
.fillMaxWidth()
+ .clip(RoundedCornerShape(12.dp))
.clickable(
onClick = onClick,
)
@@ -447,6 +464,28 @@ private fun MissionTypeItem(
style = OrbitTheme.typography.headline2SemiBold,
color = OrbitTheme.colors.white,
)
+
+ if (selected) {
+ Spacer(modifier = Modifier.weight(1f))
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(2.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ modifier = Modifier.size(16.dp),
+ painter = painterResource(id = R.drawable.ic_check),
+ tint = OrbitTheme.colors.white.copy(alpha = 0.5f),
+ contentDescription = null,
+ )
+
+ Text(
+ text = stringResource(id = feature.home.R.string.mission_select_content_selected),
+ style = OrbitTheme.typography.body2Medium,
+ color = OrbitTheme.colors.white.copy(alpha = 0.4f),
+ )
+ }
+ }
}
}
diff --git a/feature/home/src/main/java/com/yapp/home/component/bottomsheet/AlarmListBottomSheet.kt b/feature/home/src/main/java/com/yapp/home/component/bottomsheet/AlarmListBottomSheet.kt
index 9aaec0b3..e8ee3c58 100644
--- a/feature/home/src/main/java/com/yapp/home/component/bottomsheet/AlarmListBottomSheet.kt
+++ b/feature/home/src/main/java/com/yapp/home/component/bottomsheet/AlarmListBottomSheet.kt
@@ -36,6 +36,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -56,6 +57,8 @@ import com.yapp.home.component.AlarmListDropDownMenu
import com.yapp.home.component.AlarmSortDropDownMenu
import com.yapp.ui.component.checkbox.OrbitCheckBox
import feature.home.R
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
enum class BottomSheetExpandState {
@@ -87,23 +90,29 @@ internal fun AlarmListBottomSheet(
onToggleSelect: (Long) -> Unit,
onToggleActive: (Long) -> Unit,
onSwipe: (Long) -> Unit,
+ onExpanded: () -> Unit,
content: @Composable () -> Unit,
) {
var expandedType by remember { mutableStateOf(BottomSheetExpandState.HALF_EXPANDED) }
- val sheetState = rememberStandardBottomSheetState(
- confirmValueChange = {
- expandedType = when (it) {
- SheetValue.Expanded -> BottomSheetExpandState.EXPANDED
- else -> BottomSheetExpandState.HALF_EXPANDED
- }
- true
- },
- )
-
+ val sheetState = rememberStandardBottomSheetState()
val scaffoldState = rememberBottomSheetScaffoldState(bottomSheetState = sheetState)
val coroutineScope = rememberCoroutineScope()
+ LaunchedEffect(Unit) {
+ snapshotFlow { sheetState.currentValue }
+ .distinctUntilChanged()
+ .collectLatest { value ->
+ expandedType = when (value) {
+ SheetValue.Expanded -> {
+ onExpanded()
+ BottomSheetExpandState.EXPANDED
+ }
+ SheetValue.PartiallyExpanded, SheetValue.Hidden -> BottomSheetExpandState.HALF_EXPANDED
+ }
+ }
+ }
+
val nestedScrollConnection = remember {
object : androidx.compose.ui.input.nestedscroll.NestedScrollConnection {
override fun onPreScroll(
@@ -118,13 +127,6 @@ internal fun AlarmListBottomSheet(
}
}
- LaunchedEffect(sheetState.currentValue) {
- expandedType = when (sheetState.currentValue) {
- SheetValue.Expanded -> BottomSheetExpandState.EXPANDED
- else -> BottomSheetExpandState.HALF_EXPANDED
- }
- }
-
BottomSheetScaffold(
scaffoldState = scaffoldState,
sheetContent = {
@@ -324,23 +326,19 @@ private fun AlarmListTopBar(
onClick = onClickMore,
)
- if (menuExpanded) {
- AlarmListDropDownMenu(
- expanded = menuExpanded,
- onDismissRequest = onDismissRequest,
- onClickEdit = onClickEdit,
- onClickSort = onClickSort,
- )
- }
+ AlarmListDropDownMenu(
+ expanded = menuExpanded,
+ onDismissRequest = onDismissRequest,
+ onClickEdit = onClickEdit,
+ onClickSort = onClickSort,
+ )
- if (sortDropDownMenuExpanded) {
- AlarmSortDropDownMenu(
- expanded = sortDropDownMenuExpanded,
- sortOrder = sortOrder,
- onDismissRequest = onDismissRequest,
- onSetSortOrder = onSetSortOrder,
- )
- }
+ AlarmSortDropDownMenu(
+ expanded = sortDropDownMenuExpanded,
+ sortOrder = sortOrder,
+ onDismissRequest = onDismissRequest,
+ onSetSortOrder = onSetSortOrder,
+ )
}
}
}
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..fd38d5c2
--- /dev/null
+++ b/feature/home/src/main/java/com/yapp/home/component/bottomsheet/UpdateNoticeBottomSheet.kt
@@ -0,0 +1,150 @@
+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.interaction.MutableInteractionSource
+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))
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() },
+ ) { },
+ ) {
+ 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 cf91a124..b2266b16 100644
--- a/feature/home/src/main/res/values/strings.xml
+++ b/feature/home/src/main/res/values/strings.xml
@@ -48,6 +48,9 @@
미션 변경
완료
+ 미션 선택
+ 선택됨
+
%d회
횟수
@@ -121,4 +124,7 @@
%1$d시간 %2$d분 후에 울려요
%d분 후에 울려요
곧 울려요
+
+ 다시 보지 않기
+ 닫기
diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt b/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt
index 9aa81001..8e5c61d4 100644
--- a/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt
+++ b/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt
@@ -27,7 +27,6 @@ sealed class MissionContract {
data object ClickCard : Action()
data object ShowExitDialog : Action()
data object HideExitDialog : Action()
- data object RetryPostFortune : Action()
}
sealed class SideEffect : com.yapp.ui.base.SideEffect {
diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionNavGraph.kt b/feature/mission/src/main/java/com/yapp/mission/MissionNavGraph.kt
index 247a9e5c..5befebf4 100644
--- a/feature/mission/src/main/java/com/yapp/mission/MissionNavGraph.kt
+++ b/feature/mission/src/main/java/com/yapp/mission/MissionNavGraph.kt
@@ -40,7 +40,7 @@ private fun handleSideEffect(
MissionContract.SideEffect.NavigateToFortune -> {
navigator.navigateToFortune(
navOptions = navOptions {
- popUpTo(MissionRoute.route) {
+ popUpTo {
inclusive = true
}
},
@@ -50,7 +50,7 @@ private fun handleSideEffect(
MissionContract.SideEffect.NavigateToHome -> {
navigator.navigateToHome(
navOptions = navOptions {
- popUpTo(MissionRoute.route) {
+ popUpTo {
inclusive = true
}
},
diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt b/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt
index ad178833..65249b8c 100644
--- a/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt
+++ b/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt
@@ -143,12 +143,6 @@ fun MissionScreen(
MissionSuccessOverlay()
}
- state.errorMessage?.let {
- ErrorDialog(message = it) {
- eventDispatcher(MissionContract.Action.RetryPostFortune)
- }
- }
-
if (state.missionMode == MissionMode.PREVIEW) {
val insets = WindowInsets.navigationBars.asPaddingValues()
@@ -405,16 +399,6 @@ fun MissionSuccessOverlay() {
}
}
-@Composable
-fun ErrorDialog(message: String, onConfirm: () -> Unit) {
- OrbitDialog(
- title = stringResource(id = R.string.error),
- message = message,
- confirmText = stringResource(id = R.string.confirm),
- onConfirm = onConfirm,
- )
-}
-
@Composable
fun MissionLoadingScreen() {
Box(
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 9d8808e4..b5e1c45d 100644
--- a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt
+++ b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt
@@ -7,16 +7,14 @@ import com.yapp.alarm.pendingIntent.interaction.createAlarmDismissIntent
import com.yapp.analytics.AnalyticsEvent
import com.yapp.analytics.AnalyticsHelper
import com.yapp.domain.MissionMode
+import com.yapp.domain.model.FortuneCreateStatus
import com.yapp.domain.model.MissionType
import com.yapp.domain.repository.FortuneRepository
-import com.yapp.domain.repository.UserInfoRepository
import com.yapp.media.haptic.HapticFeedbackManager
import com.yapp.media.haptic.HapticType
import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.withContext
+import kotlinx.coroutines.flow.first
import org.orbitmvi.orbit.Container
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.syntax.simple.intent
@@ -30,7 +28,6 @@ class MissionViewModel @Inject constructor(
private val analyticsHelper: AnalyticsHelper,
private val hapticFeedbackManager: HapticFeedbackManager,
private val fortuneRepository: FortuneRepository,
- private val userInfoRepository: UserInfoRepository,
private val app: Application,
private val savedStateHandle: SavedStateHandle,
) : ViewModel(), ContainerHost {
@@ -53,7 +50,6 @@ class MissionViewModel @Inject constructor(
is MissionContract.Action.ClickCard -> handleMissionProgress(MissionType.TAP)
is MissionContract.Action.ShowExitDialog -> showExitDialog()
is MissionContract.Action.HideExitDialog -> hideExitDialog()
- is MissionContract.Action.RetryPostFortune -> retryPostFortune()
}
}
@@ -137,36 +133,27 @@ class MissionViewModel @Inject constructor(
private fun completeMission(type: String) = intent {
performHapticSuccess()
logMissionSuccess(type)
- if (state.missionMode == MissionMode.REAL) {
- postFortune()
- } else {
+
+ if (state.missionMode != MissionMode.REAL) {
postSideEffect(MissionContract.SideEffect.NavigateBack)
+ return@intent
}
- }
- private fun postFortune(isRetry: Boolean = false) = intent {
- val userId = userInfoRepository.userIdFlow.firstOrNull() ?: return@intent
+ val fortuneCreateStatus = fortuneRepository.fortuneCreateStatusFlow.first()
+ val hasUnseenFortune = fortuneRepository.hasUnseenFortuneFlow.first()
- val result = withContext(Dispatchers.IO) {
- fortuneRepository.postFortune(userId)
- }
-
- result.onSuccess { data ->
- fortuneRepository.saveFortuneId(data.id)
- fortuneRepository.saveFortuneScore(data.avgFortuneScore)
+ val shouldOpenFortune = (
+ fortuneCreateStatus is FortuneCreateStatus.Creating ||
+ fortuneCreateStatus is FortuneCreateStatus.Success && hasUnseenFortune
+ )
- postSideEffect(MissionContract.SideEffect.NavigateToFortune)
- }.onFailure { error ->
- if (isRetry) {
- navigateToHome()
+ postSideEffect(
+ if (shouldOpenFortune) {
+ MissionContract.SideEffect.NavigateToFortune
} else {
- reduce { state.copy(errorMessage = error.message) }
- }
- }
- }
-
- fun retryPostFortune() {
- postFortune(isRetry = true)
+ MissionContract.SideEffect.NavigateBack
+ },
+ )
}
private fun logMissionSuccess(type: String) {
@@ -183,8 +170,4 @@ class MissionViewModel @Inject constructor(
private fun performHapticSuccess() {
hapticFeedbackManager.performHapticFeedback(HapticType.SUCCESS)
}
-
- private fun navigateToHome() = intent {
- postSideEffect(MissionContract.SideEffect.NavigateToHome)
- }
}
diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingAccessScreen.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingAccessScreen.kt
index abc83d5c..1485ed59 100644
--- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingAccessScreen.kt
+++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingAccessScreen.kt
@@ -21,9 +21,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -48,7 +46,6 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus
-import com.google.accompanist.permissions.shouldShowRationale
import com.yapp.analytics.AnalyticsEvent
import com.yapp.analytics.LocalAnalyticsHelper
import com.yapp.designsystem.theme.OrbitTheme
@@ -238,8 +235,6 @@ fun OnboardingAccessScreen(
modifier = Modifier
.fillMaxSize()
.background(OrbitTheme.colors.gray_900)
- .statusBarsPadding()
- .navigationBarsPadding()
.imePadding(),
) {
if (!hasRequestedPermission) {
diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingBirthdayScreen.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingBirthdayScreen.kt
index 39868a89..ee9180dc 100644
--- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingBirthdayScreen.kt
+++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingBirthdayScreen.kt
@@ -10,9 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -104,8 +102,6 @@ fun OnboardingBirthdayScreen(
modifier = Modifier
.fillMaxSize()
.background(OrbitTheme.colors.gray_900)
- .statusBarsPadding()
- .navigationBarsPadding()
.imePadding(),
) {
OnBoardingTopAppBar(
diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingCompleteScreen2.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingCompleteScreen2.kt
index f99e8771..f0e752e1 100644
--- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingCompleteScreen2.kt
+++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingCompleteScreen2.kt
@@ -9,10 +9,8 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -57,8 +55,6 @@ fun OnboardingCompleteScreen2(
modifier = Modifier
.fillMaxSize()
.background(OrbitTheme.colors.gray_900)
- .statusBarsPadding()
- .navigationBarsPadding()
.imePadding(),
) {
OnBoardingTopAppBar(
diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingGenderScreen.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingGenderScreen.kt
index 06a2ec94..01f0df00 100644
--- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingGenderScreen.kt
+++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingGenderScreen.kt
@@ -29,6 +29,7 @@ import com.yapp.common.navigation.OrbitNavigator
import com.yapp.common.navigation.route.OnboardingBaseRoute
import com.yapp.designsystem.theme.OrbitTheme
import com.yapp.onboarding.component.UserInfoBottomSheet
+import com.yapp.ui.component.bottomsheet.OrbitBottomSheetLayout
import com.yapp.ui.component.bottomsheet.OrbitBottomSheetState
import com.yapp.ui.component.bottomsheet.rememberOrbitBottomSheetState
import com.yapp.ui.component.dialog.OrbitDialog
@@ -120,7 +121,7 @@ private suspend fun handleSideEffect(
OnboardingContract.SideEffect.OnboardingCompleted -> {
navigator.navigateToHome(
navOptions = navOptions {
- popUpTo(OnboardingBaseRoute) {
+ popUpTo {
inclusive = true
}
},
@@ -153,55 +154,57 @@ fun OnboardingGenderScreen(
}
}
- OnboardingScreen(
- currentStep = currentStep,
- totalSteps = totalSteps,
- isButtonEnabled = state.selectedGender != null,
- onNextClick = {
- processAction(OnboardingContract.Action.ShowBottomSheet)
- },
- onBackClick = {
- processAction(OnboardingContract.Action.PreviousStep)
- },
- buttonLabel = "다음",
- ) {
- Column(
- modifier = Modifier.fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
+ OrbitBottomSheetLayout(sheetState = bottomSheetState) {
+ OnboardingScreen(
+ currentStep = currentStep,
+ totalSteps = totalSteps,
+ isButtonEnabled = state.selectedGender != null,
+ onNextClick = {
+ processAction(OnboardingContract.Action.ShowBottomSheet)
+ },
+ onBackClick = {
+ processAction(OnboardingContract.Action.PreviousStep)
+ },
+ buttonLabel = "다음",
) {
- Spacer(modifier = Modifier.heightForScreenPercentage(0.05f))
- Text(
- text = stringResource(id = R.string.onboarding_step6_text_title),
- style = OrbitTheme.typography.heading1SemiBold,
- color = OrbitTheme.colors.white,
- modifier = Modifier.fillMaxWidth(),
- textAlign = TextAlign.Center,
- )
-
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 38.dp)
- .paddingForScreenPercentage(topPercentage = 0.11f),
- horizontalArrangement = Arrangement.spacedBy(15.dp),
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
) {
- listOf("남성", "여성").forEach { gender ->
- Box(modifier = Modifier.weight(1f)) {
- OrbitGenderToggle(
- label = gender,
- isSelected = state.selectedGender == gender,
- onToggle = {
- logEvent(
- AnalyticsEvent(
- type = "onboarding_gender_select",
- properties = mapOf(
- AnalyticsEvent.OnboardingPropertiesKeys.GENDER to gender,
+ Spacer(modifier = Modifier.heightForScreenPercentage(0.05f))
+ Text(
+ text = stringResource(id = R.string.onboarding_step6_text_title),
+ style = OrbitTheme.typography.heading1SemiBold,
+ color = OrbitTheme.colors.white,
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ )
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 38.dp)
+ .paddingForScreenPercentage(topPercentage = 0.11f),
+ horizontalArrangement = Arrangement.spacedBy(15.dp),
+ ) {
+ listOf("남성", "여성").forEach { gender ->
+ Box(modifier = Modifier.weight(1f)) {
+ OrbitGenderToggle(
+ label = gender,
+ isSelected = state.selectedGender == gender,
+ onToggle = {
+ logEvent(
+ AnalyticsEvent(
+ type = "onboarding_gender_select",
+ properties = mapOf(
+ AnalyticsEvent.OnboardingPropertiesKeys.GENDER to gender,
+ ),
),
- ),
- )
- processAction(OnboardingContract.Action.UpdateGender(gender))
- },
- )
+ )
+ processAction(OnboardingContract.Action.UpdateGender(gender))
+ },
+ )
+ }
}
}
}
diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingNavGraph.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingNavGraph.kt
index ff7417d7..a642d861 100644
--- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingNavGraph.kt
+++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingNavGraph.kt
@@ -123,7 +123,7 @@ private fun handleOnboardingCommonSideEffect(
OnboardingContract.SideEffect.OnboardingCompleted -> {
navigator.navigateToHome(
navOptions = navOptions {
- popUpTo(OnboardingBaseRoute) {
+ popUpTo {
inclusive = true
}
},
diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingViewModel.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingViewModel.kt
index 24f3ba73..13fc35eb 100644
--- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingViewModel.kt
+++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingViewModel.kt
@@ -147,9 +147,7 @@ class OnboardingViewModel @Inject constructor(
alarmUseCase.insertAlarm(
alarm = newAlarm,
- ).onSuccess {
- postSideEffect(OnboardingContract.SideEffect.OnboardingCompleted)
- }.onFailure {
+ ).onFailure {
Log.e("OnboardingViewModel", "Failed to create alarm", it)
}
}.onFailure {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c10bb87c..2d1248b6 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -29,6 +29,7 @@ androidx-app-compat = "1.7.0"
androidx-core = "1.15.0"
androidx-datastore = "1.1.1"
androidx-room = "2.7.2"
+androidx-work = "2.10.3"
androidx-lifecycle = "2.8.7"
@@ -44,11 +45,12 @@ activity-compose = "1.9.3"
## Hilt
hilt = "2.51.1"
hilt-navigation-compose = "1.2.0"
+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"
@@ -102,6 +104,9 @@ androidx-room-compiler = { group = "androidx.room", name = "room-compiler", vers
androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "androidx-room" }
androidx-room-paging = { group = "androidx.room", name = "room-paging", version.ref = "androidx-room" }
androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" }
+androidx-hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hilt-work" }
+androidx-work-runtime = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidx-work" }
+androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidx-work" }
## Compose Libraries
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
@@ -129,6 +134,7 @@ hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref
hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt-navigation-compose" }
+hilt-worker = { group = "androidx.hilt", name = "hilt-work", version.ref = "hilt-work" }
# Orbit
orbit-core = { group = "org.orbit-mvi", name = "orbit-core", version.ref = "orbit" }
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