Skip to content

Commit

Permalink
Merge pull request #153 from TeamHY2/Feature/#152-promotion-popup
Browse files Browse the repository at this point in the history
[Feature] PromtionDialog를 구현합니다.
  • Loading branch information
librarywon authored Dec 8, 2024
2 parents 8dd99d8 + 8c635cd commit 41530f1
Show file tree
Hide file tree
Showing 18 changed files with 476 additions and 0 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ dependencies {
implementation(platform(libs.firebase.bom))
implementation(libs.google.firebase.crashlytics)
implementation(libs.firebase.analytics)
implementation(libs.firebase.config)

implementation(libs.accompanist.permissions)
}
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@ kotlinxSerializationJson = "1.5.1"
# Credentials
credentials = "1.3.0-alpha01"
googleid = "1.1.1"
firebaseConfigKtx = "22.0.1"

[libraries]
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
firebase-config = { module = "com.google.firebase:firebase-config" }
google-firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
Expand Down Expand Up @@ -116,6 +118,7 @@ androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation
# firebase
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics", version.ref = "firebaseCrashlytics" }
firebase-firestore-ktx = { group = "com.google.firebase", name = "firebase-firestore-ktx", version.ref = "firebaseFirestoreKtx" }
firebase-config-ktx = { group = "com.google.firebase", name = "firebase-config-ktx", version.ref = "firebaseConfigKtx" }

# datastore
datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
Expand Down
4 changes: 4 additions & 0 deletions main-data/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
id("hongikyeolgong2.android.library")
alias(libs.plugins.kotlin.serialization)
}

