Repository: NMF-earth/nmf-app (language: typescript)

Issue:
Store Review
#### Summary
After 4 starts of the app, if the user has accepted terms of use, ask him to rate the app thanks to expo [store-review api](https://docs.expo.dev/versions/v38.0.0/sdk/storereview/). See more [here](https://expo.canny.io/feature-requests/p/android-in-app-review)

_NB: First to submit a valid PR, first to merge. Except if you already have merged some good code in this repo, I won’t assign any issue due to spam and beginners unable to complete a PR without a lot of help from my side._


Discussion hints:
Hello @PierreBresson! I started working on this and I have a question. Do you think that a new redux slice is needed to store the number of times the user has opened the app? Or could we just set a variable on the `userPreferences` slice? 
@EmersonPinheiro Thanks for having a look :)
Yes, you can simply set a variable in `userPreference` 👍

Pull request: Store review
✅ I have read the [contributing file](https://github.com/NMF-earth/nmf-app/blob/main/contributing.md)

## Summary

Closes #309

## Changelog

- Adds `expo-store-review`;
- Adds `timesStarted` variable, and its corresponding action and selector, in userPreferences redux slice;
- Checks if the user has started the app for the 4th time and asks for a Store Review in the `StoreReviewChecker`, which wraps the `AppNavigator` component in `App.tsx`.

## Notes
- All the implementation was done in a Windows environment and tested only on Android;
- The method `StoreReview.requestReview()` crashes the app silently when using Android Emulator **without** Play Store;
- When using Android Emulator **with** Play Store, be sure to be logged in on it, otherwise, the app will show a blank screen when calling `StoreReview.requestReview()`;
- The Play Store review modal only shows once. So, when testing, to be able to see the modal again, clean the Play Store app data in its App info on Clear Storage option (see below);
- `timesStarted` increments every time the user opens the app, but only if the user has accepted the terms of use (maybe it can be used for something else in the future);
- ~I wasn't sure about where to implement the check for review, so I've placed it in EmissionsScreen `index`. But any suggestions are welcome 😀.~

## Demo
https://user-images.githubusercontent.com/22963240/136712594-9945d39e-f63b-475d-8fb1-154112c82f1d.mp4


### Steps for clearing Play Store storage
1. Open Play Store App Info:
- By tapping and holding the Play Store app icon, then tap on App info; Or
- Go to Settings > Apps & notifications > See all apps, then search for Play Store.
2. Go to Storage & cache;
3. Clear Storage;
4. Confirm the deletion of app data.


Test command: NODE_OPTIONS='--experimental-vm-modules --max-old-space-size=4096' npx --no-install jest app/components/StoreReviewChecker/__tests__/StoreReviewChecker.test.tsx app/ducks/userPreferences/__tests__/userPreferences.actions.test.ts app/ducks/userPreferences/__tests__/userPreferences.reducer.test.ts app/ducks/userPreferences/__tests__/userPreferences.selectors.test.ts --passWithNoTests 2>&1

Fix patch:
diff --git a/App.tsx b/App.tsx
index 1d2707de..e63d7f86 100644
--- a/App.tsx
+++ b/App.tsx
@@ -13,6 +13,7 @@ import { Provider } from "react-redux";
 import { enableScreens } from "react-native-screens";
 
 import { LocalizationContext } from "utils";
+import StoreReviewChecker from "components/StoreReviewChecker";
 
 import AppNavigator from "./app/navigation/Navigator/AppNavigator";
 import store from "./app/redux/store";
@@ -98,7 +99,9 @@ const App: React.FC = () => {
               setLanguage: setLanguage,
             }}
           >
-            <AppNavigator />
+            <StoreReviewChecker>
+              <AppNavigator />
+            </StoreReviewChecker>
           </LocalizationContext.Provider>
         </FormattedProvider>
       </Provider>
