Repository: wordpress-mobile/WordPress-Android (language: kotlin)

Issue:
Add an in-app review prompt
Google recently released a [new In-App Review API for Google Play reviews](https://android-developers.googleblog.com/2020/08/in-app-review-api.html). From their [developer docs](https://developer.android.com/guide/playcore/in-app-review):

> The Google Play In-App Review API lets you prompt users to submit Play Store ratings and reviews without the inconvenience of leaving your app or game.
>
> Generally, the in-app review flow can be triggered at any time throughout the user journey of your app. During the flow, the user has the ability to rate your app using the 1 to 5 star system and to add an optional comment. Once submitted, the review is sent to the Play Store and eventually displayed.

Naturally, there are some restrictions to consider:

> To protect user privacy and avoid API misuse, there are strict guidelines that your app should follow about when to request in-app reviews and the design of the review prompt.

Let's figure out how to integrate this into WPAndroid!

/cc @mattmiklic @frosty @yaelirub 


Discussion hints:
This is cool! I'd suggest making the prompt appear after user successfully publishes a new post or page.
We already implement the existing app rating system – @khaykov, would you be able to take a quick look and see how different this is from what we use already?
@frosty It is pretty different. Currently, we just show a dialog that takes user to the Play Store where he has to write a review. The new review prompt and API are superior, and we will probably get more reviews from them since you can do it without leaving an app. I checked the guidelines and we are in a clear with our current and new flow.

We can also integrate it with a current logic that determines when to show it.
In terms of design, could we re-use the existing prompt to review, but have it lead to this new in-app UI instead of linking out to the Play Store?
@mattmiklic No, If I understand the guidelines correctly:

> Your app should not ask the user any questions before or while presenting the rating button or card, including questions about their opinion (such as “Do you like the app?”) or predictive questions (such as “Would you rate this app 5 stars”).
Ahh ok, thanks for clarifying that. I had a chance to look through the documentation and it sounds like we'd be required to figure out situations in which it would feel natural and unobstructive to make this modal dialog appear. 

I think this requires greater sensitivity than our current prompt in Notifications (are there more?) because there will be no ability to present the option to review the app in-line with what they're already doing. The only way to trigger this is in a modal sheet, which if timed incorrectly, could be annoying to the user and potentially lead to a bad review.

So I think we should take a little time to think about when we'd want to show the user this kind of sheet. After publishing is just one idea, but I'm sure there may be more.

Pull request: In-app reviews
Fixes #12605 

This adds [in-app reviews feature](https://developer.android.com/guide/playcore/in-app-review). The prompt will display after the second successful publishing (post or story). It must be a new post, and editing doesn't count.  

https://github.com/wordpress-mobile/WordPress-Android/assets/2471769/4914894f-3819-4a89-96ed-a7af502611fe

-----

## To Test:

<!-- Test instructions per dependency update: https://github.com/wordpress-mobile/WordPress-Android/blob/trunk/docs/test_instructions_per_dependency_update.md -->

Testing this feature is not available within the PR. It will be tested outside of the PR.
Internal link: p1702317430036659-slack-C441E3YTS

1. Install the app from the link.
2. Log in.
3. Publish a post or story. 
4. Verify that the blogging reminder is displayed.
5. Publish another post or story.
6. Verify that the in-app previews prompt is displayed. (Submitting the review may not be available due to the testing environment.)

-----

## Regression Notes

1. Potential unintended areas of impact

    - None

7. What I did to test those areas of impact (or what existing automated tests I relied on)

    - Nothing

8. What automated tests I added (or what prevented me from doing so)

    - `ReviewViewModelTest`

-----

## PR Submission Checklist:

- [x] I have completed the Regression Notes.
- [x] I have considered adding accessibility improvements for my changes.
- [x] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

-----

## UI Changes Testing Checklist:

- [x] Portrait and landscape orientations.
- [x] Light and dark modes.
- [x] Fonts: Larger, smaller and bold text.
- [x] High contrast.
- [x] Talkback.
- [x] Languages with large words or with letters/accents not frequently used in English.
- [x] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [x] Large and small screen sizes. (Tablet and smaller phones)
- [x] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)

Test command: ./gradlew :WordPress:testWordpressVanillaDebugUnitTest --tests "org.wordpress.android.ui.review.ReviewViewModelTest"

Fix patch:
diff --git a/WordPress/build.gradle b/WordPress/build.gradle
index a982ada743d9..fef136d1799e 100644
--- a/WordPress/build.gradle
+++ b/WordPress/build.gradle
@@ -421,6 +421,8 @@ dependencies {
     implementation "androidx.lifecycle:lifecycle-livedata-ktx:$androidxLifecycleVersion"
     implementation "androidx.lifecycle:lifecycle-process:$androidxLifecycleVersion"
     implementation "com.android.volley:volley:$androidVolleyVersion"
+    implementation "com.google.android.play:review:$googlePlayReviewVersion"
+    implementation "com.google.android.play:review-ktx:$googlePlayReviewVersion"
     implementation "com.google.android.gms:play-services-auth:$googlePlayServicesAuthVersion"
     implementation "com.google.android.gms:play-services-code-scanner:$googlePlayServicesCodeScannerVersion"
     implementation "com.google.mlkit:barcode-scanning-common:$googleMLKitBarcodeScanningVersion"
diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java
index 510ce7ee3119..e759a219bb18 100644
--- a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java
+++ b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java
@@ -33,13 +33,13 @@
 import org.wordpress.android.ui.posts.EditorBloggingPromptsViewModel;
 import org.wordpress.android.ui.posts.EditorJetpackSocialViewModel;
 import org.wordpress.android.ui.posts.PostListMainViewModel;
-import org.wordpress.android.ui.posts.prepublishing.categories.addcategory.PrepublishingAddCategoryViewModel;
+import org.wordpress.android.ui.posts.editor.StorePostViewModel;
+import org.wordpress.android.ui.posts.prepublishing.PrepublishingViewModel;
 import org.wordpress.android.ui.posts.prepublishing.categories.PrepublishingCategoriesViewModel;
+import org.wordpress.android.ui.posts.prepublishing.categories.addcategory.PrepublishingAddCategoryViewModel;
 import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeViewModel;
-import org.wordpress.android.ui.posts.prepublishing.tags.PrepublishingTagsViewModel;
-import org.wordpress.android.ui.posts.prepublishing.PrepublishingViewModel;
-import org.wordpress.android.ui.posts.editor.StorePostViewModel;
 import org.wordpress.android.ui.posts.prepublishing.publishsettings.PrepublishingPublishSettingsViewModel;
+import org.wordpress.android.ui.posts.prepublishing.tags.PrepublishingTagsViewModel;
 import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsViewModel;
 import org.wordpress.android.ui.prefs.homepage.HomepageSettingsViewModel;
 import org.wordpress.android.ui.prefs.language.LocalePickerViewModel;
@@ -53,6 +53,7 @@
 import org.wordpress.android.ui.reader.viewmodels.ReaderPostListViewModel;
 import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel;
 import org.wordpress.android.ui.reader.viewmodels.SubfilterPageViewModel;
+import org.wordpress.android.ui.review.ReviewViewModel;
 import org.wordpress.android.ui.stats.refresh.lists.DaysListViewModel;
 import org.wordpress.android.ui.stats.refresh.lists.InsightsDetailListViewModel;
 import org.wordpress.android.ui.stats.refresh.lists.InsightsListViewModel;
@@ -465,6 +466,11 @@ abstract class ViewModelModule {
     @ViewModelKey(UnifiedCommentListViewModel.class)
     abstract ViewModel unifiedCommentListViewModel(UnifiedCommentListViewModel viewModel);
 
+    @Binds
+    @IntoMap
+    @ViewModelKey(ReviewViewModel.class)
+    abstract ViewModel reviewViewModel(ReviewViewModel viewModel);
+
     @Binds
     @IntoMap
     @ViewModelKey(BloggingRemindersViewModel.class)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java
index 9cd30bdad3df..b7cac907852d 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java
@@ -27,7 +27,13 @@
 
 import com.google.android.gms.common.ConnectionResult;
 import com.google.android.gms.common.GoogleApiAvailability;
+import com.google.android.gms.tasks.Task;
 import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.google.android.play.core.review.ReviewException;
+import com.google.android.play.core.review.ReviewInfo;
+import com.google.android.play.core.review.ReviewManager;
+import com.google.android.play.core.review.ReviewManagerFactory;
+import com.google.android.play.core.review.model.ReviewErrorCode;
 
 import org.greenrobot.eventbus.EventBus;
 import org.greenrobot.eventbus.Subscribe;
@@ -95,11 +101,11 @@
 import org.wordpress.android.ui.main.WPMainNavigationView.OnPageListener;
 import org.wordpress.android.ui.main.WPMainNavigationView.PageType;
 import org.wordpress.android.ui.mlp.ModalLayoutPickerFragment;
+import org.wordpress.android.ui.mysite.BloggingPromptsOnboardingListener;
 import org.wordpress.android.ui.mysite.MySiteFragment;
 import org.wordpress.android.ui.mysite.MySiteViewModel;
 import org.wordpress.android.ui.mysite.SelectedSiteRepository;
 import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository;
-import org.wordpress.android.ui.mysite.BloggingPromptsOnboardingListener;
 import org.wordpress.android.ui.notifications.NotificationEvents;
 import org.wordpress.android.ui.notifications.NotificationsListFragment;
 import org.wordpress.android.ui.notifications.SystemNotificationsTracker;
@@ -125,6 +131,7 @@
 import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic.UpdateTask;
 import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter;
 import org.wordpress.android.ui.reader.tracker.ReaderTracker;
+import org.wordpress.android.ui.review.ReviewViewModel;
 import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource;
 import org.wordpress.android.ui.stats.StatsTimeframe;
 import org.wordpress.android.ui.stories.intro.StoriesIntroDialogFragment;
@@ -237,6 +244,7 @@ public class WPMainActivity extends LocaleAwareActivity implements
 
     private WPMainActivityViewModel mViewModel;
     private ModalLayoutPickerViewModel mMLPViewModel;
+    @NonNull private ReviewViewModel mReviewViewModel;
     private BloggingRemindersViewModel mBloggingRemindersViewModel;
     private FloatingActionButton mFloatingActionButton;
     private static final String MAIN_BOTTOM_SHEET_TAG = "MAIN_BOTTOM_SHEET_TAG";
@@ -654,6 +662,7 @@ private void initViewModel() {
 
         mViewModel = new ViewModelProvider(this, mViewModelFactory).get(WPMainActivityViewModel.class);
         mMLPViewModel = new ViewModelProvider(this, mViewModelFactory).get(ModalLayoutPickerViewModel.class);
+        mReviewViewModel = new ViewModelProvider(this, mViewModelFactory).get(ReviewViewModel.class);
         mBloggingRemindersViewModel =
                 new ViewModelProvider(this, mViewModelFactory).get(BloggingRemindersViewModel.class);
 
@@ -752,6 +761,11 @@ private void initViewModel() {
             });
         });
 
+        mReviewViewModel.getLaunchReview().observe(this, event -> event.applyIfNotHandled(unit -> {
+            launchInAppReviews();
+            return null;
+        }));
+
         BloggingReminderUtils.observeBottomSheet(
                 mBloggingRemindersViewModel.isBottomSheetShowing(),
                 this,
@@ -827,6 +841,25 @@ private void triggerCreatePageFlow(ActionType actionType) {
         }
     }
 
+    private void launchInAppReviews() {
+        ReviewManager manager = ReviewManagerFactory.create(this);
+        Task<ReviewInfo> request = manager.requestReviewFlow();
+        request.addOnCompleteListener(task -> {
+            if (task.isSuccessful()) {
+                ReviewInfo reviewInfo = task.getResult();
+                Task<Void> flow = manager.launchReviewFlow(this, reviewInfo);
+                flow.addOnFailureListener(e -> AppLog.e(T.MAIN, "Error launching google review API flow.", e));
+            } else {
+                @ReviewErrorCode int reviewErrorCode = ((ReviewException) task.getException()).getErrorCode();
+                AppLog.e(
+                        T.MAIN,
+                        "Error fetching ReviewInfo object from Review API to start in-app review process",
+                        reviewErrorCode
+                );
+            }
+        });
+    }
+
     private CreatePageDashboardSource getCreatePageDashboardSourceFromActionType(ActionType actionType) {
         if (actionType == ActionType.CREATE_NEW_PAGE_FROM_PAGES_CARD) {
             return CreatePageDashboardSource.PAGES_CARD;
@@ -1336,14 +1369,11 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
                             post,
                             site,
                             mUploadActionUseCase.getUploadAction(post),
-                            new View.OnClickListener() {
-                                @Override
-                                public void onClick(View v) {
-                                    UploadUtils.publishPost(WPMainActivity.this, post, site, mDispatcher);
-                                }
-                            },
-                            isFirstTimePublishing -> mBloggingRemindersViewModel
-                                    .onPublishingPost(site.getId(), isFirstTimePublishing)
+                            v -> UploadUtils.publishPost(WPMainActivity.this, post, site, mDispatcher),
+                            isFirstTimePublishing -> {
+                                mBloggingRemindersViewModel.onPublishingPost(site.getId(), isFirstTimePublishing);
+                                mReviewViewModel.onPublishingPost(isFirstTimePublishing);
+                            }
                     );
                 }
                 break;
@@ -1355,6 +1385,7 @@ public void onClick(View v) {
                             selectedSite.getId(),
                             isNewStory
                     );
+                    mReviewViewModel.onPublishingPost(isNewStory);
                 }
                 break;
             case RequestCodes.CREATE_SITE:
@@ -1741,8 +1772,10 @@ public void onPostUploaded(OnPostUploaded event) {
                         event.post,
                         null,
                         targetSite,
-                        isFirstTimePublishing -> mBloggingRemindersViewModel
-                                .onPublishingPost(targetSite.getId(), isFirstTimePublishing)
+                        isFirstTimePublishing -> {
+                            mBloggingRemindersViewModel.onPublishingPost(targetSite.getId(), isFirstTimePublishing);
+                            mReviewViewModel.onPublishingPost(isFirstTimePublishing);
+                        }
                 );
             }
         }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt
index bfb562c03fbe..007fa44e5b05 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt
@@ -21,6 +21,9 @@ import androidx.appcompat.widget.Toolbar
 import androidx.lifecycle.ViewModelProvider
 import androidx.viewpager.widget.ViewPager.OnPageChangeListener
 import com.google.android.material.snackbar.Snackbar
+import com.google.android.play.core.review.ReviewException
+import com.google.android.play.core.review.ReviewManagerFactory
+import com.google.android.play.core.review.model.ReviewErrorCode
 import org.wordpress.android.R
 import org.wordpress.android.WordPress
 import org.wordpress.android.databinding.PostListActivityBinding
@@ -53,6 +56,7 @@ import org.wordpress.android.ui.posts.prepublishing.PrepublishingBottomSheetFrag
 import org.wordpress.android.ui.posts.prepublishing.PrepublishingBottomSheetFragment.Companion.newInstance
 import org.wordpress.android.ui.posts.prepublishing.home.PublishPost
 import org.wordpress.android.ui.posts.prepublishing.listeners.PrepublishingBottomSheetListener