android {
Expand All @@ -11,4 +12,7 @@ dependencies {
implementation(projects.core.remote)

implementation(libs.firebase.firestore.ktx)
implementation(libs.firebase.config.ktx)
implementation(libs.datastore.preferences)
implementation(libs.kotlinx.serialization.json)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.teamhy2.main.data.datasource

import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.ktx.remoteConfig
import com.teamhy2.main.data.dto.PromotionDto
import com.teamhy2.main.data.mapper.toDomain
import com.teamhy2.main.domain.datasource.PromotionDataSource
import com.teamhy2.main.domain.model.Promotion
import com.teamhy2.main.domain.util.DateUtil
import kotlinx.coroutines.tasks.await
import kotlinx.serialization.json.Json
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import javax.inject.Inject

class RemoteConfigPromotionDataSource
@Inject
constructor() : PromotionDataSource {
private val remoteConfig: FirebaseRemoteConfig = Firebase.remoteConfig

init {
remoteConfig.setDefaultsAsync(DEFAULTS)
}

override suspend fun fetchPromotionData(): Result<Promotion> {
return runCatching {
remoteConfig.fetchAndActivate().await()
val promotionDataJson: String = remoteConfig.getString(PROMOTION_POPUP_KEY)
val promotionDto: PromotionDto = Json.decodeFromString(promotionDataJson)
promotionDto.copy(
isActive =
calculatePromotionActive(
promotionDto.startDate,
promotionDto.endDate,
),
).toDomain()
}
}

private fun calculatePromotionActive(
startDate: String,
endDate: String,
): Boolean {
return runCatching {
val formatter: DateTimeFormatter = DateTimeFormatter.ISO_DATE
val startDate: LocalDate = LocalDate.parse(startDate, formatter)
val endDate: LocalDate = LocalDate.parse(endDate, formatter)

DateUtil.isTodayWithinDateRange(startDate = startDate, endDate = endDate)
}.getOrDefault(false)
}

companion object {
const val PROMOTION_POPUP_KEY = "promotionPopup"
const val DEFAULT_PROMOTION_JSON =
"""{"imageUrl":"","detailUrl":"","startDate":"","endDate":""}"""

val DEFAULTS =
mapOf(
PROMOTION_POPUP_KEY to DEFAULT_PROMOTION_JSON,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.teamhy2.main.data.datastore

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.MutablePreferences
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.teamhy2.main.domain.util.DateUtil
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import javax.inject.Inject

class PromotionDataStore
@Inject
constructor(
private val dataStore: DataStore<Preferences>,
) {
private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_DATE

val isPromotionDismissed: Flow<Boolean> =
dataStore.data.map { preferences ->
val startDateString: String? = preferences[START_DATE]
val endDateString: String? = preferences[END_DATE]
requireNotNull(startDateString) { ERROR_START_DATE_MISSING }
requireNotNull(endDateString) { ERROR_END_DATE_MISSING }

val startDate: LocalDate = LocalDate.parse(startDateString, formatter)
val endDate: LocalDate = LocalDate.parse(endDateString, formatter)

DateUtil.isTodayWithinDateRange(startDate = startDate, endDate = endDate)
}

suspend fun savePromotionDismissPeriod(
startDate: LocalDate,
endDate: LocalDate,
) {
dataStore.edit { preferences: MutablePreferences ->
preferences[START_DATE] = startDate.format(formatter)
preferences[END_DATE] = endDate.format(formatter)
}
}

companion object Keys {
private const val PROMOTION_START_DATE_KEY = "promotion_start_date"
private const val PROMOTION_END_DATE_KEY = "promotion_end_date"

const val ERROR_START_DATE_MISSING = "DataStore에 시작 날짜가 없습니다."
const val ERROR_END_DATE_MISSING = "DataStore에 종료 날짜가 없습니다."

val START_DATE: Preferences.Key<String> =
stringPreferencesKey(PROMOTION_START_DATE_KEY)
val END_DATE: Preferences.Key<String> =
stringPreferencesKey(PROMOTION_END_DATE_KEY)
}
}
10 changes: 10 additions & 0 deletions main-data/src/main/java/com/teamhy2/main/data/di/MainModule.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.teamhy2.main.data.di

import com.teamhy2.main.data.datasource.RemoteConfigPromotionDataSource
import com.teamhy2.main.data.repository.DefaultPromotionRepository
import com.teamhy2.main.data.repository.RemoteStudyDayRepository
import com.teamhy2.main.data.repository.RemoteWiseSayingRepository
import com.teamhy2.main.domain.datasource.PromotionDataSource
import com.teamhy2.main.domain.repository.PromotionRepository
import com.teamhy2.main.domain.repository.StudyDayRepository
import com.teamhy2.main.domain.repository.WiseSayingRepository
import dagger.Binds
Expand All @@ -20,4 +24,10 @@ abstract class MainModule {
@Binds
@Singleton
abstract fun bindStudyDayRepository(remoteStudyDayRepository: RemoteStudyDayRepository): StudyDayRepository

@Binds
abstract fun bindPromotionDataSource(remoteConfigPromotionDataSource: RemoteConfigPromotionDataSource): PromotionDataSource

@Binds
abstract fun bindPromotionRepository(defaultPromotionRepository: DefaultPromotionRepository): PromotionRepository
}
12 changes: 12 additions & 0 deletions main-data/src/main/java/com/teamhy2/main/data/dto/PromotionDto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.teamhy2.main.data.dto

import kotlinx.serialization.Serializable

@Serializable
data class PromotionDto(
val imageUrl: String,
val detailUrl: String,
val startDate: String,
val endDate: String,
val isActive: Boolean = false,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.teamhy2.main.data.mapper

import com.teamhy2.main.data.dto.PromotionDto
import com.teamhy2.main.domain.model.Promotion

fun PromotionDto.toDomain(): Promotion {
return Promotion(
imageUrl = this.imageUrl,
detailUrl = this.detailUrl,
isActive = this.isActive,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.teamhy2.main.data.repository

import com.teamhy2.main.data.datastore.PromotionDataStore
import com.teamhy2.main.domain.datasource.PromotionDataSource
import com.teamhy2.main.domain.model.Promotion
import com.teamhy2.main.domain.repository.PromotionRepository
import kotlinx.coroutines.flow.Flow
import java.time.LocalDate
import javax.inject.Inject

class DefaultPromotionRepository
@Inject
constructor(
private val dataSource: PromotionDataSource,
private val dataStore: PromotionDataStore,
) : PromotionRepository {
override val isPromotionDismissed: Flow<Boolean>
get() = dataStore.isPromotionDismissed

override suspend fun fetchPromotionData(): Result<Promotion> {
return dataSource.fetchPromotionData()
}

override suspend fun savePromotionDismissPeriod(
startDate: LocalDate,
endDate: LocalDate,
): Result<Unit> =
runCatching {
dataStore.savePromotionDismissPeriod(startDate, endDate)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.teamhy2.main.domain.datasource

import com.teamhy2.main.domain.model.Promotion

interface PromotionDataSource {
suspend fun fetchPromotionData(): Result<Promotion>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.teamhy2.main.domain.model

data class Promotion(
val imageUrl: String,
val detailUrl: String,
val isActive: Boolean,
) {
companion object {
val DEFAULT =
Promotion(
imageUrl = "",
detailUrl = "",
isActive = false,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.teamhy2.main.domain.repository

import com.teamhy2.main.domain.model.Promotion
import kotlinx.coroutines.flow.Flow
import java.time.LocalDate

interface PromotionRepository {
val isPromotionDismissed: Flow<Boolean>

suspend fun fetchPromotionData(): Result<Promotion>

suspend fun savePromotionDismissPeriod(
startDate: LocalDate,
endDate: LocalDate,
): Result<Unit>
}
21 changes: 21 additions & 0 deletions main-domain/src/main/java/com/teamhy2/main/domain/util/DateUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.teamhy2.main.domain.util

import java.time.LocalDate

object DateUtil {
/**
* Checks if the current date is within the specified date range.
*
* @param startDate The start date of the range.
* @param endDate The end date of the range.
* @return True if the current date is within the range, false otherwise.
*/
fun isTodayWithinDateRange(
startDate: LocalDate,
endDate: LocalDate,
): Boolean {
val today: LocalDate = LocalDate.now()
return today.isEqual(startDate) || today.isEqual(endDate) ||
(today.isAfter(startDate) && today.isBefore(endDate))
}
}
2 changes: 2 additions & 0 deletions main-presentation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,6 @@ dependencies {
implementation(libs.accompanist.permissions)
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.navigation)

implementation(libs.coil)
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import com.teamhy2.designsystem.ui.theme.HY2Theme
import com.teamhy2.designsystem.util.compositionlocal.LocalShowSnackBar
import com.teamhy2.designsystem.util.compositionlocal.LocalTracker
import com.teamhy2.feature.home.component.InitTimerComponent
import com.teamhy2.feature.home.component.PromotionDialog
import com.teamhy2.feature.home.component.RunningTimerComponent
import com.teamhy2.feature.home.component.WeeklyStudyCalendar
import com.teamhy2.feature.home.model.HomeUiState
Expand All @@ -44,6 +45,7 @@ import com.teamhy2.hongikyeolgong2.timer.presentation.model.TimerUiState
import com.teamhy2.main.domain.model.WeeklyStudyDay
import com.teamhy2.main.domain.model.WiseSaying
import kotlinx.coroutines.flow.collectLatest
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit

Expand Down Expand Up @@ -177,6 +179,27 @@ fun HomeRoute(
)
}

if (uiState.isPromotionDialog) {
PromotionDialog(
promotionImageUrl = uiState.promotion.imageUrl,
onDetailClick = {
homeViewModel.updatePromotionDialogVisibility(false)
val intent =
Intent(Intent.ACTION_VIEW, Uri.parse(uiState.promotion.detailUrl))
context.startActivity(intent)
},
onCloseClick = { homeViewModel.updatePromotionDialogVisibility(false) },
onCloseTodayClick = {
homeViewModel.updatePromotionDismissPeriod(
startDate = LocalDate.now(),
endDate = LocalDate.now(),
)
homeViewModel.updatePromotionDialogVisibility(false)
},
onDismiss = { homeViewModel.updatePromotionDialogVisibility(false) },
)
}

HomeScreen(
weeklyStudyDays = uiState.weeklyStudyDays,
wiseSaying = uiState.wiseSaying,
Expand Down
Loading

0 comments on commit 41530f1

Please sign in to comment.