diff --git a/app/components/StoreReviewChecker/StoreReviewChecker.tsx b/app/components/StoreReviewChecker/StoreReviewChecker.tsx
new file mode 100644
index 00000000..f4b84554
--- /dev/null
+++ b/app/components/StoreReviewChecker/StoreReviewChecker.tsx
@@ -0,0 +1,43 @@
+import React, { useEffect, useRef } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import * as StoreReview from "expo-store-review";
+
+import { userPreferences } from "ducks";
+import { Preferences } from "constant";
+
+type Props = {
+  children: React.ReactElement;
+};
+
+const StoreReviewChecker = ({ children }: Props): React.ReactElement => {
+  const dispatch = useDispatch();
+  const timesStarted = useSelector(userPreferences.selectors.getTimesStarted);
+  const hasAcceptedTermsOfUseVersion =
+    Preferences.currentTermsOfUseVersion ===
+    useSelector(userPreferences.selectors.getAcceptedTermsOfUseVersion);
+  const timesStartedRef = useRef<number>();
+
+  useEffect(() => {
+    timesStartedRef.current = timesStarted;
+  }, [timesStarted]);
+  const prevTimesStarted = timesStartedRef.current;
+
+  useEffect(() => {
+    if (hasAcceptedTermsOfUseVersion) {
+      dispatch(userPreferences.actions.incrementTimesStarted());
+    }
+  }, [dispatch, hasAcceptedTermsOfUseVersion]);
+
+  useEffect(() => {
+    const checkForReview = async () => {
+      if (prevTimesStarted === 3 && timesStarted === 4 && (await StoreReview.hasAction())) {
+        await StoreReview.requestReview();
+      }
+    };
+    checkForReview();
+  }, [dispatch, prevTimesStarted, timesStarted]);
+
+  return children;
+};
+
+export default StoreReviewChecker;
diff --git a/app/components/StoreReviewChecker/index.ts b/app/components/StoreReviewChecker/index.ts
new file mode 100644
index 00000000..60365d17
--- /dev/null
+++ b/app/components/StoreReviewChecker/index.ts
@@ -0,0 +1,1 @@
+export { default } from "./StoreReviewChecker";
diff --git a/app/ducks/userPreferences/userPreferences.selectors.ts b/app/ducks/userPreferences/userPreferences.selectors.ts
index 512ec64b..e0aee2e7 100644
--- a/app/ducks/userPreferences/userPreferences.selectors.ts
+++ b/app/ducks/userPreferences/userPreferences.selectors.ts
@@ -16,9 +16,12 @@ const getActivateNotifications = (state) =>
 
 const getLanguage = (state) => pathOr(currentLanguage, [namespace, "language"], state);
 
+const getTimesStarted = (state) => pathOr(0, [namespace, "timesStarted"], state);
+
 export default {
   getAcceptedTermsOfUseVersion,
   getActivateNotifications,
   getLocation,
   getLanguage,
+  getTimesStarted,
 };