+import org.wordpress.android.ui.review.ReviewViewModel
 import org.wordpress.android.ui.stories.StoriesMediaPickerResultHandler
 import org.wordpress.android.ui.uploads.UploadActionUseCase
 import org.wordpress.android.ui.uploads.UploadUtilsWrapper
@@ -124,6 +128,9 @@ class PostsListActivity : LocaleAwareActivity(),
     @Inject
     internal lateinit var bloggingRemindersViewModel: BloggingRemindersViewModel
 
+    @Inject
+    internal lateinit var reviewViewModel: ReviewViewModel
+
     @Inject
     internal lateinit var blazeFeatureUtils: BlazeFeatureUtils
 
@@ -209,6 +216,7 @@ class PostsListActivity : LocaleAwareActivity(),
             setupContent()
             initViewModel(initPreviewState, currentBottomSheetPostId)
             initBloggingReminders()
+            initInAppReviews()
             initTabLayout(tabIndex)
             loadIntentData(intent)
         }
@@ -336,6 +344,32 @@ class PostsListActivity : LocaleAwareActivity(),
         }
     }
 
+    private fun initInAppReviews() {
+        reviewViewModel = ViewModelProvider(this@PostsListActivity, viewModelFactory)[ReviewViewModel::class.java]
+        reviewViewModel.launchReview.observeEvent(this) { launchInAppReviews() }
+    }
+
+    private fun launchInAppReviews() {
+        val manager = ReviewManagerFactory.create(this)
+        val request = manager.requestReviewFlow()
+        request.addOnCompleteListener { task ->
+            if (task.isSuccessful) {
+                val reviewInfo = task.result
+                val flow = manager.launchReviewFlow(this, reviewInfo)
+                flow.addOnFailureListener { e ->
+                    AppLog.e(AppLog.T.POSTS, "Error launching google review API flow.", e)
+                }
+            } else {
+                @ReviewErrorCode val reviewErrorCode = (task.exception as ReviewException).errorCode
+                AppLog.e(
+                    AppLog.T.POSTS,
+                    "Error fetching ReviewInfo object from Review API to start in-app review process",
+                    reviewErrorCode
+                )
+            }
+        }
+    }
+
     private fun setupActions() {
         viewModel.dialogAction.observe(this@PostsListActivity) {
             it?.show(this@PostsListActivity, supportFragmentManager, uiHelpers)
@@ -350,6 +384,7 @@ class PostsListActivity : LocaleAwareActivity(),
                     uploadUtilsWrapper
                 ) { isFirstTimePublishing ->
                     bloggingRemindersViewModel.onPublishingPost(site.id, isFirstTimePublishing)
+                    reviewViewModel.onPublishingPost(isFirstTimePublishing)
                 }
             }
         }
