diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2eafa6da..e62a75f9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -123,6 +123,9 @@ dependencies { // Mavericks implementation(libs.bundles.mavericks) + // Play Store + implementation(libs.bundles.plays) + // ETC implementation(libs.timber) implementation(libs.lottie.compose) diff --git a/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt b/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt new file mode 100644 index 00000000..c19a02af --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt @@ -0,0 +1,29 @@ +package com.sopt.clody.core.review + +import android.app.Activity +import com.google.android.play.core.review.ReviewManagerFactory +import com.sopt.clody.presentation.utils.appupdate.AppUpdateUtils +import timber.log.Timber + +object InAppReviewManager { + fun showPopup(activity: Activity) { + if (activity.isFinishing || activity.isDestroyed) return + + val reviewManager = ReviewManagerFactory.create(activity) + val request = reviewManager.requestReviewFlow() + + request.addOnCompleteListener { task -> + if (task.isSuccessful) { + val reviewInfo = task.result + reviewManager.launchReviewFlow(activity, reviewInfo) + } else { + try { + AppUpdateUtils.navigateToMarket(activity) + } catch (e: Exception) { + e.printStackTrace() + Timber.e(e, "Failed to open store for app review") + } + } + } + } +} diff --git a/app/src/main/java/com/sopt/clody/data/local/datasource/AppReviewLocalDataSource.kt b/app/src/main/java/com/sopt/clody/data/local/datasource/AppReviewLocalDataSource.kt new file mode 100644 index 00000000..4c2c5992 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/local/datasource/AppReviewLocalDataSource.kt @@ -0,0 +1,5 @@ +package com.sopt.clody.data.local.datasource + +interface AppReviewLocalDataSource { + var shouldShowPopup: Boolean +} diff --git a/app/src/main/java/com/sopt/clody/data/local/datasourceimpl/AppReviewLocalDataSourceImpl.kt b/app/src/main/java/com/sopt/clody/data/local/datasourceimpl/AppReviewLocalDataSourceImpl.kt new file mode 100644 index 00000000..684eb9f6 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/local/datasourceimpl/AppReviewLocalDataSourceImpl.kt @@ -0,0 +1,20 @@ +package com.sopt.clody.data.local.datasourceimpl + +import android.content.SharedPreferences +import androidx.core.content.edit +import com.sopt.clody.data.local.datasource.AppReviewLocalDataSource +import com.sopt.clody.di.qualifier.ReviewPrefs +import javax.inject.Inject + +class AppReviewLocalDataSourceImpl @Inject constructor( + @ReviewPrefs private val sharedPreferences: SharedPreferences, +) : AppReviewLocalDataSource { + + override var shouldShowPopup: Boolean + get() = sharedPreferences.getBoolean(SHOULD_SHOW_POPUP, true) + set(value) = sharedPreferences.edit { putBoolean(SHOULD_SHOW_POPUP, value) } + + companion object { + private const val SHOULD_SHOW_POPUP = "shouldShowPopup" + } +} diff --git a/app/src/main/java/com/sopt/clody/data/repositoryimpl/ReviewRepositoryImpl.kt b/app/src/main/java/com/sopt/clody/data/repositoryimpl/ReviewRepositoryImpl.kt new file mode 100644 index 00000000..7421396a --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/repositoryimpl/ReviewRepositoryImpl.kt @@ -0,0 +1,15 @@ +package com.sopt.clody.data.repositoryimpl + +import com.sopt.clody.data.local.datasource.AppReviewLocalDataSource +import com.sopt.clody.domain.repository.ReviewRepository +import javax.inject.Inject + +class ReviewRepositoryImpl @Inject constructor( + private val appReviewLocalDataSource: AppReviewLocalDataSource, +) : ReviewRepository { + override fun getShouldShowPopup(): Boolean = appReviewLocalDataSource.shouldShowPopup + + override fun setShouldShowPopup(state: Boolean) { + appReviewLocalDataSource.shouldShowPopup = state + } +} diff --git a/app/src/main/java/com/sopt/clody/di/LocalDataSourceModule.kt b/app/src/main/java/com/sopt/clody/di/LocalDataSourceModule.kt index 1f28e407..648f66dd 100644 --- a/app/src/main/java/com/sopt/clody/di/LocalDataSourceModule.kt +++ b/app/src/main/java/com/sopt/clody/di/LocalDataSourceModule.kt @@ -3,9 +3,12 @@ package com.sopt.clody.di import android.content.SharedPreferences import com.sopt.clody.data.datastore.TokenDataStore import com.sopt.clody.data.datastore.TokenDataStoreImpl +import com.sopt.clody.data.local.datasource.AppReviewLocalDataSource import com.sopt.clody.data.local.datasource.FirstDraftLocalDataSource +import com.sopt.clody.data.local.datasourceimpl.AppReviewLocalDataSourceImpl import com.sopt.clody.data.local.datasourceimpl.FirstDraftLocalDataSourceImpl import com.sopt.clody.di.qualifier.FirstDraftPrefs +import com.sopt.clody.di.qualifier.ReviewPrefs import com.sopt.clody.di.qualifier.TokenPrefs import dagger.Module import dagger.Provides @@ -26,4 +29,9 @@ object LocalDataSourceModule { @Singleton fun provideFirstDraftLocalDataSource(@FirstDraftPrefs sharedPreferences: SharedPreferences): FirstDraftLocalDataSource = FirstDraftLocalDataSourceImpl(sharedPreferences) + + @Provides + @Singleton + fun provideAppReviewLocalDataSource(@ReviewPrefs sharedPreferences: SharedPreferences): AppReviewLocalDataSource = + AppReviewLocalDataSourceImpl(sharedPreferences) } diff --git a/app/src/main/java/com/sopt/clody/di/RepositoryModule.kt b/app/src/main/java/com/sopt/clody/di/RepositoryModule.kt index b1615eb0..008046e6 100644 --- a/app/src/main/java/com/sopt/clody/di/RepositoryModule.kt +++ b/app/src/main/java/com/sopt/clody/di/RepositoryModule.kt @@ -6,6 +6,7 @@ import com.sopt.clody.data.repositoryimpl.AuthRepositoryImpl import com.sopt.clody.data.repositoryimpl.DiaryRepositoryImpl import com.sopt.clody.data.repositoryimpl.DraftRepositoryImpl import com.sopt.clody.data.repositoryimpl.NotificationRepositoryImpl +import com.sopt.clody.data.repositoryimpl.ReviewRepositoryImpl import com.sopt.clody.data.repositoryimpl.TokenReissueRepositoryImpl import com.sopt.clody.data.repositoryimpl.TokenRepositoryImpl import com.sopt.clody.domain.repository.AccountManagementRepository @@ -14,6 +15,7 @@ import com.sopt.clody.domain.repository.AuthRepository import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.domain.repository.NotificationRepository +import com.sopt.clody.domain.repository.ReviewRepository import com.sopt.clody.domain.repository.TokenReissueRepository import com.sopt.clody.domain.repository.TokenRepository import dagger.Binds @@ -72,4 +74,10 @@ abstract class RepositoryModule { abstract fun bindDraftRepository( draftRepositoryImpl: DraftRepositoryImpl, ): DraftRepository + + @Binds + @Singleton + abstract fun bindReviewRepository( + reviewRepositoryImpl: ReviewRepositoryImpl, + ): ReviewRepository } diff --git a/app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt b/app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt index 0fda0f82..795a2b89 100644 --- a/app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt +++ b/app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt @@ -3,6 +3,7 @@ package com.sopt.clody.di import android.content.Context import android.content.SharedPreferences import com.sopt.clody.di.qualifier.FirstDraftPrefs +import com.sopt.clody.di.qualifier.ReviewPrefs import com.sopt.clody.di.qualifier.TokenPrefs import dagger.Module import dagger.Provides @@ -15,17 +16,24 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object SharedPreferencesModule { - @TokenPrefs @Provides @Singleton + @TokenPrefs fun provideTokenSharedPreferences(@ApplicationContext context: Context): SharedPreferences { return context.getSharedPreferences("token_prefs", Context.MODE_PRIVATE) } - @FirstDraftPrefs @Provides @Singleton + @FirstDraftPrefs fun provideFirstDraftSharedPreferences(@ApplicationContext context: Context): SharedPreferences { return context.getSharedPreferences("first_draft_prefs", Context.MODE_PRIVATE) } + + @Provides + @Singleton + @ReviewPrefs + fun provideReviewSharedPreferences(@ApplicationContext context: Context): SharedPreferences { + return context.getSharedPreferences("review_prefs", Context.MODE_PRIVATE) + } } diff --git a/app/src/main/java/com/sopt/clody/di/qualifier/Qualifier.kt b/app/src/main/java/com/sopt/clody/di/qualifier/Qualifier.kt index dc14802f..8af33e80 100644 --- a/app/src/main/java/com/sopt/clody/di/qualifier/Qualifier.kt +++ b/app/src/main/java/com/sopt/clody/di/qualifier/Qualifier.kt @@ -9,3 +9,7 @@ annotation class TokenPrefs @Qualifier @Retention(AnnotationRetention.BINARY) annotation class FirstDraftPrefs + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class ReviewPrefs diff --git a/app/src/main/java/com/sopt/clody/domain/repository/ReviewRepository.kt b/app/src/main/java/com/sopt/clody/domain/repository/ReviewRepository.kt new file mode 100644 index 00000000..044a0cf5 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/domain/repository/ReviewRepository.kt @@ -0,0 +1,6 @@ +package com.sopt.clody.domain.repository + +interface ReviewRepository { + fun getShouldShowPopup(): Boolean + fun setShouldShowPopup(state: Boolean) +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt index 6cc39e2d..550a570b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt @@ -28,6 +28,7 @@ fun NavGraphBuilder.homeScreen( selectedYear = selectedYear, selectedMonth = selectedMonth, selectedDay = selectedDay, + isFromReplyDiary = isFromReplyDiary, navigateToDiaryList = navigateToDiaryList, navigateToSetting = navigateToSetting, navigateToWriteDiary = navigateToWriteDiary, @@ -41,7 +42,8 @@ fun NavController.navigateToHome( selectedYear: Int = LocalDate.now().year, selectedMonth: Int = LocalDate.now().monthValue, selectedDay: Int? = LocalDate.now().dayOfMonth, + isFromReplyDiary: Boolean = false, navOptions: NavOptionsBuilder.() -> Unit = {}, ) { - navigate(Route.Home(selectedYear, selectedMonth, selectedDay), navOptions) + navigate(Route.Home(selectedYear, selectedMonth, selectedDay, isFromReplyDiary), navOptions) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 97d7615c..6d94bffa 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sopt.clody.R +import com.sopt.clody.core.review.InAppReviewManager import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen @@ -49,6 +50,7 @@ fun HomeRoute( selectedYear: Int, selectedMonth: Int, selectedDay: Int?, + isFromReplyDiary: Boolean, navigateToDiaryList: (year: Int, month: Int) -> Unit, navigateToSetting: () -> Unit, navigateToWriteDiary: (year: Int, month: Int, date: Int) -> Unit, @@ -67,6 +69,7 @@ fun HomeRoute( val showFirstDraftPopup by homeViewModel.showFirstDraftPopup.collectAsStateWithLifecycle() val draftAlarmEnableToast by homeViewModel.draftAlarmEnableToast.collectAsStateWithLifecycle() val context = LocalContext.current + val showInAppReviewPopup by homeViewModel.showInAppReviewPopup.collectAsStateWithLifecycle() val isError = calendarState is CalendarState.Error || dailyDiariesState is DailyDiariesState.Error val errorMessage = when { @@ -77,6 +80,11 @@ fun HomeRoute( LaunchedEffect(Unit) { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME) + + if (showInAppReviewPopup && isFromReplyDiary) { + InAppReviewManager.showPopup(context as Activity) + homeViewModel.updateShowInAppReviewPopup(false) + } } LaunchedEffect(selectedYear, selectedMonth, selectedDay) { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index c90b579d..38b5d732 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -12,6 +12,7 @@ import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.domain.repository.NotificationRepository +import com.sopt.clody.domain.repository.ReviewRepository import com.sopt.clody.presentation.ui.home.calendar.model.DiaryDateData import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationChangeState import com.sopt.clody.presentation.utils.network.ErrorMessages @@ -29,6 +30,7 @@ class HomeViewModel @Inject constructor( private val networkUtil: NetworkUtil, private val draftRepository: DraftRepository, private val fcmTokenProvider: FcmTokenProvider, + private val reviewRepository: ReviewRepository, ) : ViewModel() { private val _calendarState = MutableStateFlow>(CalendarState.Idle) @@ -81,6 +83,9 @@ class HomeViewModel @Inject constructor( private val _draftAlarmEnableToast = MutableStateFlow(false) val draftAlarmEnableToast: StateFlow = _draftAlarmEnableToast + private val _showInAppReviewPopup = MutableStateFlow(reviewRepository.getShouldShowPopup()) + val showInAppReviewPopup: StateFlow get() = _showInAppReviewPopup + private val _errorState = MutableStateFlow>(false to "") val errorState: StateFlow> = _errorState @@ -251,4 +256,9 @@ class HomeViewModel @Inject constructor( fun resetDraftAlarmEnableToast() { _draftAlarmEnableToast.value = false } + + fun updateShowInAppReviewPopup(state: Boolean) { + reviewRepository.setShouldShowPopup(state) + _showInAppReviewPopup.value = state + } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt index c4aa168e..f112fc19 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt @@ -49,7 +49,7 @@ fun ReplyDiaryRoute( month: Int, date: Int, replyStatus: ReplyStatus, - navigateToHome: (year: Int, month: Int, date: Int) -> Unit, + navigateToHome: (year: Int, month: Int, date: Int, isFromReplyDiary: Boolean) -> Unit, viewModel: ReplyDiaryViewModel = hiltViewModel(), ) { val replyDiaryState by viewModel.replyDiaryState.collectAsState() @@ -65,7 +65,7 @@ fun ReplyDiaryRoute( BackHandler { val currentTime = System.currentTimeMillis() if (currentTime - backPressedTime <= backPressThreshold) { - navigateToHome(year, month, date) + navigateToHome(year, month, date, true) } else { backPressedTime = currentTime } @@ -79,7 +79,7 @@ fun ReplyDiaryRoute( is ReplyDiaryState.Success -> { val successState = replyDiaryState as ReplyDiaryState.Success ReplyDiaryScreen( - navigateToHome = { navigateToHome(year, month, date) }, + navigateToHome = { navigateToHome(year, month, date, true) }, replyStatus = replyStatus, replyDiaryState = successState, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt index f1e5b161..492ea5f2 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt @@ -10,7 +10,7 @@ import com.sopt.clody.presentation.ui.replydiary.ReplyDiaryRoute import com.sopt.clody.presentation.utils.navigation.Route fun NavGraphBuilder.replyDiaryScreen( - navigateToHome: (year: Int, month: Int, date: Int) -> Unit, + navigateToHome: (year: Int, month: Int, date: Int, isFromReplyDiary: Boolean) -> Unit, ) { composable { backStackEntry -> backStackEntry.toRoute().apply { diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt index 7c423e1e..334475b6 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt @@ -31,6 +31,7 @@ sealed interface Route { val selectedYear: Int, val selectedMonth: Int, val selectedDay: Int? = null, + val isFromReplyDiary: Boolean = false, ) : Route @Serializable diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bb253a92..fb65e83e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,6 +50,8 @@ firebase-config-ktx = "22.1.0" mavericks = "3.0.9" +play-review = "2.0.2" + [libraries] # AndroidX Core core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } @@ -122,6 +124,10 @@ mavericks = { module = "com.airbnb.android:mavericks", version.ref = "mavericks" mavericks-compose = { module = "com.airbnb.android:mavericks-compose", version.ref = "mavericks" } mavericks-hilt = { module = "com.airbnb.android:mavericks-hilt", version.ref = "mavericks" } +# PlayStore In App Review +play-review = { group = "com.google.android.play", name = "review", version.ref = "play-review"} +play-review-ktx = { group = "com.google.android.play", name = "review-ktx", version.ref = "play-review"} + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } @@ -193,3 +199,8 @@ mavericks = [ "mavericks-compose", "mavericks-hilt" ] + +plays = [ + "play-review", + "play-review-ktx" +]