From 9c1e52a216db6b153669347f5446c0edf05b824d Mon Sep 17 00:00:00 2001 From: Arnaud AMBROSELLI Date: Thu, 12 May 2022 15:32:13 +0200 Subject: [PATCH] feat: migrate from asyncstorager to mmkv --- App.js | 25 ++++++++- ios/Podfile.lock | 14 +++++ package.json | 2 +- src/Router.js | 5 +- src/components/Quizz/index.js | 6 +-- src/components/Quizz/utils.js | 10 ++-- src/hooks/useStateWithAsyncStorage.js | 37 ------------- src/redux/persistConfig.js | 6 +-- src/scenes/AddDrink/BarCodeReader.js | 6 +-- src/scenes/ConsoFollowUp/Diagram.js | 22 ++++---- src/scenes/Defis/Defi7Days/Defi7Days.js | 10 ++-- src/scenes/Defis/Defi7Days/Onboarding.js | 4 +- src/scenes/Defis/Defi7Days/utils.js | 6 +-- src/scenes/Defis/TopTimeline.js | 4 +- src/scenes/Gains/Estimation.js | 4 +- src/scenes/Gains/GainsCalendar.js | 6 +-- src/scenes/Gains/MyGains.js | 4 +- src/scenes/Gains/MyGoal.js | 4 +- src/scenes/Gains/recoil.js | 37 +++++++------ src/scenes/Infos/Reminder.js | 8 +-- src/scenes/NPS/NPS.js | 20 +++---- src/scenes/Quizzs/QuizzEvaluateConso/utils.js | 4 +- src/scenes/Quizzs/QuizzMotivations/index.js | 6 +-- src/scenes/Quizzs/QuizzOnboarding/utils.js | 4 +- src/scenes/WelcomeScreen/WelcomeScreen.js | 4 +- src/services/matomo/index.js | 12 ++--- src/services/storage.js | 54 +++++++++++++++++++ yarn.lock | 5 ++ 28 files changed, 195 insertions(+), 134 deletions(-) delete mode 100644 src/hooks/useStateWithAsyncStorage.js create mode 100644 src/services/storage.js diff --git a/App.js b/App.js index d43daa936..50726fb03 100644 --- a/App.js +++ b/App.js @@ -1,8 +1,10 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import * as Sentry from '@sentry/react-native'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { Provider } from 'react-redux'; +import { RecoilRoot } from 'recoil'; import { PersistGate } from 'redux-persist/integration/react'; +import { InteractionManager } from 'react-native'; import { persistor, store } from './src/redux/store'; import Router from './src/Router'; import './src/services/polyfills'; @@ -10,13 +12,32 @@ import './src/services/polyfills'; import { SENTRY_XXX } from './src/config'; import { ToastProvider } from './src/services/toast'; import './src/styles/theme'; -import { RecoilRoot } from 'recoil'; +import { hasMigratedFromAsyncStorage, migrateFromAsyncStorage } from './src/services/storage'; if (!__DEV__) { Sentry.init({ dsn: SENTRY_XXX }); } const App = () => { + // TODO: Remove `hasMigratedFromAsyncStorage` after a while (when everyone has migrated) + const [hasMigrated, setHasMigrated] = useState(hasMigratedFromAsyncStorage); + + useEffect(() => { + if (!hasMigratedFromAsyncStorage) { + InteractionManager.runAfterInteractions(async () => { + try { + await migrateFromAsyncStorage(); + setHasMigrated(true); + } catch (e) { + // TODO: fall back to AsyncStorage? Wipe storage clean and use MMKV? Crash app? + } + }); + } + }, []); + + console.log({ hasMigrated }); + if (!hasMigrated) return null; + return ( diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ac8c4167a..f6ed73b59 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -73,6 +73,9 @@ PODS: - fmt (6.2.1) - glog (0.3.5) - libevent (2.1.12) + - MMKV (1.2.13): + - MMKVCore (~> 1.2.13) + - MMKVCore (1.2.13) - OpenSSL-Universal (1.1.180) - Permission-Camera (3.3.1): - RNPermissions @@ -287,6 +290,9 @@ PODS: - react-native-config/App (= 1.4.5) - react-native-config/App (1.4.5): - React-Core + - react-native-mmkv (2.4.1): + - MMKV (>= 1.2.13) + - React-Core - react-native-netinfo (5.9.10): - React-Core - react-native-safe-area-context (3.4.1): @@ -463,6 +469,7 @@ DEPENDENCIES: - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - react-native-camera (from `../node_modules/react-native-camera`) - react-native-config (from `../node_modules/react-native-config`) + - react-native-mmkv (from `../node_modules/react-native-mmkv`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-webview (from `../node_modules/react-native-webview`) @@ -508,6 +515,8 @@ SPEC REPOS: - FlipperKit - fmt - libevent + - MMKV + - MMKVCore - OpenSSL-Universal - Sentry - YogaKit @@ -553,6 +562,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-camera" react-native-config: :path: "../node_modules/react-native-config" + react-native-mmkv: + :path: "../node_modules/react-native-mmkv" react-native-netinfo: :path: "../node_modules/@react-native-community/netinfo" react-native-safe-area-context: @@ -630,6 +641,8 @@ SPEC CHECKSUMS: fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 5337263514dd6f09803962437687240c5dc39aa4 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 + MMKV: aac95d817a100479445633f2b3ed8961b4ac5043 + MMKVCore: 3388952ded307e41b3ed8a05892736a236ed1b8e OpenSSL-Universal: 1aa4f6a6ee7256b83db99ec1ccdaa80d10f9af9b Permission-Camera: bae27a8503530770c35aadfecbb97ec71823382a Permission-Notifications: 4b21cfdd5e8aab2cbb1f117aaa17b44e1f5736c4 @@ -647,6 +660,7 @@ SPEC CHECKSUMS: React-jsinspector: 41e58e5b8e3e0bf061fdf725b03f2144014a8fb0 react-native-camera: b8cc03e2feec0c04403d0998e37cf519d8fd4c6f react-native-config: 6502b1879f97ed5ac570a029961fc35ea606cd14 + react-native-mmkv: b5c7f9bc369eef2b8a2aa36e8a15949989fa823f react-native-netinfo: 30fb89fa913c342be82a887b56e96be6d71201dd react-native-safe-area-context: 9e40fb181dac02619414ba1294d6c2a807056ab9 react-native-webview: 0aa2cde4ee7e3e1c5fffdf64dbce9c709aa18155 diff --git a/package.json b/package.json index 56acbcf80..62af40948 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "react-native-config": "^1.4.3", "react-native-device-info": "^5.5.3", "react-native-gesture-handler": "^1.10.3", + "react-native-mmkv": "^2.4.1", "react-native-permissions": "^3.0.5", "react-native-push-notification": "^7.4.0", "react-native-reanimated": "2.3.0-alpha.2", @@ -81,7 +82,6 @@ "jest": { "preset": "react-native", "setupFiles": [ - "./__mocks__/AsyncStorage.js", "./__mocks__/Sentry.js" ], "forceCoverageMatch": [ diff --git a/src/Router.js b/src/Router.js index e247b1d3f..dcd1002ad 100644 --- a/src/Router.js +++ b/src/Router.js @@ -1,5 +1,4 @@ import React, { Component } from 'react'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { NavigationContainer } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; @@ -22,6 +21,7 @@ import WelcomeScreen from './scenes/WelcomeScreen/WelcomeScreen'; import AppStateHandler from './services/AppStateHandler'; import matomo from './services/matomo'; import NotificationService from './services/notifications'; +import { storage } from './services/storage'; const Tabs = createBottomTabNavigator(); const TabsNavigator = ({ navigation }) => { @@ -111,8 +111,7 @@ class Router extends Component { initView = async () => { await matomo.initMatomo(); await matomo.logAppVisit('initApp'); - // await AsyncStorage.clear(); - const onBoardingDone = await AsyncStorage.getItem('@OnboardingDoneWithCGU'); + const onBoardingDone = storage.getBoolean('@OnboardingDoneWithCGU'); if (!onBoardingDone) return this.setState({ initialRouteName: 'WELCOME' }); return this.setState({ initialRouteName: 'TABS' }); }; diff --git a/src/components/Quizz/index.js b/src/components/Quizz/index.js index 3d0eff827..0eb45c027 100644 --- a/src/components/Quizz/index.js +++ b/src/components/Quizz/index.js @@ -1,5 +1,4 @@ import React, { useEffect, useState } from 'react'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import { createStackNavigator } from '@react-navigation/stack'; import styled from 'styled-components'; import Background from '../Background'; @@ -7,6 +6,7 @@ import ProgressBar from '../ProgressBar'; import UnderlinedButton from '../UnderlinedButton'; import Question from './Question'; import { fetchStoredAnswers } from './utils'; +import { storage } from '../../services/storage'; /* HOW DOES THE QUESTIONS WORK: @@ -44,14 +44,14 @@ const Quizz = ({ memoryKeyAnswers, memoryKeyResult, questions, route, mapAnswers const endOfQuestions = questionIndex === questions.length - 1; // await matomo.logQuizzAnswer({ questionKey, answerKey, score }); - await AsyncStorage.setItem(memoryKeyAnswers, JSON.stringify(newAnswers)); + storage.set(memoryKeyAnswers, JSON.stringify(newAnswers)); if (endOfQuestions) { const addictionResult = mapAnswersToResult(questions, newAnswers); // await matomo.logAddictionResult(addictionResult); // await matomo.logQuizzFinish(); if (addictionResult) { - await AsyncStorage.setItem(memoryKeyResult, JSON.stringify(addictionResult)); + storage.set(memoryKeyResult, JSON.stringify(addictionResult)); } setState({ resultKey: addictionResult }); } diff --git a/src/components/Quizz/utils.js b/src/components/Quizz/utils.js index 4cd1f977b..ccf797a96 100644 --- a/src/components/Quizz/utils.js +++ b/src/components/Quizz/utils.js @@ -1,5 +1,5 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; import { capture } from '../../services/sentry'; +import { storage } from '../../services/storage'; // Utils export const findQuestion = (questions, questionKey) => @@ -9,8 +9,8 @@ export const getAnswerScore = (questions, answers, questionKey) => findAnswer(findQuestion(questions, questionKey), answers[questionKey])?.score; export const getGenderFromLocalStorage = async () => { - const storedAnswers = await AsyncStorage.getItem('@Quizz_answers'); - if (storedAnswers !== null) { + const storedAnswers = storage.getString('@Quizz_answers'); + if (typeof storedAnswers === 'string') { const newAnswers = JSON.parse(storedAnswers); return newAnswers.gender; } @@ -28,14 +28,14 @@ export const fetchStoredAnswers = async ({ memoryKeyAnswers, memoryKeyResult, qu const toReturn = { answers: null, result: null }; try { - const storedAnswers = await AsyncStorage.getItem(memoryKeyAnswers); + const storedAnswers = storage.getString(memoryKeyAnswers); if (storedAnswers !== null) { toReturn.answers = JSON.parse(storedAnswers); } else { toReturn.answers = computeInitAnswersState(); } if (memoryKeyResult) { - const storedResultKey = await AsyncStorage.getItem(memoryKeyResult); + const storedResultKey = storage.getBoolean(memoryKeyResult); if (storedResultKey !== null) { toReturn.result = JSON.parse(storedResultKey); } diff --git a/src/hooks/useStateWithAsyncStorage.js b/src/hooks/useStateWithAsyncStorage.js deleted file mode 100644 index 30e133572..000000000 --- a/src/hooks/useStateWithAsyncStorage.js +++ /dev/null @@ -1,37 +0,0 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { useIsFocused } from '@react-navigation/native'; -import { useEffect, useMemo, useState } from 'react'; - -const useStateWithAsyncStorage = (key, initValue, debug_resetOnInit = false) => { - const [value, setValue] = useState(initValue); - - const valueType = useMemo(() => typeof initValue, [initValue]); - const isFocused = useIsFocused(); - - const getInitItemValue = async () => { - if (debug_resetOnInit) return AsyncStorage.removeItem(key); - const foundValue = await AsyncStorage.getItem(key); - if (!foundValue) return; - if (valueType === 'number') { - setValue(Number(foundValue)); - } else if (valueType === 'boolean') { - setValue(foundValue === 'true' ? true : false); - } else { - setValue(JSON.parse(foundValue)); - } - }; - - const setValueInAsyncStorage = (newValue) => { - setValue(newValue); - AsyncStorage.setItem(key, JSON.stringify(newValue)); - }; - - useEffect(() => { - if (isFocused) getInitItemValue(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isFocused]); - - return [value, setValueInAsyncStorage]; -}; - -export default useStateWithAsyncStorage; diff --git a/src/redux/persistConfig.js b/src/redux/persistConfig.js index 6c6287f95..19685fc56 100644 --- a/src/redux/persistConfig.js +++ b/src/redux/persistConfig.js @@ -1,12 +1,12 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; import { createMigrate } from 'redux-persist'; +import { reduxStorage } from '../services/storage'; const migrations = {}; export default { - key: 'addicto', + key: 'addicto2', version: 4, - storage: AsyncStorage, + storage: reduxStorage, debug: false, migrate: createMigrate(migrations, { debug: false }), blacklist: [ diff --git a/src/scenes/AddDrink/BarCodeReader.js b/src/scenes/AddDrink/BarCodeReader.js index b845049ae..5f50406da 100644 --- a/src/scenes/AddDrink/BarCodeReader.js +++ b/src/scenes/AddDrink/BarCodeReader.js @@ -1,5 +1,4 @@ import React, { Component } from 'react'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import { Alert, TouchableWithoutFeedback } from 'react-native'; import { RNCamera } from 'react-native-camera'; import styled from 'styled-components'; @@ -15,6 +14,7 @@ import { CameraButton, CameraButtonsContainerSafe, } from './styles'; +import { storage } from '../../services/storage'; class BarCodeReader extends Component { state = { @@ -30,7 +30,7 @@ class BarCodeReader extends Component { }; showScanAlert = async () => { - const dontShowScanAlert = await AsyncStorage.getItem('@ScanAlert'); + const dontShowScanAlert = storage.getString('@ScanAlert'); if (dontShowScanAlert) return; setTimeout(() => { if (this.props.visible) { @@ -45,7 +45,7 @@ class BarCodeReader extends Component { { text: 'Ne plus afficher', onPress: async () => { - await AsyncStorage.setItem('@ScanAlert', 'true'); + storage.set('@ScanAlert', 'true'); }, style: 'cancel', }, diff --git a/src/scenes/ConsoFollowUp/Diagram.js b/src/scenes/ConsoFollowUp/Diagram.js index 44a05466b..cfd9de427 100644 --- a/src/scenes/ConsoFollowUp/Diagram.js +++ b/src/scenes/ConsoFollowUp/Diagram.js @@ -1,9 +1,9 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; import React, { useEffect, useState } from 'react'; import { connect } from 'react-redux'; import UnderlinedButton from '../../components/UnderlinedButton'; import { dateIsBeforeOrToday } from '../../helpers/dateHelpers'; import { fakeConsoData } from '../../reference/mocks/fakeConsoData'; +import { storage } from '../../services/storage'; import { screenHeight } from '../../styles/theme'; import { checkIfThereIsDrinks, @@ -72,12 +72,12 @@ const Diagram = ({ useEffect(() => { (async () => { try { - const storedValue = await AsyncStorage.getItem('@Quizz_answers'); + const storedValue = storage.getString('@Quizz_answers'); if (!storedValue) return; const quizzAnswers = JSON.parse(storedValue); if (!quizzAnswers) return; setHighestAcceptableDosesPerDay(getAcceptableDosePerDay(quizzAnswers.gender)); - } catch (e) { } + } catch (e) {} })(); }, []); @@ -177,16 +177,16 @@ Diagram.defaultProps = { const makeStateToProps = () => - (realState, { asPreview }) => { - const state = asPreview ? { conso: fakeConsoData.partial } : realState; + (realState, { asPreview }) => { + const state = asPreview ? { conso: fakeConsoData.partial } : realState; - return { - days: getDaysForDiagram(state), - thereIsDrinks: checkIfThereIsDrinks(state), - dailyDoses: getDailyDoses(state), - highestDailyDose: getHighestDailyDoses(state), - }; + return { + days: getDaysForDiagram(state), + thereIsDrinks: checkIfThereIsDrinks(state), + dailyDoses: getDailyDoses(state), + highestDailyDose: getHighestDailyDoses(state), }; + }; const mergeProps = (stateProps, dispatch, ownProps) => ({ ...ownProps, diff --git a/src/scenes/Defis/Defi7Days/Defi7Days.js b/src/scenes/Defis/Defi7Days/Defi7Days.js index ed5cc204a..e24271b87 100644 --- a/src/scenes/Defis/Defi7Days/Defi7Days.js +++ b/src/scenes/Defis/Defi7Days/Defi7Days.js @@ -1,9 +1,9 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; import { useFocusEffect } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import React, { useEffect, useState } from 'react'; import Background from '../../../components/Background'; import HeaderBackground from '../../../components/HeaderBackground'; +import { storage } from '../../../services/storage'; import Reminder from '../../Infos/Reminder'; import QuizzEvaluateConso from '../../Quizzs/QuizzEvaluateConso'; import QuizzLifeQuality from '../../Quizzs/QuizzLifeQuality'; @@ -23,7 +23,7 @@ const Defi7DaysStack = createStackNavigator(); const Defi7DaysNavigator = () => { const [initialScreen, setInitialScreen] = useState(null); const initNavigator = async () => { - const defiStartedAt = await AsyncStorage.getItem('DEFI_7_JOURS_STARTED_AT'); + const defiStartedAt = storage.getString('DEFI_7_JOURS_STARTED_AT'); if (defiStartedAt) return setInitialScreen('DEFI_7_DAYS_MENU'); return setInitialScreen('ONBOARDING'); }; @@ -108,9 +108,9 @@ const Defi7DaysMenu = ({ navigation }) => { const [lastUpdate, setLastUpdate] = useState(''); const getValidatedDays = async () => { - const storedLastUpdate = await AsyncStorage.getItem('DEFI_7_JOURS_LAST_UPDATE'); + const storedLastUpdate = storage.getString('DEFI_7_JOURS_LAST_UPDATE'); if (storedLastUpdate) setLastUpdate(storedLastUpdate); - const storedValidateDays = await AsyncStorage.getItem('DEFI_7_JOURS_VALIDATED_DAYS'); + const storedValidateDays = storage.getString('DEFI_7_JOURS_VALIDATED_DAYS'); if (storedValidateDays) setValidateDays(Number(storedValidateDays)); }; @@ -125,7 +125,7 @@ const Defi7DaysMenu = ({ navigation }) => { const hackAndUnlockDay = async (day) => { await new Promise((res) => setTimeout(res, 1000)); // better UX - await AsyncStorage.setItem('DEFI_7_JOURS_VALIDATED_DAYS', `${day}`); + storage.set('DEFI_7_JOURS_VALIDATED_DAYS', `${day}`); setLastUpdate('UNLOCK'); setValidateDays(day); }; diff --git a/src/scenes/Defis/Defi7Days/Onboarding.js b/src/scenes/Defis/Defi7Days/Onboarding.js index 820dc926c..4985bb2ee 100644 --- a/src/scenes/Defis/Defi7Days/Onboarding.js +++ b/src/scenes/Defis/Defi7Days/Onboarding.js @@ -1,5 +1,4 @@ import React from 'react'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import styled from 'styled-components'; import ButtonPrimary from '../../../components/ButtonPrimary'; import H1 from '../../../components/H1'; @@ -9,11 +8,12 @@ import TextStyled from '../../../components/TextStyled'; import UnderlinedButton from '../../../components/UnderlinedButton'; import matomo from '../../../services/matomo'; import { defaultPadding } from '../../../styles/theme'; +import { storage } from '../../../services/storage'; export default ({ navigation }) => { const startDefi = async () => { const startAt = new Date().toISOString().split('T')[0]; - await AsyncStorage.setItem('DEFI_7_JOURS_STARTED_AT', startAt); + storage.set('DEFI_7_JOURS_STARTED_AT', startAt); matomo.logClickStartDefi7Days(); navigation.navigate('DEFI_7_DAYS_REMINDER', { title: 'Un rappel pour penser à faire votre défi 7 jours', diff --git a/src/scenes/Defis/Defi7Days/utils.js b/src/scenes/Defis/Defi7Days/utils.js index 53524bda3..a966313de 100644 --- a/src/scenes/Defis/Defi7Days/utils.js +++ b/src/scenes/Defis/Defi7Days/utils.js @@ -1,10 +1,10 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; import matomo from '../../../services/matomo'; +import { storage } from '../../../services/storage'; export const setValidatedDays = async (day) => { await new Promise((res) => setTimeout(res, 1000)); // better UX - await AsyncStorage.setItem('DEFI_7_JOURS_VALIDATED_DAYS', `${day}`); + storage.set('DEFI_7_JOURS_VALIDATED_DAYS', `${day}`); const lastUpdate = new Date().toISOString().split('T')[0]; - await AsyncStorage.setItem('DEFI_7_JOURS_LAST_UPDATE', lastUpdate); + storage.set('DEFI_7_JOURS_LAST_UPDATE', lastUpdate); matomo.logValidateDayInDefi7Days(day); }; diff --git a/src/scenes/Defis/TopTimeline.js b/src/scenes/Defis/TopTimeline.js index 423d368eb..f401b8ca0 100644 --- a/src/scenes/Defis/TopTimeline.js +++ b/src/scenes/Defis/TopTimeline.js @@ -1,10 +1,10 @@ import React, { useEffect, useState } from 'react'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import { TouchableWithoutFeedback } from 'react-native'; import styled from 'styled-components'; import Lock from '../../components/Illustrations/Lock'; import StarButton from '../../components/Illustrations/StarButton'; import { Dot } from './Timeline'; +import { storage } from '../../services/storage'; const TopTimeline = ({ nbdays, validatedDays, activeDay, hackAndUnlockDay }) => { return ( @@ -46,7 +46,7 @@ const Day = ({ locked, done, index, unLock }) => { const unLockLevel = async () => { setPressed(0); await unLock(index); - await AsyncStorage.setItem('DEFI_7_JOURS_LAST_UPDATE', 'UNLOCK'); + storage.set('DEFI_7_JOURS_LAST_UPDATE', 'UNLOCK'); }; useEffect(() => { diff --git a/src/scenes/Gains/Estimation.js b/src/scenes/Gains/Estimation.js index 9ea2c7c9f..789b679b5 100644 --- a/src/scenes/Gains/Estimation.js +++ b/src/scenes/Gains/Estimation.js @@ -7,12 +7,12 @@ import H1 from '../../components/H1'; import TextStyled from '../../components/TextStyled'; import { screenHeight } from '../../styles/theme'; import EstimationConsosList from './EstimationConsosList'; -import { drinksByWeekState } from './recoil'; +import { maxDrinksPerWeekSelector } from './recoil'; const Estimation = () => { const navigation = useNavigation(); - const maxDrinksPerWeekGoal = useRecoilValue(drinksByWeekState); + const maxDrinksPerWeekGoal = useRecoilValue(maxDrinksPerWeekSelector); const complete = () => { navigation.navigate('GAINS'); diff --git a/src/scenes/Gains/GainsCalendar.js b/src/scenes/Gains/GainsCalendar.js index 2df209411..517621fe5 100644 --- a/src/scenes/Gains/GainsCalendar.js +++ b/src/scenes/Gains/GainsCalendar.js @@ -7,7 +7,7 @@ import styled, { css } from 'styled-components'; import H1 from '../../components/H1'; import TextStyled from '../../components/TextStyled'; import { getDailyDoses } from '../ConsoFollowUp/consoDuck'; -import { drinksByWeekState } from './recoil'; +import { maxDrinksPerWeekSelector } from './recoil'; /* markedDates is an object with keys such as `2022-04-30` and values such as @@ -21,9 +21,7 @@ markedDates is an object with keys such as `2022-04-30` and values such as */ const GainsCalendar = ({ isOnboarded, dailyDoses, dayNoDrink }) => { - console.log(dailyDoses, dayNoDrink); - - const maxDrinksPerWeekGoal = useRecoilValue(drinksByWeekState); + const maxDrinksPerWeekGoal = useRecoilValue(maxDrinksPerWeekSelector); return ( diff --git a/src/scenes/Gains/MyGains.js b/src/scenes/Gains/MyGains.js index 844bce1d4..948ce639a 100644 --- a/src/scenes/Gains/MyGains.js +++ b/src/scenes/Gains/MyGains.js @@ -19,7 +19,7 @@ import GainsCalendar from './GainsCalendar'; import MyGoal from './MyGoal'; import OnBoardingGain from './OnBoardingGain'; import { getDaysForFeed, getDailyDoses, getDrinksState } from '../ConsoFollowUp/consoDuck'; -import { daysWithGoalNoDrinkState, drinksByWeekState } from './recoil'; +import { daysWithGoalNoDrinkState, maxDrinksPerWeekSelector } from './recoil'; const MyGains = ({ days, dailyDoses }) => { @@ -33,7 +33,7 @@ const MyGains = ({ days, dailyDoses }) => { const beginDate = '3 avril'; const beginDay = 'mercredi'; - const maxDrinksPerWeekGoal = useRecoilValue(drinksByWeekState); + const maxDrinksPerWeekGoal = useRecoilValue(maxDrinksPerWeekSelector); const dayNoDrink = useRecoilValue(daysWithGoalNoDrinkState)?.length; const isOnboarded = useMemo(() => !!maxDrinksPerWeekGoal, [maxDrinksPerWeekGoal]); diff --git a/src/scenes/Gains/MyGoal.js b/src/scenes/Gains/MyGoal.js index 3bd571fc5..b0efd2a72 100644 --- a/src/scenes/Gains/MyGoal.js +++ b/src/scenes/Gains/MyGoal.js @@ -9,12 +9,12 @@ import Done from '../../components/Illustrations/Done'; import Economy from '../../components/Illustrations/Economy'; import TextStyled from '../../components/TextStyled'; import { drinksCatalog } from '../ConsoFollowUp/drinksCatalog'; -import { daysWithGoalNoDrinkState, drinksByWeekState, estimationDrinksPerWeekState } from './recoil'; +import { daysWithGoalNoDrinkState, maxDrinksPerWeekSelector, estimationDrinksPerWeekState } from './recoil'; const MyGoal = () => { const navigation = useNavigation(); - const maxDrinksPerWeekGoal = useRecoilValue(drinksByWeekState); + const maxDrinksPerWeekGoal = useRecoilValue(maxDrinksPerWeekSelector); const dayNoDrink = useRecoilValue(daysWithGoalNoDrinkState)?.length; const ToGoal = () => { diff --git a/src/scenes/Gains/recoil.js b/src/scenes/Gains/recoil.js index 9beb8b39d..ae9d90b11 100644 --- a/src/scenes/Gains/recoil.js +++ b/src/scenes/Gains/recoil.js @@ -1,36 +1,43 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; import { atom, selector } from 'recoil'; +import { storage } from '../../services/storage'; -const getInitValueFromStorage = async (key, defaultValue) => - new Promise(async (resolve) => { - const valueType = typeof defaultValue; - const foundValue = await AsyncStorage.getItem(key); - if (!foundValue) return resolve(defaultValue); - if (valueType === 'number') return resolve(Number(foundValue)); - if (valueType === 'boolean') return resolve(foundValue === 'true' ? true : false); - return resolve(JSON.parse(foundValue)); - }); +const getInitValueFromStorage = (key, defaultValue) => { + const valueType = typeof defaultValue; + if (valueType === 'number') { + const foundValue = storage.getNumber(key); + if (!foundValue) return defaultValue; + return Number(foundValue); + } + if (valueType === 'boolean') { + const foundValue = storage.getBoolean(key); + if (!foundValue) return defaultValue; + return foundValue; + } + const foundValue = storage.getString(key); + if (!foundValue) return defaultValue; + return JSON.parse(foundValue); +}; export const daysWithGoalNoDrinkState = atom({ key: 'daysWithGoalNoDrinkState', default: getInitValueFromStorage('@DaysWithGoalNoDrink', []), - effects: [({ onSet }) => onSet((newValue) => AsyncStorage.setItem('@DaysWithGoalNoDrink', JSON.stringify(newValue)))], + effects: [({ onSet }) => onSet((newValue) => storage.set('@DaysWithGoalNoDrink', JSON.stringify(newValue)))], }); export const drinksByDrinkingDayState = atom({ key: 'drinksByDrinkingDayState', default: getInitValueFromStorage('@StoredDrinksByDrinkingDay', 0), - effects: [({ onSet }) => onSet((newValue) => AsyncStorage.setItem('@StoredDrinksByDrinkingDay', newValue))], + effects: [({ onSet }) => onSet((newValue) => storage.set('@StoredDrinksByDrinkingDay', newValue))], }); export const estimationDrinksPerWeekState = atom({ key: 'estimationDrinksPerWeekState', default: getInitValueFromStorage('@GainEstimationDrinksPerWeek', []), - effects: [({ onSet }) => onSet((newValue) => AsyncStorage.setItem('@GainEstimationDrinksPerWeek', newValue))], + effects: [({ onSet }) => onSet((newValue) => storage.set('@GainEstimationDrinksPerWeek', newValue))], }); -export const drinksByWeekState = selector({ - key: 'drinksByWeekState', +export const maxDrinksPerWeekSelector = selector({ + key: 'maxDrinksPerWeekSelector', get: ({ get }) => { const drinksByDrinkingDay = get(drinksByDrinkingDayState); const daysWithGoalNoDrink = get(daysWithGoalNoDrinkState); diff --git a/src/scenes/Infos/Reminder.js b/src/scenes/Infos/Reminder.js index f327216b4..5aad81c1c 100644 --- a/src/scenes/Infos/Reminder.js +++ b/src/scenes/Infos/Reminder.js @@ -1,5 +1,4 @@ import React, { Component } from 'react'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import { Alert, Platform } from 'react-native'; import { openSettings } from 'react-native-permissions'; import styled from 'styled-components'; @@ -16,6 +15,7 @@ import matomo from '../../services/matomo'; import NotificationService from '../../services/notifications'; import { defaultPadding } from '../../styles/theme'; import { followupNumberOfDays } from '../ConsoFollowUp/consoDuck'; +import { storage } from '../../services/storage'; const notifReminderTitle = "C'est l'heure de votre suivi quotidien !"; const notifReminderMessage = "N'oubliez pas de remplir votre agenda Oz"; @@ -37,7 +37,7 @@ class Reminder extends Component { getReminder = async (showAlert = true) => { const isRegistered = await NotificationService.checkPermission(); - const reminder = await AsyncStorage.getItem('@Reminder'); + const reminder = storage.getString('@Reminder'); // eslint-disable-next-line eqeqeq if (Boolean(reminder) && new Date(reminder) == 'Invalid Date') { this.deleteReminder(); @@ -93,14 +93,14 @@ class Reminder extends Component { this.setState({ timePickerVisible: false }); return; } - await AsyncStorage.setItem('@Reminder', reminder.toISOString()); + storage.set('@Reminder', reminder.toISOString()); await this.scheduleNotification(reminder); await matomo.logReminderSet(Date.parse(reminder)); this.setState({ reminder, timePickerVisible: false }); }; deleteReminder = async () => { - await AsyncStorage.removeItem('@Reminder'); + storage.delete('@Reminder'); NotificationService.cancelAll(); this.setState({ reminder: null, timePickerVisible: false }); matomo.logReminderDelete(); diff --git a/src/scenes/NPS/NPS.js b/src/scenes/NPS/NPS.js index e7ea9d2a8..7114e800c 100644 --- a/src/scenes/NPS/NPS.js +++ b/src/scenes/NPS/NPS.js @@ -1,5 +1,4 @@ import React, { Component } from 'react'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import PropTypes from 'prop-types'; import { Alert, AppState, Modal, Platform } from 'react-native'; import { SafeAreaProvider } from 'react-native-safe-area-context'; @@ -23,6 +22,7 @@ import { TopSubTitle, TopTitle, } from './styles'; +import { storage } from '../../services/storage'; // just to make sure nothing goes the bad way in production, debug is always false @@ -86,17 +86,17 @@ class NPS extends Component { } reset = async () => { - await AsyncStorage.removeItem(STORE_KEYS.NPS_DONE); - await AsyncStorage.removeItem(STORE_KEYS.INITIAL_OPENING); + storage.delete(STORE_KEYS.NPS_DONE); + storage.delete(STORE_KEYS.INITIAL_OPENING); }; checkNeedNPS = async () => { - const NPSDone = await AsyncStorage.getItem(STORE_KEYS.NPS_DONE); + const NPSDone = storage.getString(STORE_KEYS.NPS_DONE); if (NPSDone) return; - const appFirstOpening = await AsyncStorage.getItem(STORE_KEYS.INITIAL_OPENING); + const appFirstOpening = storage.getString(STORE_KEYS.INITIAL_OPENING); if (!appFirstOpening) { - await AsyncStorage.setItem(STORE_KEYS.INITIAL_OPENING, new Date().toISOString()); + storage.set(STORE_KEYS.INITIAL_OPENING, new Date().toISOString()); NotificationService.scheduleNotification({ date: new Date(Date.now() + NPSTimeoutMS), title: this.props.notifTitle, @@ -104,11 +104,11 @@ class NPS extends Component { }); return; } - const opening = await AsyncStorage.getItem(STORE_KEYS.INITIAL_OPENING); + const opening = storage.getString(STORE_KEYS.INITIAL_OPENING); const timeForNPS = Date.now() - Date.parse(new Date(opening)) > NPSTimeoutMS; if (!timeForNPS) return; matomo.logNPSOpen(); - await AsyncStorage.setItem(STORE_KEYS.NPS_DONE, 'true'); + storage.set(STORE_KEYS.NPS_DONE, 'true'); this.setState({ visible: true }); }; @@ -147,7 +147,7 @@ class NPS extends Component { style: 'cancel', onPress: () => { this.notifHandled = false; - AsyncStorage.setItem(STORE_KEYS.NPS_DONE, 'true'); + storage.set(STORE_KEYS.NPS_DONE, 'true'); }, }, ], @@ -187,7 +187,7 @@ class NPS extends Component { return; } const { userIdLocalStorageKey } = this.props; - const userId = await AsyncStorage.getItem(userIdLocalStorageKey); + const userId = storage.getString(userIdLocalStorageKey); this.setSendButton('Merci !'); matomo.logNPSUsefulSend(useful); matomo.logNPSRecoSend(reco); diff --git a/src/scenes/Quizzs/QuizzEvaluateConso/utils.js b/src/scenes/Quizzs/QuizzEvaluateConso/utils.js index a795d4094..b7cf93cf0 100644 --- a/src/scenes/Quizzs/QuizzEvaluateConso/utils.js +++ b/src/scenes/Quizzs/QuizzEvaluateConso/utils.js @@ -1,9 +1,9 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; import { getAnswerScore } from '../../../components/Quizz/utils'; import { capture } from '../../../services/sentry'; +import { storage } from '../../../services/storage'; export const getGenderFromLocalStorage = async () => { - const storedAnswers = await AsyncStorage.getItem('@Quizz_answers'); + const storedAnswers = storage.getString('@Quizz_answers'); if (storedAnswers !== null) { const newAnswers = JSON.parse(storedAnswers); return newAnswers.gender; diff --git a/src/scenes/Quizzs/QuizzMotivations/index.js b/src/scenes/Quizzs/QuizzMotivations/index.js index ae5205138..4dca9a6d0 100644 --- a/src/scenes/Quizzs/QuizzMotivations/index.js +++ b/src/scenes/Quizzs/QuizzMotivations/index.js @@ -1,5 +1,4 @@ import React, { useEffect, useState } from 'react'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import { createStackNavigator } from '@react-navigation/stack'; import Background from '../../../components/Background'; import ButtonPrimary from '../../../components/ButtonPrimary'; @@ -10,6 +9,7 @@ import Results from './ResultsMotivations'; import Section from './Section'; import sections from './sections'; import { Paragraph, ScreenBgStyled, TopContainer, TopTitle, TopTitleContainer } from './styles'; +import { storage } from '../../../services/storage'; const QuizzMotivationsStack = createStackNavigator(); @@ -43,8 +43,8 @@ const QuizzMotivations = ({ navigation, route }) => { }; const validateAnswers = async () => { - await AsyncStorage.setItem(memoryKeyAnswers, JSON.stringify(answers)); - await AsyncStorage.setItem(memoryKeyResult, 'true'); + storage.set(memoryKeyAnswers, JSON.stringify(answers)); + storage.set(memoryKeyResult, true); navigation.push('QUIZZ_RESULTS'); }; diff --git a/src/scenes/Quizzs/QuizzOnboarding/utils.js b/src/scenes/Quizzs/QuizzOnboarding/utils.js index f72fc76f3..bdba93cb9 100644 --- a/src/scenes/Quizzs/QuizzOnboarding/utils.js +++ b/src/scenes/Quizzs/QuizzOnboarding/utils.js @@ -1,11 +1,11 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; import { capture } from '../../../services/sentry'; import { getAnswerScore } from '../../../components/Quizz/utils'; +import { storage } from '../../../services/storage'; // Utils export const getGenderFromLocalStorage = async () => { - const storedAnswers = await AsyncStorage.getItem('@Quizz_answers'); + const storedAnswers = storage.getString('@Quizz_answers'); if (storedAnswers !== null) { const newAnswers = JSON.parse(storedAnswers); return newAnswers.gender; diff --git a/src/scenes/WelcomeScreen/WelcomeScreen.js b/src/scenes/WelcomeScreen/WelcomeScreen.js index 4761fbef0..acd7da8b3 100644 --- a/src/scenes/WelcomeScreen/WelcomeScreen.js +++ b/src/scenes/WelcomeScreen/WelcomeScreen.js @@ -1,5 +1,4 @@ import React, { useRef, useState } from 'react'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import RNBootSplash from 'react-native-bootsplash'; import Swiper from 'react-native-swiper'; import styled from 'styled-components'; @@ -13,6 +12,7 @@ import CONSTANTS from '../../reference/constants'; import matomo from '../../services/matomo'; import { screenHeight } from '../../styles/theme'; import { Screen1, Screen2, Screen3 } from './Screens'; +import { storage } from '../../services/storage'; const WelcomeScreen = ({ navigation }) => { const [agreed, setAgreed] = useState(false); @@ -30,7 +30,7 @@ const WelcomeScreen = ({ navigation }) => { }; const onStartPress = async () => { - AsyncStorage.setItem('@OnboardingDoneWithCGU', 'true'); + storage.set('@OnboardingDoneWithCGU', true); RNBootSplash.show({ duration: 250 }); await new Promise((res) => setTimeout(res, 250)); navigation.navigate('TABS'); diff --git a/src/services/matomo/index.js b/src/services/matomo/index.js index 09eb70fa3..4e0cd0632 100644 --- a/src/services/matomo/index.js +++ b/src/services/matomo/index.js @@ -1,22 +1,22 @@ import NetInfo from '@react-native-community/netinfo'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import DeviceInfo from 'react-native-device-info'; import { Platform } from 'react-native'; import Matomo from './lib'; import { MATOMO_IDSITE_1, MATOMO_IDSITE_2, MATOMO_URL, MATOMO_URL_2 } from '../../config'; import { getGenderFromLocalStorage } from '../../components/Quizz/utils'; import { mapOnboardingResultToMatomoProfile } from '../../scenes/Quizzs/QuizzOnboarding/utils'; +import { storage } from '../storage'; const initMatomo = async () => { - let userId = await AsyncStorage.getItem('@UserIdv2'); + let userId = storage.getString('@UserIdv2'); if (!userId) { userId = Matomo.makeid(); - await AsyncStorage.setItem('@UserIdv2', userId); + storage.set('@UserIdv2', userId); } - const prevVisits = await AsyncStorage.getItem('@NumberOfVisits'); + const prevVisits = storage.getNumber('@NumberOfVisits'); const newVisits = prevVisits ? Number(prevVisits) + 1 : 1; - await AsyncStorage.setItem('@NumberOfVisits', `${newVisits}`); + storage.set('@NumberOfVisits', `${newVisits}`); Matomo.init({ baseUrl: MATOMO_URL, @@ -30,7 +30,7 @@ const initMatomo = async () => { idsite: MATOMO_IDSITE_2, }); - const resultKey = await AsyncStorage.getItem('@Quizz_result'); + const resultKey = storage.getString('@Quizz_result'); const gender = await getGenderFromLocalStorage(); Matomo.setUserProperties({ version: DeviceInfo.getVersion(), diff --git a/src/services/storage.js b/src/services/storage.js new file mode 100644 index 000000000..05d4620a7 --- /dev/null +++ b/src/services/storage.js @@ -0,0 +1,54 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { MMKV } from 'react-native-mmkv'; + +export const storage = new MMKV(); +// fucking compatibility with redux-persis +export const reduxStorage = { + setItem: (key, value) => { + storage.set(key, value); + return Promise.resolve(true); + }, + getItem: (key) => { + const value = storage.getString(key); + return Promise.resolve(value); + }, + removeItem: (key) => { + storage.delete(key); + return Promise.resolve(); + }, +}; + +// TODO: Remove `hasMigratedFromAsyncStorage` after a while (when everyone has migrated) +export const hasMigratedFromAsyncStorage = storage.getBoolean('hasMigratedFromAsyncStorage'); + +// TODO: Remove `hasMigratedFromAsyncStorage` after a while (when everyone has migrated) +export async function migrateFromAsyncStorage() { + console.log('Migrating from AsyncStorage -> MMKV...'); + const start = global.performance.now(); + + const keys = await AsyncStorage.getAllKeys(); + + for (const key of keys) { + try { + const value = await AsyncStorage.getItem(key); + + if (value != null) { + if (['true', 'false'].includes(value)) { + storage.set(key, value === 'true'); + } else { + storage.set(key, value); + } + + AsyncStorage.removeItem(key); + } + } catch (error) { + console.error(`Failed to migrate key "${key}" from AsyncStorage to MMKV!`, error); + throw error; + } + } + + storage.set('hasMigratedFromAsyncStorage', true); + + const end = global.performance.now(); + console.log(`Migrated from AsyncStorage -> MMKV in ${end - start}ms!`); +} diff --git a/yarn.lock b/yarn.lock index 79647258b..c14c04bb4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6819,6 +6819,11 @@ react-native-iphone-x-helper@^1.3.0: resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010" integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg== +react-native-mmkv@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-2.4.1.tgz#9e6a29f55911be2b990776bfb961b4f118126153" + integrity sha512-sCH8nm7KmRnMzG6wNDfon8jw/mFUfVAEQed5258kpMc2LtEdK47P34GPCovTnEtogWX8z8RQZ5rEGGgty8srtA== + react-native-permissions@^3.0.5: version "3.3.1" resolved "https://registry.yarnpkg.com/react-native-permissions/-/react-native-permissions-3.3.1.tgz#8a359d9c0531afcde2f642a459b48c4c1e3e8f2d"