Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ dependencies {
// Mavericks
implementation(libs.bundles.mavericks)

// Play Store
implementation(libs.bundles.plays)

// ETC
implementation(libs.timber)
implementation(libs.lottie.compose)
Expand Down
29 changes: 29 additions & 0 deletions app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt
Original file line number Diff line number Diff line change
@@ -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)
Comment on lines +15 to +18
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add null check for reviewInfo before launching review flow.

While the task success check is good, you should also validate that reviewInfo is not null before launching the review flow to prevent potential crashes.

 request.addOnCompleteListener { task ->
     if (task.isSuccessful) {
         val reviewInfo = task.result
-        reviewManager.launchReviewFlow(activity, reviewInfo)
+        if (reviewInfo != null) {
+            reviewManager.launchReviewFlow(activity, reviewInfo)
+        }
     } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
request.addOnCompleteListener { task ->
if (task.isSuccessful) {
val reviewInfo = task.result
reviewManager.launchReviewFlow(activity, reviewInfo)
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
if (reviewInfo != null) {
reviewManager.launchReviewFlow(activity, reviewInfo)
}
} else {
// Log the failure for debugging
Timber.w("In-app review request failed: ${task.exception?.message}")
// Fallback logic with error handling
try {
val uri = Uri.parse("market://details?id=${activity.packageName}")
val intent = Intent(Intent.ACTION_VIEW, uri)
if (intent.resolveActivity(activity.packageManager) != null) {
activity.startActivity(intent)
} else {
// Fallback to web version if Play Store app isn't available
val webUri = Uri.parse("https://play.google.com/store/apps/details?id=${activity.packageName}")
val webIntent = Intent(Intent.ACTION_VIEW, webUri)
activity.startActivity(webIntent)
}
} catch (e: Exception) {
Timber.e(e, "Failed to open store for app review")
}
}
}
}
}
🤖 Prompt for AI Agents
In app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt around
lines 16 to 19, add a null check for the variable reviewInfo before calling
reviewManager.launchReviewFlow. Ensure that reviewInfo is not null to prevent
potential crashes by wrapping the launchReviewFlow call inside an if statement
that verifies reviewInfo is non-null.

} else {
try {
AppUpdateUtils.navigateToMarket(activity)
} catch (e: Exception) {
e.printStackTrace()
Timber.e(e, "Failed to open store for app review")
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.sopt.clody.data.local.datasource

interface AppReviewLocalDataSource {
var shouldShowPopup: Boolean
}
Original file line number Diff line number Diff line change
@@ -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) }
Comment on lines +13 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P5
인터페이스를 get/set 함수로 안하고 이렇게 하는 방식 좋네욤. 저장/읽기만 잇으니 더 간결한듯!


companion object {
private const val SHOULD_SHOW_POPUP = "shouldShowPopup"
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
8 changes: 8 additions & 0 deletions app/src/main/java/com/sopt/clody/di/LocalDataSourceModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
8 changes: 8 additions & 0 deletions app/src/main/java/com/sopt/clody/di/RepositoryModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -72,4 +74,10 @@ abstract class RepositoryModule {
abstract fun bindDraftRepository(
draftRepositoryImpl: DraftRepositoryImpl,
): DraftRepository

@Binds
@Singleton
abstract fun bindReviewRepository(
reviewRepositoryImpl: ReviewRepositoryImpl,
): ReviewRepository
}
12 changes: 10 additions & 2 deletions app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}
4 changes: 4 additions & 0 deletions app/src/main/java/com/sopt/clody/di/qualifier/Qualifier.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ annotation class TokenPrefs
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class FirstDraftPrefs

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ReviewPrefs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.sopt.clody.domain.repository

interface ReviewRepository {
fun getShouldShowPopup(): Boolean
fun setShouldShowPopup(state: Boolean)
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ fun NavGraphBuilder.homeScreen(
selectedYear = selectedYear,
selectedMonth = selectedMonth,
selectedDay = selectedDay,
isFromReplyDiary = isFromReplyDiary,
navigateToDiaryList = navigateToDiaryList,
navigateToSetting = navigateToSetting,
navigateToWriteDiary = navigateToWriteDiary,
Expand All @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<MonthlyCalendarResponseDto>>(CalendarState.Idle)
Expand Down Expand Up @@ -81,6 +83,9 @@ class HomeViewModel @Inject constructor(
private val _draftAlarmEnableToast = MutableStateFlow(false)
val draftAlarmEnableToast: StateFlow<Boolean> = _draftAlarmEnableToast

private val _showInAppReviewPopup = MutableStateFlow(reviewRepository.getShouldShowPopup())
val showInAppReviewPopup: StateFlow<Boolean> get() = _showInAppReviewPopup

private val _errorState = MutableStateFlow<Pair<Boolean, String>>(false to "")
val errorState: StateFlow<Pair<Boolean, String>> = _errorState

Expand Down Expand Up @@ -251,4 +256,9 @@ class HomeViewModel @Inject constructor(
fun resetDraftAlarmEnableToast() {
_draftAlarmEnableToast.value = false
}

fun updateShowInAppReviewPopup(state: Boolean) {
reviewRepository.setShouldShowPopup(state)
_showInAppReviewPopup.value = state
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
}
Expand All @@ -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,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Route.ReplyDiary> { backStackEntry ->
backStackEntry.toRoute<Route.ReplyDiary>().apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ sealed interface Route {
val selectedYear: Int,
val selectedMonth: Int,
val selectedDay: Int? = null,
val isFromReplyDiary: Boolean = false,
) : Route

@Serializable
Expand Down
11 changes: 11 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down Expand Up @@ -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" }
Expand Down Expand Up @@ -193,3 +199,8 @@ mavericks = [
"mavericks-compose",
"mavericks-hilt"
]

plays = [
"play-review",
"play-review-ktx"
]