From 23eb461fb82782126ccdc8f8acc1f898969570bb Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Tue, 21 Apr 2026 14:20:19 +0200 Subject: [PATCH 1/4] feat: Do not display the review pop-up again if the user clicks no --- .../AppReviewSettingsRepository.kt | 9 ++---- .../inappreview/BaseInAppReviewManager.kt | 3 +- .../reviewmanagers/InAppReviewManager.kt | 29 +++++++------------ 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/InAppReview/src/main/kotlin/com/infomaniak/core/inappreview/AppReviewSettingsRepository.kt b/InAppReview/src/main/kotlin/com/infomaniak/core/inappreview/AppReviewSettingsRepository.kt index 09dd5a76c..9ce96962e 100644 --- a/InAppReview/src/main/kotlin/com/infomaniak/core/inappreview/AppReviewSettingsRepository.kt +++ b/InAppReview/src/main/kotlin/com/infomaniak/core/inappreview/AppReviewSettingsRepository.kt @@ -41,7 +41,6 @@ private val Context.dataStore by preferencesDataStore( class AppReviewSettingsRepository(private val context: Context) { internal var appReviewThreshold = DEFAULT_APP_REVIEW_THRESHOLD - internal var maxAppReviewThreshold = appReviewThreshold * 10 fun flowFor(key: Preferences.Key) = context.dataStore.data .map { it[key] ?: (getInitialValue(key) as T) } @@ -56,7 +55,7 @@ class AppReviewSettingsRepository(private val context: Context) { private fun getInitialValue(key: Preferences.Key) = when (key) { APP_REVIEW_THRESHOLD_KEY -> appReviewThreshold - ALREADY_GAVE_REVIEW_KEY -> DEFAULT_ALREADY_GAVE_REVIEW + ALREADY_ASK_REVIEW_KEY -> DEFAULT_ALREADY_GAVE_REVIEW else -> throw IllegalArgumentException("Unknown Preferences.Key") } @@ -72,16 +71,12 @@ class AppReviewSettingsRepository(private val context: Context) { context.dataStore.edit(MutablePreferences::clear) } - suspend fun resetReviewSettings() { - setValue(APP_REVIEW_THRESHOLD_KEY, maxAppReviewThreshold) - } - companion object { private const val TAG = "AppReviewSettingsRepository" val APP_REVIEW_THRESHOLD_KEY = intPreferencesKey("appReviewThresholdKey") - val ALREADY_GAVE_REVIEW_KEY = booleanPreferencesKey("alreadyGaveReview") + val ALREADY_ASK_REVIEW_KEY = booleanPreferencesKey("alreadyGaveReview") internal const val DATA_STORE_NAME = "AppReviewSettingsDataStore" diff --git a/InAppReview/src/main/kotlin/com/infomaniak/core/inappreview/BaseInAppReviewManager.kt b/InAppReview/src/main/kotlin/com/infomaniak/core/inappreview/BaseInAppReviewManager.kt index 386c022eb..c28339393 100644 --- a/InAppReview/src/main/kotlin/com/infomaniak/core/inappreview/BaseInAppReviewManager.kt +++ b/InAppReview/src/main/kotlin/com/infomaniak/core/inappreview/BaseInAppReviewManager.kt @@ -27,7 +27,6 @@ abstract class BaseInAppReviewManager(private val activity: ComponentActivity) : open fun init( countdownBehavior: Behavior = Behavior.LifecycleBased, appReviewThreshold: Int? = null, - maxAppReviewThreshold: Int? = null, onUserWantToReview: (() -> Unit)? = null, onUserWantToGiveFeedback: (() -> Unit)? = null ) = Unit @@ -40,6 +39,8 @@ abstract class BaseInAppReviewManager(private val activity: ComponentActivity) : open fun decrementAppReviewCountdown() = Unit + open fun onReviewDialogShown() = Unit + enum class Behavior { /** This behavior uses the activity's lifecycle observer to automatically update the countdown */ LifecycleBased, diff --git a/InAppReview/src/standard/kotlin/com/infomaniak/core/inappreview/reviewmanagers/InAppReviewManager.kt b/InAppReview/src/standard/kotlin/com/infomaniak/core/inappreview/reviewmanagers/InAppReviewManager.kt index a96aac561..38f209442 100644 --- a/InAppReview/src/standard/kotlin/com/infomaniak/core/inappreview/reviewmanagers/InAppReviewManager.kt +++ b/InAppReview/src/standard/kotlin/com/infomaniak/core/inappreview/reviewmanagers/InAppReviewManager.kt @@ -23,7 +23,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.google.android.play.core.review.ReviewManagerFactory import com.infomaniak.core.inappreview.AppReviewSettingsRepository -import com.infomaniak.core.inappreview.AppReviewSettingsRepository.Companion.ALREADY_GAVE_REVIEW_KEY +import com.infomaniak.core.inappreview.AppReviewSettingsRepository.Companion.ALREADY_ASK_REVIEW_KEY import com.infomaniak.core.inappreview.AppReviewSettingsRepository.Companion.APP_REVIEW_THRESHOLD_KEY import com.infomaniak.core.inappreview.BaseInAppReviewManager import com.infomaniak.core.webview.ui.WebViewActivity @@ -38,20 +38,18 @@ class InAppReviewManager(private val activity: ComponentActivity) : BaseInAppRev private val appReviewSettingsRepository = AppReviewSettingsRepository(activity) private val appReviewCountdown = appReviewSettingsRepository.flowFor(APP_REVIEW_THRESHOLD_KEY) - private val alreadyGaveReview = appReviewSettingsRepository.flowFor(ALREADY_GAVE_REVIEW_KEY) - + private val alreadyAskReview = appReviewSettingsRepository.flowFor(ALREADY_ASK_REVIEW_KEY) private var onUserWantsToReview: (() -> Unit)? = null private var onUserWantsToGiveFeedback: (() -> Unit)? = null override val shouldDisplayReviewDialog = - combine(alreadyGaveReview, appReviewCountdown) { alreadyGaveReview, countdown -> + combine(alreadyAskReview, appReviewCountdown) { alreadyGaveReview, countdown -> !alreadyGaveReview && countdown <= 0 }.distinctUntilChanged() override fun init( countdownBehavior: Behavior, appReviewThreshold: Int?, - maxAppReviewThreshold: Int?, onUserWantToReview: (() -> Unit)?, onUserWantToGiveFeedback: (() -> Unit)? ) { @@ -60,7 +58,6 @@ class InAppReviewManager(private val activity: ComponentActivity) : BaseInAppRev if (countdownBehavior == Behavior.LifecycleBased) activity.lifecycle.addObserver(observer = this) appReviewThreshold?.let { appReviewSettingsRepository.appReviewThreshold = it } - maxAppReviewThreshold?.let { appReviewSettingsRepository.maxAppReviewThreshold = it } } override fun onResume(owner: LifecycleOwner) { @@ -71,37 +68,33 @@ class InAppReviewManager(private val activity: ComponentActivity) : BaseInAppRev //region public interface override fun onUserWantsToReview() { onUserWantsToReview?.invoke() - resetAppReviewSettings() - setAppReviewedStatus() launchInAppReview() } override fun onUserWantsToGiveFeedback(feedbackUrl: String) { onUserWantsToGiveFeedback?.invoke() - resetAppReviewSettings() + setAppReviewedStatus() WebViewActivity.startActivity(activity, feedbackUrl) } - override fun onUserWantsToDismiss() { - resetAppReviewSettings() - } + override fun onUserWantsToDismiss() = Unit override fun decrementAppReviewCountdown() { activity.lifecycleScope.launch(Dispatchers.IO) { val appReviewCountdown = appReviewSettingsRepository.getValue(APP_REVIEW_THRESHOLD_KEY) - alreadyGaveReview.collectLatest { hasGivenReview -> - if (!hasGivenReview) set(APP_REVIEW_THRESHOLD_KEY, appReviewCountdown - 1) + alreadyAskReview.collectLatest { hasAskedReview -> + if (!hasAskedReview) set(APP_REVIEW_THRESHOLD_KEY, appReviewCountdown - 1) } } } - //endregion - private fun resetAppReviewSettings() = activity.lifecycleScope.launch(Dispatchers.IO) { - appReviewSettingsRepository.resetReviewSettings() + override fun onReviewDialogShown() { + setAppReviewedStatus() } + //endregion private fun setAppReviewedStatus() = activity.lifecycleScope.launch(Dispatchers.IO) { - set(ALREADY_GAVE_REVIEW_KEY, true) + set(ALREADY_ASK_REVIEW_KEY, true) } private fun set(key: Preferences.Key, value: T) = activity.lifecycleScope.launch(Dispatchers.IO) { From b9ea339488712b2d6cc6003213970cec8b339097 Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Fri, 7 Aug 2026 11:26:25 +0200 Subject: [PATCH 2/4] fix: Rename key and remove unused code --- .../core/inappreview/AppReviewSettingsRepository.kt | 6 +++--- .../core/inappreview/reviewmanagers/InAppReviewManager.kt | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/InAppReview/src/main/kotlin/com/infomaniak/core/inappreview/AppReviewSettingsRepository.kt b/InAppReview/src/main/kotlin/com/infomaniak/core/inappreview/AppReviewSettingsRepository.kt index 9ce96962e..2f6ad109f 100644 --- a/InAppReview/src/main/kotlin/com/infomaniak/core/inappreview/AppReviewSettingsRepository.kt +++ b/InAppReview/src/main/kotlin/com/infomaniak/core/inappreview/AppReviewSettingsRepository.kt @@ -55,7 +55,7 @@ class AppReviewSettingsRepository(private val context: Context) { private fun getInitialValue(key: Preferences.Key) = when (key) { APP_REVIEW_THRESHOLD_KEY -> appReviewThreshold - ALREADY_ASK_REVIEW_KEY -> DEFAULT_ALREADY_GAVE_REVIEW + ALREADY_ASK_REVIEW_KEY -> DEFAULT_ALREADY_ASK_REVIEW else -> throw IllegalArgumentException("Unknown Preferences.Key") } @@ -76,11 +76,11 @@ class AppReviewSettingsRepository(private val context: Context) { private const val TAG = "AppReviewSettingsRepository" val APP_REVIEW_THRESHOLD_KEY = intPreferencesKey("appReviewThresholdKey") - val ALREADY_ASK_REVIEW_KEY = booleanPreferencesKey("alreadyGaveReview") + val ALREADY_ASK_REVIEW_KEY = booleanPreferencesKey("alreadyAskReviewKey") internal const val DATA_STORE_NAME = "AppReviewSettingsDataStore" - private const val DEFAULT_ALREADY_GAVE_REVIEW = false + private const val DEFAULT_ALREADY_ASK_REVIEW = false private const val DEFAULT_APP_REVIEW_THRESHOLD = 50 } diff --git a/InAppReview/src/standard/kotlin/com/infomaniak/core/inappreview/reviewmanagers/InAppReviewManager.kt b/InAppReview/src/standard/kotlin/com/infomaniak/core/inappreview/reviewmanagers/InAppReviewManager.kt index 38f209442..d8bf06e3a 100644 --- a/InAppReview/src/standard/kotlin/com/infomaniak/core/inappreview/reviewmanagers/InAppReviewManager.kt +++ b/InAppReview/src/standard/kotlin/com/infomaniak/core/inappreview/reviewmanagers/InAppReviewManager.kt @@ -73,12 +73,9 @@ class InAppReviewManager(private val activity: ComponentActivity) : BaseInAppRev override fun onUserWantsToGiveFeedback(feedbackUrl: String) { onUserWantsToGiveFeedback?.invoke() - setAppReviewedStatus() WebViewActivity.startActivity(activity, feedbackUrl) } - override fun onUserWantsToDismiss() = Unit - override fun decrementAppReviewCountdown() { activity.lifecycleScope.launch(Dispatchers.IO) { val appReviewCountdown = appReviewSettingsRepository.getValue(APP_REVIEW_THRESHOLD_KEY) From 2d6bc34953321de1e1a089258860d97e390085bb Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Fri, 7 Aug 2026 11:38:57 +0200 Subject: [PATCH 3/4] feat: Update README --- InAppReview/README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/InAppReview/README.md b/InAppReview/README.md index c64d4e893..98196369b 100644 --- a/InAppReview/README.md +++ b/InAppReview/README.md @@ -6,7 +6,7 @@ It supports two distribution flavors: - **`standard`** — Google Play Review API (`ReviewManagerFactory`) - **`fdroid`** — No-op manager (no Play dependency). If you still want to prompt users, you can show `ReviewAlertDialog` yourself. -The review prompt is only shown after a configurable number of app launches ("countdown"), and is not shown again after the user has tapped "Review" (i.e., once the review flow has been triggered). +The review prompt is only shown after a configurable number of app launches ("countdown"), and is never shown again once it has been displayed to the user. ## 1. Integration (Gradle) @@ -45,7 +45,6 @@ class MainActivity : ComponentActivity() { // Manual → you call decrementAppReviewCountdown() yourself countdownBehavior = BaseInAppReviewManager.Behavior.LifecycleBased, appReviewThreshold = 50, // Optional: initial countdown value (default 50) - maxAppReviewThreshold = 500, // Optional: value reset to after user interacts (default threshold × 10) onUserWantToReview = { }, // Optional: called when user taps "Review" onUserWantToGiveFeedback = { }, // Optional: called when user taps "Give feedback" ) @@ -53,8 +52,8 @@ class MainActivity : ComponentActivity() { } ``` -> **Defaults:** `appReviewThreshold = 50`, `maxAppReviewThreshold = appReviewThreshold * 10`. -> After any user interaction (review, feedback, or dismiss), the countdown is reset to `maxAppReviewThreshold` so the prompt is not shown again too soon. +> **Default:** `appReviewThreshold = 50`. +> Once the prompt has been displayed, call `onReviewDialogShown()` so it is never shown again. --- @@ -63,9 +62,11 @@ class MainActivity : ComponentActivity() { ### 3.1 Observe `shouldDisplayReviewDialog` `shouldDisplayReviewDialog` is a `Flow` that emits `true` when: -- the user has **not** already submitted a review, **and** +- the user has **not** already been asked for a review, **and** - the countdown has reached **0 or below**. +Once the prompt is shown, call `onReviewDialogShown()` to mark it as asked; the flow will never emit `true` again. + Collect it in your Compose UI and show a dialog or bottom sheet accordingly: ```kotlin @@ -74,6 +75,7 @@ fun ReviewPromptObserver(reviewManager: InAppReviewManager) { val shouldDisplay by reviewManager.shouldDisplayReviewDialog.collectAsState(initial = false) if (shouldDisplay) { + reviewManager.onReviewDialogShown() ReviewDialog( onReview = { reviewManager.onUserWantsToReview() }, onFeedback = { reviewManager.onUserWantsToGiveFeedback("https://yourapp.example.com/feedback") }, From 5867896377da601d7dd97f1caf5a821c63a21148 Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Fri, 7 Aug 2026 12:50:52 +0200 Subject: [PATCH 4/4] fix: Add migration for preferences --- .../AppReviewSettingsRepository.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/InAppReview/src/main/kotlin/com/infomaniak/core/inappreview/AppReviewSettingsRepository.kt b/InAppReview/src/main/kotlin/com/infomaniak/core/inappreview/AppReviewSettingsRepository.kt index 2f6ad109f..76000f7a3 100644 --- a/InAppReview/src/main/kotlin/com/infomaniak/core/inappreview/AppReviewSettingsRepository.kt +++ b/InAppReview/src/main/kotlin/com/infomaniak/core/inappreview/AppReviewSettingsRepository.kt @@ -18,6 +18,7 @@ package com.infomaniak.core.inappreview import android.content.Context +import androidx.datastore.core.DataMigration import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.preferences.core.MutablePreferences import androidx.datastore.preferences.core.Preferences @@ -31,10 +32,27 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +private val migrateAlreadyAskReviewKey = object : DataMigration { + override suspend fun shouldMigrate(currentData: Preferences): Boolean = + currentData[AppReviewSettingsRepository.ALREADY_ASK_REVIEW_KEY] == null && + currentData[AppReviewSettingsRepository.LEGACY_ALREADY_ASK_REVIEW_KEY] != null + + override suspend fun migrate(currentData: Preferences): Preferences { + val legacyValue = currentData[AppReviewSettingsRepository.LEGACY_ALREADY_ASK_REVIEW_KEY] ?: return currentData + return currentData.toMutablePreferences().apply { + this[AppReviewSettingsRepository.ALREADY_ASK_REVIEW_KEY] = legacyValue + remove(AppReviewSettingsRepository.LEGACY_ALREADY_ASK_REVIEW_KEY) + }.toPreferences() + } + + override suspend fun cleanUp() = Unit +} + private val Context.dataStore by preferencesDataStore( name = AppReviewSettingsRepository.DATA_STORE_NAME, // In case we have a CorruptionException, we want to clear all DataStore preferences corruptionHandler = ReplaceFileCorruptionHandler { emptyPreferences() }, + produceMigrations = { listOf(migrateAlreadyAskReviewKey) }, ) @Suppress("UNCHECKED_CAST") @@ -77,6 +95,7 @@ class AppReviewSettingsRepository(private val context: Context) { val APP_REVIEW_THRESHOLD_KEY = intPreferencesKey("appReviewThresholdKey") val ALREADY_ASK_REVIEW_KEY = booleanPreferencesKey("alreadyAskReviewKey") + val LEGACY_ALREADY_ASK_REVIEW_KEY = booleanPreferencesKey("alreadyGaveReview") internal const val DATA_STORE_NAME = "AppReviewSettingsDataStore"