diff --git a/.github/workflows/android-pull-request-ci.yml b/.github/workflows/android-pull-request-ci.yml
index 3d02ab19..a1f20b31 100644
--- a/.github/workflows/android-pull-request-ci.yml
+++ b/.github/workflows/android-pull-request-ci.yml
@@ -26,10 +26,12 @@ jobs:
env:
BASE_URL: ${{ secrets.BASE_URL }}
KAKAO_NATIVE_KEY: ${{ secrets.KAKAO_NATIVE_KEY }}
+ AMPLITUDE_KEY: ${{ secrets.AMPLITUDE_KEY }}
run: |
echo base.url=\"$BASE_URL\" >> ./local.properties
echo kakao.native.key=\"$KAKAO_NATIVE_KEY\" >> ./local.properties
echo kakaoNativeKey=$KAKAO_NATIVE_KEY >> ./local.properties
+ echo amplitude.key=\"$AMPLITUDE_KEY\" >> ./local.properties
- name: Create Google Services JSON File
env:
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 9fe5c94b..f0d4aa53 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -58,4 +58,5 @@ dependencies {
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.espresso.core)
+ implementation(libs.amplitude.analytics)
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 12762fed..af711630 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
+
+
+ @SetValue(KEY_POPUP_VISIBILITY)
+ suspend fun setPopupVisibility(boolean: Boolean)
+
+ @GetValue(KEY_MARKET_UPDATE)
+ fun flowMarketUpdate(): Flow
+
+ @SetValue(KEY_MARKET_UPDATE)
+ suspend fun setMarketUpdate(boolean: Boolean)
+
@ClearValues
suspend fun clearAll()
+
companion object {
const val KEY_ACCESSTOKEN = "key-accesstoken"
const val KEY_REFRESHTOKEN = "key-refreshtoken"
const val KEY_DEVICETOKEN = "key-devicetoken"
const val KEY_AUTOLOGIN = "key-autologin"
const val KEY_FCM_ALLOWED = "key-fcm-allowed"
+ const val KEY_POPUP_VISIBILITY = "key-popup-dialog"
+ const val KEY_MARKET_UPDATE = "key-market-update"
}
}
diff --git a/core/designsystem/src/main/res/drawable/shape_white_fill_2_rect.xml b/core/designsystem/src/main/res/drawable/shape_white_fill_2_rect.xml
new file mode 100644
index 00000000..ea574595
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/shape_white_fill_2_rect.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml
index a22fe03d..c4f23723 100644
--- a/core/designsystem/src/main/res/values/strings.xml
+++ b/core/designsystem/src/main/res/values/strings.xml
@@ -155,6 +155,11 @@
님의 클립
이주의 링크
이주의 추천 사이트
+ 1분 설문조사 참여하고\n스타벅스 기프티콘 받기
+ 토스터 사용 피드백을 남겨주시면\n추첨을 통해 기프티콘을 드려요!
+ 참여하기
+ 일주일간 보지 않기
+
클립의 이름은 최대 15자까지 입력 가능해요
diff --git a/core/model/src/main/java/org/sopt/model/home/PopupInfo.kt b/core/model/src/main/java/org/sopt/model/home/PopupInfo.kt
new file mode 100644
index 00000000..41dc9e5a
--- /dev/null
+++ b/core/model/src/main/java/org/sopt/model/home/PopupInfo.kt
@@ -0,0 +1,9 @@
+package org.sopt.model.home
+
+data class PopupInfo(
+ val popupId: Int,
+ val popupImage: String,
+ val popupActiveStartDate: String,
+ val popupActiveEndDate: String,
+ val popupLinkUrl: String,
+)
diff --git a/core/model/src/main/java/org/sopt/model/home/PopupInvisible.kt b/core/model/src/main/java/org/sopt/model/home/PopupInvisible.kt
new file mode 100644
index 00000000..66c43e34
--- /dev/null
+++ b/core/model/src/main/java/org/sopt/model/home/PopupInvisible.kt
@@ -0,0 +1,6 @@
+package org.sopt.model.home
+
+data class PopupInvisible(
+ val popupId: Int,
+ val popupHideUntil: String,
+)
diff --git a/data-remote/home/src/main/java/org/sopt/remote/home/api/PopupService.kt b/data-remote/home/src/main/java/org/sopt/remote/home/api/PopupService.kt
new file mode 100644
index 00000000..a3f23894
--- /dev/null
+++ b/data-remote/home/src/main/java/org/sopt/remote/home/api/PopupService.kt
@@ -0,0 +1,25 @@
+package org.sopt.remote.home.api
+
+import org.sopt.network.model.response.base.BaseResponse
+import org.sopt.remote.home.request.RequestPopupInvisibleDto
+import org.sopt.remote.home.response.ResponsePopupInfoDto
+import org.sopt.remote.home.response.ResponsePopupInvisibleDto
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.PATCH
+
+interface PopupService {
+ companion object {
+ const val API = "api"
+ const val V2 = "v2"
+ const val POPUP = "popup"
+ }
+
+ @PATCH("/$API/$V2/$POPUP")
+ suspend fun patchPopupInvisible(
+ @Body requestPopupInvisibleDto: RequestPopupInvisibleDto,
+ ): BaseResponse
+
+ @GET("/$API/$V2/$POPUP")
+ suspend fun getPopupInfo(): BaseResponse
+}
diff --git a/data-remote/home/src/main/java/org/sopt/remote/home/datasource/RemotePopupDatasourceImpl.kt b/data-remote/home/src/main/java/org/sopt/remote/home/datasource/RemotePopupDatasourceImpl.kt
new file mode 100644
index 00000000..d6536f35
--- /dev/null
+++ b/data-remote/home/src/main/java/org/sopt/remote/home/datasource/RemotePopupDatasourceImpl.kt
@@ -0,0 +1,19 @@
+package org.sopt.remote.home.datasource
+
+import org.sopt.home.datasource.RemotePopupDataSource
+import org.sopt.model.home.PopupInfo
+import org.sopt.model.home.PopupInvisible
+import org.sopt.remote.home.api.PopupService
+import org.sopt.remote.home.request.RequestPopupInvisibleDto
+import org.sopt.remote.home.response.toCoreModel
+import javax.inject.Inject
+
+class RemotePopupDatasourceImpl @Inject constructor(
+ private val popupService: PopupService,
+) : RemotePopupDataSource {
+ override suspend fun patchPopupInvisible(popupId: Long, hideDate: Long): PopupInvisible =
+ popupService.patchPopupInvisible(RequestPopupInvisibleDto(popupId, hideDate)).data!!.toCoreModel()
+
+ override suspend fun getPopupInfo(): List =
+ popupService.getPopupInfo().data!!.popupList.map { it.toCoreModel() }
+}
diff --git a/data-remote/home/src/main/java/org/sopt/remote/home/di/HomeDataSourceModule.kt b/data-remote/home/src/main/java/org/sopt/remote/home/di/HomeDataSourceModule.kt
index 741e3551..2ea85376 100644
--- a/data-remote/home/src/main/java/org/sopt/remote/home/di/HomeDataSourceModule.kt
+++ b/data-remote/home/src/main/java/org/sopt/remote/home/di/HomeDataSourceModule.kt
@@ -5,7 +5,9 @@ import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.sopt.home.datasource.RemoteHomeDataSource
+import org.sopt.home.datasource.RemotePopupDataSource
import org.sopt.remote.home.datasource.RemoteHomeDataSourceImpl
+import org.sopt.remote.home.datasource.RemotePopupDatasourceImpl
import javax.inject.Singleton
@Module
@@ -16,4 +18,10 @@ abstract class HomeDataSourceModule {
abstract fun bindRemoteHomeDatasource(
remoteHomeDataSourceImpl: RemoteHomeDataSourceImpl,
): RemoteHomeDataSource
+
+ @Singleton
+ @Binds
+ abstract fun bindRemotePopupDataSource(
+ remotePopupDataSourceImpl: RemotePopupDatasourceImpl,
+ ): RemotePopupDataSource
}
diff --git a/data-remote/home/src/main/java/org/sopt/remote/home/di/HomeServiceModule.kt b/data-remote/home/src/main/java/org/sopt/remote/home/di/HomeServiceModule.kt
index b750cfaa..e7f289c7 100644
--- a/data-remote/home/src/main/java/org/sopt/remote/home/di/HomeServiceModule.kt
+++ b/data-remote/home/src/main/java/org/sopt/remote/home/di/HomeServiceModule.kt
@@ -6,8 +6,10 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.sopt.network.di.AuthLinkMindRetrofit
import org.sopt.remote.home.api.HomeService
+import org.sopt.remote.home.api.PopupService
import retrofit2.Retrofit
import javax.inject.Singleton
+
@Module
@InstallIn(SingletonComponent::class)
object HomeServiceModule {
@@ -16,4 +18,9 @@ object HomeServiceModule {
@Provides
fun provideHomeService(@AuthLinkMindRetrofit retrofit: Retrofit): HomeService =
retrofit.create(HomeService::class.java)
+
+ @Singleton
+ @Provides
+ fun providePopupService(@AuthLinkMindRetrofit retrofit: Retrofit): PopupService =
+ retrofit.create(PopupService::class.java)
}
diff --git a/data-remote/home/src/main/java/org/sopt/remote/home/request/RequestPopupInvisibleDto.kt b/data-remote/home/src/main/java/org/sopt/remote/home/request/RequestPopupInvisibleDto.kt
new file mode 100644
index 00000000..a7e19640
--- /dev/null
+++ b/data-remote/home/src/main/java/org/sopt/remote/home/request/RequestPopupInvisibleDto.kt
@@ -0,0 +1,12 @@
+package org.sopt.remote.home.request
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class RequestPopupInvisibleDto(
+ @SerialName("popupId")
+ val popupId: Long,
+ @SerialName("hideDate")
+ val hideDate: Long,
+)
diff --git a/data-remote/home/src/main/java/org/sopt/remote/home/response/ResponsePopupInfoDto.kt b/data-remote/home/src/main/java/org/sopt/remote/home/response/ResponsePopupInfoDto.kt
new file mode 100644
index 00000000..441edb2b
--- /dev/null
+++ b/data-remote/home/src/main/java/org/sopt/remote/home/response/ResponsePopupInfoDto.kt
@@ -0,0 +1,37 @@
+package org.sopt.remote.home.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import org.sopt.model.home.PopupInfo
+
+@Serializable
+data class ResponsePopupInfoDto(
+ @SerialName("popupList")
+ val popupList: List,
+)
+
+@Serializable
+data class ResponsePopupInfo(
+ @SerialName("id")
+ val id: Int,
+ @SerialName("image")
+ val image: String,
+ @SerialName("activeStartDate")
+ val activeStartDate: String,
+ @SerialName("activeEndDate")
+ val activeEndDate: String,
+ @SerialName("linkUrl")
+ val linkUrl: String,
+)
+
+internal fun ResponsePopupInfoDto.toCoreModel() = popupList.map {
+ it.toCoreModel()
+}
+
+internal fun ResponsePopupInfo.toCoreModel() = PopupInfo(
+ popupId = id,
+ popupImage = image,
+ popupActiveStartDate = activeStartDate,
+ popupActiveEndDate = activeEndDate,
+ popupLinkUrl = linkUrl,
+)
diff --git a/data-remote/home/src/main/java/org/sopt/remote/home/response/ResponsePopupInvisibleDto.kt b/data-remote/home/src/main/java/org/sopt/remote/home/response/ResponsePopupInvisibleDto.kt
new file mode 100644
index 00000000..a475497f
--- /dev/null
+++ b/data-remote/home/src/main/java/org/sopt/remote/home/response/ResponsePopupInvisibleDto.kt
@@ -0,0 +1,18 @@
+package org.sopt.remote.home.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import org.sopt.model.home.PopupInvisible
+
+@Serializable
+data class ResponsePopupInvisibleDto(
+ @SerialName("popupId")
+ val popupId: Int,
+ @SerialName("hideUntil")
+ val hideUntil: String,
+)
+
+internal fun ResponsePopupInvisibleDto.toCoreModel() = PopupInvisible(
+ popupId = popupId,
+ popupHideUntil = hideUntil,
+)
diff --git a/data/home/src/main/java/org/sopt/home/datasource/RemotePopupDataSource.kt b/data/home/src/main/java/org/sopt/home/datasource/RemotePopupDataSource.kt
new file mode 100644
index 00000000..a6f76163
--- /dev/null
+++ b/data/home/src/main/java/org/sopt/home/datasource/RemotePopupDataSource.kt
@@ -0,0 +1,9 @@
+package org.sopt.home.datasource
+
+import org.sopt.model.home.PopupInfo
+import org.sopt.model.home.PopupInvisible
+
+interface RemotePopupDataSource {
+ suspend fun patchPopupInvisible(popupId: Long, hideDate: Long): PopupInvisible
+ suspend fun getPopupInfo(): List
+}
diff --git a/data/home/src/main/java/org/sopt/home/di/RepositoryModule.kt b/data/home/src/main/java/org/sopt/home/di/RepositoryModule.kt
index 397f2b75..9137c2a7 100644
--- a/data/home/src/main/java/org/sopt/home/di/RepositoryModule.kt
+++ b/data/home/src/main/java/org/sopt/home/di/RepositoryModule.kt
@@ -6,6 +6,8 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.sopt.home.repository.HomeRepoImpl
import org.sopt.home.repository.HomeRepository
+import org.sopt.home.repository.PopupRepoImpl
+import org.sopt.home.repository.PopupRepository
import javax.inject.Singleton
@Module
@@ -16,4 +18,10 @@ abstract class RepositoryModule {
abstract fun bindHomeRepository(
homeRepoImpl: HomeRepoImpl,
): HomeRepository
+
+ @Singleton
+ @Binds
+ abstract fun bindPopupRepository(
+ popupRepoImpl: PopupRepoImpl,
+ ): PopupRepository
}
diff --git a/data/home/src/main/java/org/sopt/home/repository/PopupRepoImpl.kt b/data/home/src/main/java/org/sopt/home/repository/PopupRepoImpl.kt
new file mode 100644
index 00000000..ef1388af
--- /dev/null
+++ b/data/home/src/main/java/org/sopt/home/repository/PopupRepoImpl.kt
@@ -0,0 +1,16 @@
+package org.sopt.home.repository
+
+import org.sopt.home.datasource.RemotePopupDataSource
+import org.sopt.model.home.PopupInfo
+import org.sopt.model.home.PopupInvisible
+import javax.inject.Inject
+
+class PopupRepoImpl @Inject constructor(
+ private val remotePopupDataSource: RemotePopupDataSource,
+) : PopupRepository {
+ override suspend fun patchPopupInvisible(popupId: Long, hideDate: Long): Result =
+ runCatching { remotePopupDataSource.patchPopupInvisible(popupId, hideDate) }
+
+ override suspend fun getPopupInfo(): Result> =
+ runCatching { remotePopupDataSource.getPopupInfo() }
+}
diff --git a/domain/home/src/main/java/org/sopt/home/repository/PopupRepository.kt b/domain/home/src/main/java/org/sopt/home/repository/PopupRepository.kt
new file mode 100644
index 00000000..9b921c84
--- /dev/null
+++ b/domain/home/src/main/java/org/sopt/home/repository/PopupRepository.kt
@@ -0,0 +1,9 @@
+package org.sopt.home.repository
+
+import org.sopt.model.home.PopupInfo
+import org.sopt.model.home.PopupInvisible
+
+interface PopupRepository {
+ suspend fun patchPopupInvisible(popupId: Long, hideDate: Long): Result
+ suspend fun getPopupInfo(): Result>
+}
diff --git a/domain/home/src/main/java/org/sopt/home/usecase/GetPopupInfo.kt b/domain/home/src/main/java/org/sopt/home/usecase/GetPopupInfo.kt
new file mode 100644
index 00000000..163db982
--- /dev/null
+++ b/domain/home/src/main/java/org/sopt/home/usecase/GetPopupInfo.kt
@@ -0,0 +1,11 @@
+package org.sopt.home.usecase
+
+import org.sopt.home.repository.PopupRepository
+import org.sopt.model.home.PopupInfo
+import javax.inject.Inject
+
+class GetPopupInfo @Inject constructor(
+ private val popupRepository: PopupRepository,
+) {
+ suspend operator fun invoke(): Result> = popupRepository.getPopupInfo()
+}
diff --git a/domain/home/src/main/java/org/sopt/home/usecase/PatchPopupInvisible.kt b/domain/home/src/main/java/org/sopt/home/usecase/PatchPopupInvisible.kt
new file mode 100644
index 00000000..65b069ec
--- /dev/null
+++ b/domain/home/src/main/java/org/sopt/home/usecase/PatchPopupInvisible.kt
@@ -0,0 +1,12 @@
+package org.sopt.home.usecase
+
+import org.sopt.home.repository.PopupRepository
+import org.sopt.model.home.PopupInvisible
+import javax.inject.Inject
+
+class PatchPopupInvisible @Inject constructor(
+ private val popupRepository: PopupRepository,
+) {
+ suspend operator fun invoke(popupId: Long, hideDate: Long): Result =
+ popupRepository.patchPopupInvisible(popupId, hideDate)
+}
diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts
index da8ce5d0..17332ef8 100644
--- a/feature/home/build.gradle.kts
+++ b/feature/home/build.gradle.kts
@@ -18,4 +18,6 @@ dependencies {
implementation(libs.jsoup)
implementation(libs.orbit.core)
implementation(libs.orbit.viewmodel)
+ implementation(libs.google.play.core)
+ implementation(projects.core.datastore)
}
diff --git a/feature/home/src/main/java/org/sopt/home/HomeContract.kt b/feature/home/src/main/java/org/sopt/home/HomeContract.kt
index 797a17cf..493dd45a 100644
--- a/feature/home/src/main/java/org/sopt/home/HomeContract.kt
+++ b/feature/home/src/main/java/org/sopt/home/HomeContract.kt
@@ -1,6 +1,8 @@
package org.sopt.home
+import org.sopt.home.model.UpdatePriority
import org.sopt.model.category.Category
+import org.sopt.model.home.PopupInfo
import org.sopt.model.home.RecommendLink
import org.sopt.model.home.WeekBestLink
@@ -14,6 +16,8 @@ data class HomeState(
val url: String = "",
val categoryId: Long? = 0,
val categoryName: String? = "전체 클립",
+ val popupList: List = emptyList(),
+ val marketUpdate: UpdatePriority = UpdatePriority.EMPTY,
) {
fun calculateProgress(): Int {
if (readToastNum > allToastNum) return 0
@@ -31,4 +35,6 @@ sealed interface HomeSideEffect {
data object NavigateClipLink : HomeSideEffect
data object NavigateWebView : HomeSideEffect
data object ShowBottomSheet : HomeSideEffect
+ data object ShowPopupInfo : HomeSideEffect
+ data object ShowUpdateDialog : HomeSideEffect
}
diff --git a/feature/home/src/main/java/org/sopt/home/HomeFragment.kt b/feature/home/src/main/java/org/sopt/home/HomeFragment.kt
index 70926045..25fed57e 100644
--- a/feature/home/src/main/java/org/sopt/home/HomeFragment.kt
+++ b/feature/home/src/main/java/org/sopt/home/HomeFragment.kt
@@ -14,6 +14,8 @@ import org.sopt.home.adapter.HomeWeekLinkAdapter
import org.sopt.home.adapter.HomeWeekRecommendLinkAdapter
import org.sopt.home.adapter.ItemDecoration
import org.sopt.home.databinding.FragmentHomeBinding
+import org.sopt.home.model.UpdatePriority
+import org.sopt.model.home.PopupInfo
import org.sopt.ui.base.BindingFragment
import org.sopt.ui.nav.DeepLinkUtil
import org.sopt.ui.view.onThrottleClick
@@ -27,6 +29,7 @@ class HomeFragment : BindingFragment({ FragmentHomeBinding.
private lateinit var homeWeekLinkAdapter: HomeWeekLinkAdapter
private lateinit var homeWeekRecommendLinkAdapter: HomeWeekRecommendLinkAdapter
private val viewModel by viewModels()
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
@@ -62,6 +65,7 @@ class HomeFragment : BindingFragment({ FragmentHomeBinding.
is HomeSideEffect.NavigateClipLink -> navigateToDestination(
"featureSaveLink://ClipLinkFragment/${viewModel.container.stateFlow.value.categoryId}/${viewModel.container.stateFlow.value.categoryName}",
)
+
is HomeSideEffect.ShowBottomSheet -> showHomeBottomSheet()
is HomeSideEffect.NavigateWebView -> {
val encodedURL = URLEncoder.encode(viewModel.container.stateFlow.value.url, StandardCharsets.UTF_8.toString())
@@ -69,6 +73,9 @@ class HomeFragment : BindingFragment({ FragmentHomeBinding.
"featureSaveLink://webViewFragment/${0}/${false}/${false}/$encodedURL",
)
}
+
+ is HomeSideEffect.ShowPopupInfo -> showPopupInfo(viewModel.container.stateFlow.value.popupList)
+ is HomeSideEffect.ShowUpdateDialog -> showUpdateDialog(viewModel.container.stateFlow.value.marketUpdate)
}
}
@@ -80,8 +87,11 @@ class HomeFragment : BindingFragment({ FragmentHomeBinding.
getMainPageUserClip()
getRecommendSite()
getWeekBestLink()
+ getPopupListInfo()
+ checkMarketUpdateState()
}
}
+
private fun navigateToSetting() {
binding.ivHomeSetting.onThrottleClick {
viewModel.navigateSetting()
@@ -154,4 +164,29 @@ class HomeFragment : BindingFragment({ FragmentHomeBinding.
}
}
}
+
+ private fun showPopupInfo(popupList: List) {
+ popupList.forEach {
+ if (viewModel.checkPopupDate(it.popupActiveStartDate, it.popupActiveEndDate)
+ ) {
+ val surveyDialog = SurveyDialogFragment.newInstance(
+ it.popupImage,
+ { viewModel.navigateWebview(it.popupLinkUrl) },
+ { viewModel.patchPopupInvisible(it.popupId.toLong(), 7) },
+ { viewModel.setPopupVisible() },
+ )
+ surveyDialog.show(parentFragmentManager, this.tag)
+ }
+ }
+ }
+
+ private fun showUpdateDialog(marketUpdate: UpdatePriority) {
+ if (marketUpdate != UpdatePriority.EMPTY) {
+ val marketUpdateDialog = MarketUpdateDialogFragment.newInstance(
+ marketUpdate,
+ { viewModel.setMarketUpdateVisible() },
+ )
+ marketUpdateDialog.show(parentFragmentManager, this.tag)
+ }
+ }
}
diff --git a/feature/home/src/main/java/org/sopt/home/HomeViewModel.kt b/feature/home/src/main/java/org/sopt/home/HomeViewModel.kt
index 9326aa7f..57035cfb 100644
--- a/feature/home/src/main/java/org/sopt/home/HomeViewModel.kt
+++ b/feature/home/src/main/java/org/sopt/home/HomeViewModel.kt
@@ -1,9 +1,14 @@
package org.sopt.home
+import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.google.android.play.core.appupdate.AppUpdateManagerFactory
+import com.google.android.play.core.install.model.UpdateAvailability
import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.orbitmvi.orbit.Container
import org.orbitmvi.orbit.ContainerHost
@@ -13,19 +18,30 @@ 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 org.sopt.datastore.datastore.SecurityDataStore
import org.sopt.domain.category.category.usecase.PostAddCategoryTitleUseCase
+import org.sopt.home.model.UpdatePriority
import org.sopt.home.usecase.GetMainPageUserClip
+import org.sopt.home.usecase.GetPopupInfo
import org.sopt.home.usecase.GetRecommendSite
import org.sopt.home.usecase.GetWeekBestLink
+import org.sopt.home.usecase.PatchPopupInvisible
import org.sopt.model.category.Category
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(
+ @ApplicationContext private val context: Context,
private val getMainPageUserClip: GetMainPageUserClip,
private val getRecommendSite: GetRecommendSite,
private val getWeekBestLink: GetWeekBestLink,
private val postAddCategoryTitle: PostAddCategoryTitleUseCase,
+ private val patchPopupInvisible: PatchPopupInvisible,
+ private val getPopupInfo: GetPopupInfo,
+ private val dataStore: SecurityDataStore,
) : ContainerHost, ViewModel() {
override val container: Container =
container(HomeState())
@@ -86,6 +102,53 @@ class HomeViewModel @Inject constructor(
}
}
+ fun getPopupListInfo() = intent {
+ if (dataStore.flowPopupVisibility().first()) {
+ getPopupInfo.invoke().onSuccess {
+ postSideEffect(HomeSideEffect.ShowPopupInfo)
+ reduce {
+ state.copy(popupList = it)
+ }
+ }.onFailure {
+ Log.d("getPopupListInfo", "$it")
+ }
+ }
+ }
+
+ fun patchPopupInvisible(popupId: Long, hideDate: Long) {
+ viewModelScope.launch {
+ patchPopupInvisible.invoke(popupId, hideDate)
+ .onSuccess {
+ Log.d("patchPopupInvisible", "$it")
+ }
+ .onFailure {
+ Log.d("patchPopupInvisible", "$it")
+ }
+ }
+ }
+
+ fun checkMarketUpdateState() {
+ viewModelScope.launch {
+ if (dataStore.flowMarketUpdate().first()) {
+ val appUpdateManager = AppUpdateManagerFactory.create(context)
+ val appUpdateTask = appUpdateManager.appUpdateInfo
+
+ appUpdateTask.addOnSuccessListener { appUpdateInfo ->
+ if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
+ intent {
+ postSideEffect(HomeSideEffect.ShowUpdateDialog)
+ reduce {
+ state.copy(marketUpdate = UpdatePriority.toUpdatePriority(appUpdateInfo.updatePriority()))
+ }
+ }
+ }
+ }.addOnFailureListener { appUpdateInfo ->
+ Log.d("appUpdateInfo", appUpdateInfo.message.toString())
+ }
+ }
+ }
+ }
+
fun navigateSearch() = intent { postSideEffect(HomeSideEffect.NavigateSearch) }
fun navigateSetting() = intent { postSideEffect(HomeSideEffect.NavigateSetting) }
fun showBottomSheet() = intent { postSideEffect(HomeSideEffect.ShowBottomSheet) }
@@ -106,4 +169,24 @@ class HomeViewModel @Inject constructor(
reduce { state.copy(url = url) }
postSideEffect(HomeSideEffect.NavigateWebView)
}
+
+ fun setPopupVisible() {
+ viewModelScope.launch {
+ dataStore.setPopupVisibility(false)
+ }
+ }
+
+ fun setMarketUpdateVisible() {
+ viewModelScope.launch {
+ dataStore.setMarketUpdate(false)
+ }
+ }
+
+ fun checkPopupDate(activeStartDate: String, activeEndDate: String): Boolean {
+ val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
+ val today = Calendar.getInstance().time
+ val startDate = dateFormat.parse(activeStartDate)
+ val endDate = dateFormat.parse(activeEndDate)
+ return today.after(startDate) && today.before(endDate) || today == startDate || today == endDate
+ }
}
diff --git a/feature/home/src/main/java/org/sopt/home/MarketUpdateDialogFragment.kt b/feature/home/src/main/java/org/sopt/home/MarketUpdateDialogFragment.kt
new file mode 100644
index 00000000..2bb79c39
--- /dev/null
+++ b/feature/home/src/main/java/org/sopt/home/MarketUpdateDialogFragment.kt
@@ -0,0 +1,107 @@
+package org.sopt.home
+
+import android.content.Intent
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import androidx.core.view.isInvisible
+import org.sopt.home.databinding.FragmentMarketUpdateDialogBinding
+import org.sopt.home.model.UpdatePriority
+import org.sopt.ui.base.BindingDialogFragment
+import org.sopt.ui.view.onThrottleClick
+import kotlin.system.exitProcess
+
+class MarketUpdateDialogFragment : BindingDialogFragment(
+ { FragmentMarketUpdateDialogBinding.inflate(it) },
+) {
+ private var marketUpdatePriority: UpdatePriority? = null
+ private var marketUpdateVisible: () -> Unit = {}
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setStyle(STYLE_NO_FRAME, android.R.style.Theme_Dialog)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ marketUpdateVisible.invoke()
+ checkUpdatePriority(marketUpdatePriority)
+
+ binding.btnMarketUpdateDialogSkip.onThrottleClick {
+ dismiss()
+ }
+ binding.btnMarketUpdateDialogUpdate.onThrottleClick {
+ dismiss()
+ navigateMarket()
+ exitProcess(0)
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ val dialog = dialog
+ if (dialog != null) {
+ dialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ dialog.setCanceledOnTouchOutside(false)
+ }
+ }
+
+ private fun checkUpdatePriority(marketUpdatePriority: UpdatePriority?) {
+ when (marketUpdatePriority) {
+ UpdatePriority.EMPTY -> {
+ binding.tvMarketUpdateDialogTitle.text = "업데이트 알림"
+ binding.tvMarketUpdateDialogSubtitle.text = "토스터의 사용성이 개선되었어요!\n지금 바로 업데이트해보세요"
+ binding.btnMarketUpdateDialogUpdate.text = "지금 업데이트"
+ binding.btnMarketUpdateDialogSkip.isInvisible = false
+ }
+
+ UpdatePriority.MINOR -> {
+ binding.tvMarketUpdateDialogTitle.text = "업데이트 알림"
+ binding.tvMarketUpdateDialogSubtitle.text = "토스터의 사용성이 개선되었어요!\n지금 바로 업데이트해보세요"
+ binding.btnMarketUpdateDialogUpdate.text = "지금 업데이트"
+ binding.btnMarketUpdateDialogSkip.isInvisible = false
+ }
+
+ UpdatePriority.MAJOR -> {
+ binding.tvMarketUpdateDialogTitle.text = "기능 업데이트 알림"
+ binding.tvMarketUpdateDialogSubtitle.text = "토스터의 기능이 추가되었어요!\n지금 바로 업데이트해보세요"
+ binding.btnMarketUpdateDialogUpdate.text = "지금 업데이트"
+ binding.btnMarketUpdateDialogSkip.isInvisible = false
+ }
+
+ UpdatePriority.CRITICAL -> {
+ binding.tvMarketUpdateDialogTitle.text = "신규 기능 업데이트 알림"
+ binding.tvMarketUpdateDialogSubtitle.text = "토스터의 새로운 기능을 이용하기 위해서는\n업데이트가 필요해요!\n최신 버전으로 업데이트 하시겠어요?"
+ binding.btnMarketUpdateDialogUpdate.text = "업데이트 하기"
+ binding.btnMarketUpdateDialogSkip.isInvisible = true
+ }
+
+ null -> {}
+ }
+ }
+
+ private fun navigateMarket() {
+ val appPackageName = "org.sopt.linkmind"
+ runCatching {
+ context?.packageManager?.getPackageInfo("com.android.vending", 0)
+ }.onSuccess {
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$appPackageName")))
+ }.onFailure {
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$appPackageName")))
+ }
+ }
+
+ companion object {
+ fun newInstance(
+ updatePriority: UpdatePriority,
+ setMarketUpdateVisible: () -> Unit,
+ ): MarketUpdateDialogFragment {
+ return MarketUpdateDialogFragment().apply {
+ marketUpdatePriority = updatePriority
+ marketUpdateVisible = setMarketUpdateVisible
+ }
+ }
+ }
+}
diff --git a/feature/home/src/main/java/org/sopt/home/SurveyDialogFragment.kt b/feature/home/src/main/java/org/sopt/home/SurveyDialogFragment.kt
new file mode 100644
index 00000000..c770e8e5
--- /dev/null
+++ b/feature/home/src/main/java/org/sopt/home/SurveyDialogFragment.kt
@@ -0,0 +1,77 @@
+package org.sopt.home
+
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.os.Bundle
+import android.view.View
+import coil.load
+import dagger.hilt.android.AndroidEntryPoint
+import org.sopt.home.databinding.FragmentSurveyDialogBinding
+import org.sopt.ui.base.BindingDialogFragment
+import org.sopt.ui.view.onThrottleClick
+
+@AndroidEntryPoint
+class SurveyDialogFragment : BindingDialogFragment(
+ { FragmentSurveyDialogBinding.inflate(it) },
+) {
+ private var imageUrl: String? = null
+ private var linkUrl: () -> Unit = {}
+ private var handleSkip: () -> Unit = {}
+ private var popupVisibility: () -> Unit = {}
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ imageUrl = arguments?.getString("imageUrl")
+ setStyle(STYLE_NO_FRAME, android.R.style.Theme_Dialog)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ popupVisibility.invoke()
+ setSurveyImage(imageUrl)
+
+ binding.ivSurveyDialogClose.onThrottleClick {
+ dismiss()
+ }
+
+ binding.btnSurveyDialog.onThrottleClick {
+ linkUrl.invoke()
+ dismiss()
+ }
+
+ binding.btnSurveyDialogSkip.onThrottleClick {
+ handleSkip.invoke()
+ dismiss()
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ val dialog = dialog
+ if (dialog != null) {
+ dialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ }
+ }
+
+ private fun setSurveyImage(text: String?) =
+ binding.ivSurveyDialog.load(text)
+
+ companion object {
+ fun newInstance(
+ imageUrl: String,
+ onNavigateWebView: () -> Unit,
+ onNegativeButtonClick: () -> Unit,
+ setPopupVisibility: () -> Unit,
+ ): SurveyDialogFragment {
+ val args = Bundle().apply {
+ putString("imageUrl", imageUrl)
+ }
+ return SurveyDialogFragment().apply {
+ arguments = args
+ linkUrl = onNavigateWebView
+ handleSkip = onNegativeButtonClick
+ popupVisibility = setPopupVisibility
+ }
+ }
+ }
+}
diff --git a/feature/home/src/main/java/org/sopt/home/model/UpdatePriority.kt b/feature/home/src/main/java/org/sopt/home/model/UpdatePriority.kt
new file mode 100644
index 00000000..533dc833
--- /dev/null
+++ b/feature/home/src/main/java/org/sopt/home/model/UpdatePriority.kt
@@ -0,0 +1,20 @@
+package org.sopt.home.model
+
+enum class UpdatePriority {
+ EMPTY,
+ MINOR,
+ MAJOR,
+ CRITICAL,
+ ;
+
+ companion object {
+ fun toUpdatePriority(value: Int): UpdatePriority {
+ return when (value) {
+ in 0..1 -> MINOR
+ in 2..3 -> MAJOR
+ in 4..5 -> CRITICAL
+ else -> EMPTY
+ }
+ }
+ }
+}
diff --git a/feature/home/src/main/res/layout/fragment_market_update_dialog.xml b/feature/home/src/main/res/layout/fragment_market_update_dialog.xml
new file mode 100644
index 00000000..d65351ff
--- /dev/null
+++ b/feature/home/src/main/res/layout/fragment_market_update_dialog.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/feature/home/src/main/res/layout/fragment_survey_dialog.xml b/feature/home/src/main/res/layout/fragment_survey_dialog.xml
new file mode 100644
index 00000000..e152ce04
--- /dev/null
+++ b/feature/home/src/main/res/layout/fragment_survey_dialog.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/feature/login/src/main/java/org/sopt/login/onboarding/LoginActivity.kt b/feature/login/src/main/java/org/sopt/login/onboarding/LoginActivity.kt
index 8d719c0c..957c5df2 100644
--- a/feature/login/src/main/java/org/sopt/login/onboarding/LoginActivity.kt
+++ b/feature/login/src/main/java/org/sopt/login/onboarding/LoginActivity.kt
@@ -43,6 +43,8 @@ class LoginActivity : AppCompatActivity() {
initCheckAutoLogin()
initKakaoLoginBtnClickListener()
initAuthStateObserver()
+ initPopupVisible()
+ initMarketUpdateVisible()
binding.vpOnboarding.adapter = LoginViewPagerAdapter(this)
binding.indicatorOnboarding.attachTo(binding.vpOnboarding)
@@ -99,6 +101,18 @@ class LoginActivity : AppCompatActivity() {
}
}
+ private fun initPopupVisible() {
+ lifecycleScope.launch {
+ dataStore.setPopupVisibility(true)
+ }
+ }
+
+ private fun initMarketUpdateVisible() {
+ lifecycleScope.launch {
+ dataStore.setMarketUpdate(true)
+ }
+ }
+
companion object {
@JvmStatic
fun newInstance(context: Context) = Intent(context, LoginActivity::class.java).apply {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5a18d7f3..29a8e40e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -47,6 +47,7 @@ detekt = "1.23.1"
kakao-login = "2.19.0"
google-service = "4.4.0"
+google-play-core = "1.10.2"
process-pheonix = "2.1.2"
preference = "1.2.1"
core-splashscreen = "1.0.1"
@@ -54,6 +55,8 @@ core-splashscreen = "1.0.1"
viewpager-indicator = "5.0"
activity = "1.8.0"
+amplitude = "1.+"
+
[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
@@ -152,10 +155,13 @@ viewpager-indicator = { group = "com.tbuonomo", name = "dotsindicator", version.
detekt-plugin-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" }
kakao-login = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao-login" }
+google-play-core = { group = "com.google.android.play", name = "core", version.ref = "google-play-core" }
process-phoenix = { module = "com.jakewharton:process-phoenix", version.ref = "process-pheonix" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+amplitude-analytics = { group = "com.amplitude", name = "analytics-android", version.ref = "amplitude" }
+
[bundles]
firebase = ["firebase-analytics", "firebase-database", "firebase-messaging", "firebase-remoteConfig"]
androidx-lifecycle = ["androidx-lifecycle-runtime-ktx", "androidx-lifecycle-viewmodel-ktx"]