@@ -458,6 +493,7 @@ class PostsListActivity : LocaleAwareActivity(),
                     site.id,
                     isNewStory
                 )
+                reviewViewModel.onPublishingPost(isNewStory)
             }
         }
     }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
index ebbc64b69f60..672f98cb5085 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
@@ -171,6 +171,9 @@ public enum DeletablePrefKey implements PrefKey {
         SITE_JETPACK_CAPABILITIES,
         REMOVED_QUICK_START_CARD_TYPE,
         PINNED_DYNAMIC_CARD,
+        // PUBLISHED_POST_COUNT will increase until it reaches ReviewViewModel.TARGET_COUNT_POST_PUBLISHED
+        PUBLISHED_POST_COUNT,
+        IN_APP_REVIEW_SHOWN,
         BLOGGING_REMINDERS_SHOWN,
         SHOULD_SCHEDULE_CREATE_SITE_NOTIFICATION,
         SHOULD_SHOW_WEEKLY_ROUNDUP_NOTIFICATION,
@@ -1364,6 +1367,22 @@ public static boolean hasManualFeatureConfig(String featureKey) {
         return DeletablePrefKey.MANUAL_FEATURE_CONFIG.name() + featureKey;
     }
 
+    public static void incrementPublishedPostCount() {
+        putInt(DeletablePrefKey.PUBLISHED_POST_COUNT, getPublishedPostCount() + 1);
+    }
+
+    public static int getPublishedPostCount() {
+        return prefs().getInt(DeletablePrefKey.PUBLISHED_POST_COUNT.name(), 0);
+    }
+
+    public static void setInAppReviewsShown() {
+        putBoolean(DeletablePrefKey.IN_APP_REVIEW_SHOWN, true);
+    }
+
+    public static boolean isInAppReviewsShown() {
+        return prefs().getBoolean(DeletablePrefKey.IN_APP_REVIEW_SHOWN.name(), false);
+    }
+
     public static void setBloggingRemindersShown(int siteId) {
         prefs().edit().putBoolean(getBloggingRemindersConfigKey(siteId), true).apply();
     }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt
index 6a02a0c17915..78357a4280bd 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt
@@ -183,6 +183,22 @@ class AppPrefsWrapper @Inject constructor() {
         return AppPrefs.getManualFeatureConfig(featureKey)
     }
 
+    fun incrementPublishedPostCount() {
+        AppPrefs.incrementPublishedPostCount()
+    }
+
+    fun getPublishedPostCount(): Int {
+        return AppPrefs.getPublishedPostCount()
+    }
+
+    fun setInAppReviewsShown() {
+        AppPrefs.setInAppReviewsShown()
+    }
+
+    fun isInAppReviewsShown(): Boolean {
+        return AppPrefs.isInAppReviewsShown()
+    }
+
     fun setBloggingRemindersShown(siteId: Int) {
         AppPrefs.setBloggingRemindersShown(siteId)
     }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt
new file mode 100644
index 000000000000..579f97bc187f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt
@@ -0,0 +1,35 @@
+package org.wordpress.android.ui.review
+
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import org.wordpress.android.ui.prefs.AppPrefsWrapper
+import org.wordpress.android.util.config.InAppReviewsFeatureConfig
+import org.wordpress.android.viewmodel.Event
+import javax.inject.Inject
+
+class ReviewViewModel @Inject constructor(
+    private val appPrefsWrapper: AppPrefsWrapper,
+    private val inAppReviewsFeatureConfig: InAppReviewsFeatureConfig
+) : ViewModel() {
+    private val _launchReview = MutableLiveData<Event<Unit>>()
+    val launchReview = _launchReview as LiveData<Event<Unit>>
+
+    fun onPublishingPost(isFirstTimePublishing: Boolean) {
+        if (inAppReviewsFeatureConfig.isEnabled() && !appPrefsWrapper.isInAppReviewsShown() && isFirstTimePublishing) {
+            if (appPrefsWrapper.getPublishedPostCount() < TARGET_COUNT_POST_PUBLISHED) {
+                appPrefsWrapper.incrementPublishedPostCount()
+            }
+            if (appPrefsWrapper.getPublishedPostCount() == TARGET_COUNT_POST_PUBLISHED) {
+                _launchReview.value = Event(Unit)
+                appPrefsWrapper.setInAppReviewsShown()
+            }
+        }
+    }
+
+    companion object {
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val TARGET_COUNT_POST_PUBLISHED = 2
+    }
+}
diff --git a/build.gradle b/build.gradle
index 44d6b2f96300..143c62272b1d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -70,6 +70,7 @@ ext {
     googleMaterialVersion = '1.9.0'
 
     googleMLKitBarcodeScanningVersion = '17.0.0'
+    googlePlayReviewVersion = '2.0.1'
     googlePlayServicesAuthVersion = '20.4.1'
     googlePlayServicesCodeScannerVersion = '16.0.0-beta3'
     jsoupVersion = '1.16.2'


Test patch:
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/review/ReviewViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/review/ReviewViewModelTest.kt
new file mode 100644
index 000000000000..be459d325fe5
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/review/ReviewViewModelTest.kt
@@ -0,0 +1,69 @@
+package org.wordpress.android.ui.review
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnitRunner
+import org.mockito.kotlin.whenever
+import org.wordpress.android.eventToList
+import org.wordpress.android.ui.prefs.AppPrefsWrapper
+import org.wordpress.android.util.config.InAppReviewsFeatureConfig
+import kotlin.test.assertEquals
+
+@RunWith(MockitoJUnitRunner::class)
+class ReviewViewModelTest {
+    @Rule
+    @JvmField
+    val rule = InstantTaskExecutorRule()
+
+    @Mock
+    lateinit var inAppReviewsFeatureConfig: InAppReviewsFeatureConfig
+
+    @Mock
+    lateinit var appPrefsWrapper: AppPrefsWrapper
+
+    private lateinit var viewModel: ReviewViewModel
+
+    private lateinit var events: MutableList<Unit>
+
+    @Before
+    fun setup() {
+        whenever(inAppReviewsFeatureConfig.isEnabled()).thenReturn(true)
+        viewModel = ReviewViewModel(appPrefsWrapper, inAppReviewsFeatureConfig)
+        events = mutableListOf()
+        events = viewModel.launchReview.eventToList()
+    }
+
+    @Test
+    fun onPublishingPost_whenPublishedCountIsLow_doNotLaunchInAppReviews() {
+        whenever(appPrefsWrapper.isInAppReviewsShown()).thenReturn(false)
+        whenever(appPrefsWrapper.getPublishedPostCount()).thenReturn(ReviewViewModel.TARGET_COUNT_POST_PUBLISHED - 1)
+
+        viewModel.onPublishingPost(true)
+
+        assertEquals(events.size, 0)
+    }
+
+    @Test
+    fun onPublishingPost_whenInAppReviewsAlreadyShown_doNotLaunchInAppReviews() {
+        whenever(appPrefsWrapper.isInAppReviewsShown()).thenReturn(true)
+
+        viewModel.onPublishingPost(true)
+
+        assertEquals(events.size, 0)
+    }
+
+    @Test
+    fun onPublishingPost_whenPublishedCountIsHigh_launchInAppReviews() {
+        whenever(appPrefsWrapper.isInAppReviewsShown()).thenReturn(false)
+        whenever(appPrefsWrapper.getPublishedPostCount()).thenReturn(ReviewViewModel.TARGET_COUNT_POST_PUBLISHED)
+
+        viewModel.onPublishingPost(true)
+
+        // Verify `launchReview` is triggered.
+        assertEquals(Unit, events.last())
+    }
+}
diff --git a/docs/test_instructions_per_dependency_update.md b/docs/test_instructions_per_dependency_update.md
index 9617747e26d6..60ef32eec1cc 100644
--- a/docs/test_instructions_per_dependency_update.md
+++ b/docs/test_instructions_per_dependency_update.md
@@ -54,6 +54,7 @@ rather than strict requirements.
     3. [MLKitBarcodeScanning](#mlkitbarcodescanning)
     4. [PlayServicesAuth](#playservicesauth)
     5. [PlayServicesCoreScanner](#playservicescodescanner)
+    6. [PlayReview](#playreview)
 5. Network
     1. [Okio](#okio)
 6. Tool
@@ -421,6 +422,19 @@ Step.3:
 
 -----
 
+### PlayReview [[googlePlayReviewVersion](https://github.com/wordpress-mobile/WordPress-Android/blob/trunk/build.gradle)] <a name="playreview"></a>
+
+<details>
+    <summary>1. In app reviews</summary>
+
+- Perform a clean install.
+- Publish three (`ReviewViewModel.TARGET_COUNT_POST_PUBLISHED + 1`) new posts or stories.
+- Verify that there are no crashes.
+
+</details>
+
+-----
+
 ### Okio [[squareupOkioVersion](https://github.com/wordpress-mobile/WordPress-Android/blob/trunk/build.gradle)] <a name="okio"></a>
 
 <details>