diff --git a/app/ducks/userPreferences/userPreferences.slice.ts b/app/ducks/userPreferences/userPreferences.slice.ts
index 4b0212fa..db07bab3 100644
--- a/app/ducks/userPreferences/userPreferences.slice.ts
+++ b/app/ducks/userPreferences/userPreferences.slice.ts
@@ -8,6 +8,7 @@ const initialState = {
   activatedNotifications: false,
   location: ElectricityType.world,
   language: currentLanguage,
+  timesStarted: 0,
 };
 
 const userPreferences = createSlice({
@@ -26,6 +27,9 @@ const userPreferences = createSlice({
     changeLanguage(state, action: PayloadAction<string>) {
       state.language = action.payload;
     },
+    incrementTimesStarted(state) {
+      state.timesStarted += 1;
+    },
   },
 });
 
@@ -34,6 +38,7 @@ const {
   updateLocation,
   toggleNotifications,
   changeLanguage,
+  incrementTimesStarted,
 } = userPreferences.actions;
 
 export const actions = {
@@ -41,6 +46,7 @@ export const actions = {
   updateLocation,
   toggleNotifications,
   changeLanguage,
+  incrementTimesStarted,
 };
 
 export const namespace = userPreferences.name;
diff --git a/package.json b/package.json
index 014a1b63..a1674743 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,7 @@
     "expo-notifications": "0.12.3",
     "expo-sharing": "9.2.1",
     "expo-status-bar": "1.0.4",
+    "expo-store-review": "4.1.0",
     "expo-updates": "0.8.2",
     "expo-web-browser": "9.2.0",
     "i18n-js": "3.5.1",
diff --git a/yarn.lock b/yarn.lock
index 05d66264..22a83c98 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7029,6 +7029,11 @@ expo-status-bar@1.0.4:
   resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-1.0.4.tgz#d8a4c4418b5868c1606969b12bdee85b6fa7d8a4"
   integrity sha512-s7nc496D/Zn1NGiMJ5wu6HyIdXxbgGtmZZtbHm7rpbcmLdf28GmMSNHDx7M0t00BMhky7VAurTCUo+BJs8ugsw==
 
+expo-store-review@4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/expo-store-review/-/expo-store-review-4.1.0.tgz#42a9f21ad3114f0c48d8d428182dd0b8e2f2ad76"
+  integrity sha512-vIrgr26MykIyEhqf8UpqxBjO5VzSM7mpBATOLZswphQnWyAD0nL5XmVnba9kOJkFkNzzW4ur0W7/+j2nGeYO6Q==
+
 expo-structured-headers@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/expo-structured-headers/-/expo-structured-headers-1.1.1.tgz#95e28ff8686a2c06fe6ad1decdbb94cc44716683"
@@ -14547,8 +14552,10 @@ watchpack@^1.7.4:
   resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453"
   integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==
   dependencies:
+    chokidar "^3.4.1"
     graceful-fs "^4.1.2"
     neo-async "^2.5.0"
+    watchpack-chokidar2 "^2.0.1"
   optionalDependencies:
     chokidar "^3.4.1"
     watchpack-chokidar2 "^2.0.1"


Test patch:
diff --git a/app/components/StoreReviewChecker/__tests__/StoreReviewChecker.test.tsx b/app/components/StoreReviewChecker/__tests__/StoreReviewChecker.test.tsx
new file mode 100644
index 00000000..b292a2ce
--- /dev/null
+++ b/app/components/StoreReviewChecker/__tests__/StoreReviewChecker.test.tsx
@@ -0,0 +1,18 @@
+import React from "react";
+import { create } from "react-test-renderer";
+import { View } from "react-native";
+
+import StoreReviewChecker from "..";
+
+describe("StoreReviewChecker tests", () => {
+  const children = <View />;
+  it("should render correctly", () => {
+    const tree = create(<StoreReviewChecker>{children}</StoreReviewChecker>).toJSON();
+    expect(tree).toMatchSnapshot();
+  });
+
+  it("should render children correctly", () => {
+    const tree = create(<StoreReviewChecker>{children}</StoreReviewChecker>).toJSON();
+    expect(tree.type).toBe(children.type.displayName);
+  });
+});
diff --git a/app/components/StoreReviewChecker/__tests__/__snapshots__/StoreReviewChecker.test.tsx.snap b/app/components/StoreReviewChecker/__tests__/__snapshots__/StoreReviewChecker.test.tsx.snap
new file mode 100644
index 00000000..bc068dd1
--- /dev/null
+++ b/app/components/StoreReviewChecker/__tests__/__snapshots__/StoreReviewChecker.test.tsx.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`StoreReviewChecker tests should render correctly 1`] = `<View />`;
diff --git a/app/ducks/userPreferences/__tests__/__snapshots__/userPreferences.actions.test.ts.snap b/app/ducks/userPreferences/__tests__/__snapshots__/userPreferences.actions.test.ts.snap
index 9c527d77..bacf47c0 100644
--- a/app/ducks/userPreferences/__tests__/__snapshots__/userPreferences.actions.test.ts.snap
+++ b/app/ducks/userPreferences/__tests__/__snapshots__/userPreferences.actions.test.ts.snap
@@ -4,6 +4,7 @@ exports[`userPreferences actions should export expected actions 1`] = `
 Object {
   "acceptTermsOfUse": [Function],
   "changeLanguage": [Function],
+  "incrementTimesStarted": [Function],
   "toggleNotifications": [Function],
   "updateLocation": [Function],
 }
diff --git a/app/ducks/userPreferences/__tests__/userPreferences.actions.test.ts b/app/ducks/userPreferences/__tests__/userPreferences.actions.test.ts
index c08514f3..beab68ac 100644
--- a/app/ducks/userPreferences/__tests__/userPreferences.actions.test.ts
+++ b/app/ducks/userPreferences/__tests__/userPreferences.actions.test.ts
@@ -32,4 +32,11 @@ describe("userPreferences actions should", () => {
     };
     expect(userPreferences.actions.toggleNotifications(true)).toEqual(expectedAction);
   });
+
+  it("be able to increment the number of times app has started", () => {
+    const expectedAction = {
+      type: userPreferences.actions.incrementTimesStarted.toString(),
+    };
+    expect(userPreferences.actions.incrementTimesStarted()).toEqual(expectedAction);
+  });
 });
diff --git a/app/ducks/userPreferences/__tests__/userPreferences.reducer.test.ts b/app/ducks/userPreferences/__tests__/userPreferences.reducer.test.ts
index fb03c32f..f7dc85ce 100644
--- a/app/ducks/userPreferences/__tests__/userPreferences.reducer.test.ts
+++ b/app/ducks/userPreferences/__tests__/userPreferences.reducer.test.ts
@@ -13,6 +13,7 @@ describe("userPreferences reducer should", () => {
       activatedNotifications: false,
       location: ElectricityType.world,
       language: currentLanguage,
+      timesStarted: 0,
     });
   });
 
@@ -29,6 +30,7 @@ describe("userPreferences reducer should", () => {
       activatedNotifications: false,
       location: ElectricityType.world,
       language: currentLanguage,
+      timesStarted: 0,
     });
   });
 
@@ -43,6 +45,7 @@ describe("userPreferences reducer should", () => {
       activatedNotifications: false,
       location: ElectricityType.belgium,
       language: currentLanguage,
+      timesStarted: 0,
     });
   });
 
@@ -57,6 +60,7 @@ describe("userPreferences reducer should", () => {
       activatedNotifications: true,
       location: ElectricityType.world,
       language: currentLanguage,
+      timesStarted: 0,
     });
   });
 
@@ -71,6 +75,21 @@ describe("userPreferences reducer should", () => {
       activatedNotifications: false,
       location: ElectricityType.world,
       language: "fr",
+      timesStarted: 0,
+    });
+  });
+
+  it("handle increment of the number of times app has started", () => {
+    const expectedAction = {
+      type: userPreferences.actions.incrementTimesStarted.toString(),
+    };
+
+    expect(userPreferences.reducer(undefined, expectedAction)).toEqual({
+      acceptedTermsOfUseVersion: 0,
+      activatedNotifications: false,
+      location: ElectricityType.world,
+      language: currentLanguage,
+      timesStarted: 1,
     });
   });
 });
diff --git a/app/ducks/userPreferences/__tests__/userPreferences.selectors.test.ts b/app/ducks/userPreferences/__tests__/userPreferences.selectors.test.ts
index 436151b3..9292c3ea 100644
--- a/app/ducks/userPreferences/__tests__/userPreferences.selectors.test.ts
+++ b/app/ducks/userPreferences/__tests__/userPreferences.selectors.test.ts
@@ -11,6 +11,7 @@ const initialState = {
   activatedNotifications: false,
   location: ElectricityType.world,
   language: supportedLanguages.en,
+  timesStarted: 4,
 };
 
 describe("userPreferences selector should", () => {
@@ -35,4 +36,7 @@ describe("userPreferences selector should", () => {
 
   test("return current language", () =>
     expect(userPreferences.selectors.getLanguage(state)).toEqual(initialState.language));
+
+  test("return current timesStarted", () =>
+    expect(userPreferences.selectors.getTimesStarted(state)).toEqual(initialState.timesStarted));
 });