diff --git a/app/AppHook.ts b/app/AppHook.ts index f0b7d17710..7c3ba69d77 100644 --- a/app/AppHook.ts +++ b/app/AppHook.ts @@ -14,6 +14,10 @@ import { selectSelectedCurrency } from 'store/settings/currency' import { onLogOut, setWalletState, WalletState } from 'store/app' import { resetLoginAttempt } from 'store/security' import { formatCurrency } from 'utils/FormatCurrency' +import { + selectCoreAnalyticsConsent, + selectTouAndPpConsent +} from 'store/settings/securityPrivacy' export type AppHook = { onExit: () => Observable @@ -33,8 +37,9 @@ export function useApp( const selectedCurrency = useSelector(selectSelectedCurrency) const [navigationContainerSet, setNavigationContainerSet] = useState(false) const [initRouteSet, setInitRouteSet] = useState(false) - const { getSetting } = repository.userSettingsRepo const { setAnalyticsConsent } = usePosthogContext() + const coreAnalyticsConsentSetting = useSelector(selectCoreAnalyticsConsent) + const touAndPpConsentSetting = useSelector(selectTouAndPpConsent) const deleteWallet = useCallback(() => { walletSetupHook.destroyWallet() @@ -49,19 +54,22 @@ export function useApp( }, [appNavHook, deleteWallet]) useEffect(waitForNavigationContainer, [appNavHook.navigation]) - useEffect(watchCoreAnalyticsFlagFx, [getSetting, setAnalyticsConsent]) + useEffect(watchCoreAnalyticsFlagFx, [ + coreAnalyticsConsentSetting, + setAnalyticsConsent + ]) useEffect(decideInitialRoute, [ appNavHook, dispatch, initRouteSet, navigationContainerSet, repository, - signOut + signOut, + touAndPpConsentSetting ]) function watchCoreAnalyticsFlagFx() { - const setting = getSetting('CoreAnalytics') as boolean | undefined - setAnalyticsConsent(setting) + setAnalyticsConsent(coreAnalyticsConsentSetting) } function waitForNavigationContainer() { @@ -83,7 +91,7 @@ export function useApp( setInitRouteSet(true) AsyncStorage.getItem(SECURE_ACCESS_SET).then(result => { if (result) { - if (!repository.userSettingsRepo.getSetting('ConsentToTOU&PP')) { + if (!touAndPpConsentSetting) { //User has probably killed app before consent to TOU, so we'll clear all data and //return him to onboarding signOut() diff --git a/app/Repo.ts b/app/Repo.ts index d620cb9e18..d94ce410a3 100644 --- a/app/Repo.ts +++ b/app/Repo.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import StorageTools from 'repository/StorageTools' /** @@ -7,7 +7,6 @@ import StorageTools from 'repository/StorageTools' * * Suffix "_" is for destructive migration of database. In the future, we want gracefully migrate data with no data loss. */ -const USER_SETTINGS = 'USER_SETTINGS' const VIEW_ONCE_INFORMATION = 'VIEW_ONCE_INFORMATION' /** @@ -40,7 +39,6 @@ export type RecentContact = { type: AddrBookItemType } -export type Setting = 'CoreAnalytics' | 'ConsentToTOU&PP' export type SettingValue = number | string | boolean | undefined export type AddrBookItemType = 'account' | 'contact' @@ -51,13 +49,6 @@ export type Repo = { infoHasBeenShown: (info: ViewOnceInformation) => boolean saveViewOnceInformation: (info: ViewOnceInformation[]) => void } - /** - * Store any simple user settings here - */ - userSettingsRepo: { - setSetting: (setting: Setting, value: SettingValue) => void - getSetting: (setting: Setting) => SettingValue | undefined - } flush: () => void initialized: boolean } @@ -65,9 +56,6 @@ export type Repo = { export function useRepo(): Repo { const [initialized, setInitialized] = useState(false) const [viewOnceInfo, setViewOnceInfo] = useState([]) - const [userSettings, setUserSettings] = useState>( - new Map() - ) useEffect(() => { ;(async () => { @@ -76,22 +64,6 @@ export function useRepo(): Repo { })() }, []) - const setSetting = (setting: Setting, value: SettingValue) => { - const updatedSettings = new Map(userSettings) - updatedSettings.set(setting, value) - setUserSettings(updatedSettings) - StorageTools.saveMapToStorage(USER_SETTINGS, updatedSettings).catch( - reason => console.error(reason) - ) - } - - const getSetting = useCallback( - (setting: Setting) => { - return userSettings.get(setting) - }, - [userSettings] - ) - const saveViewOnceInformation = (info: ViewOnceInformation[]) => { // we use set so we don't allow duplicates const infoSet = [...new Set(info)] @@ -109,17 +81,10 @@ export function useRepo(): Repo { * Clear hook states */ const flush = () => { - setUserSettings(new Map()) setInitialized(false) } async function loadInitialStatesFromStorage() { - setUserSettings( - await StorageTools.loadFromStorageAsMap( - USER_SETTINGS - ) - ) - const initialViewOnceInfoFromStorage = await StorageTools.loadFromStorageAsArray( VIEW_ONCE_INFORMATION @@ -129,10 +94,6 @@ export function useRepo(): Repo { } return { - userSettingsRepo: { - setSetting, - getSetting - }, informationViewOnceRepo: { viewOnceInfo: viewOnceInfo, saveViewOnceInformation, diff --git a/app/components/TermsNConditionsModal.tsx b/app/components/TermsNConditionsModal.tsx index 9f5e1c4889..23e3829744 100644 --- a/app/components/TermsNConditionsModal.tsx +++ b/app/components/TermsNConditionsModal.tsx @@ -13,6 +13,8 @@ import { useBeforeRemoveListener } from 'hooks/useBeforeRemoveListener' import { PRIVACY_POLICY_URL, TERMS_OF_USE_URL } from 'resources/Constants' +import { useDispatch } from 'react-redux' +import { setTouAndPpConsent } from 'store/settings/securityPrivacy' interface Props { onNext: () => void @@ -20,10 +22,11 @@ interface Props { } const TermsNConditionsModal = ({ onNext, onReject }: Props) => { - const { theme, repo } = useApplicationContext() + const { theme } = useApplicationContext() const [touChecked, setTouChecked] = useState(false) const [ppChecked, setPpChecked] = useState(false) const nextBtnEnabled = touChecked && ppChecked + const dispatch = useDispatch() useBeforeRemoveListener(onReject, [RemoveEvents.GO_BACK], true) @@ -32,7 +35,7 @@ const TermsNConditionsModal = ({ onNext, onReject }: Props) => { // he would be able to enter app without consent to Terms n Conditions. // To prevent this, we set 'ConsentToTOU&PP' to repo and check that on app startup. function saveConsentAndProceed() { - repo.userSettingsRepo.setSetting('ConsentToTOU&PP', true) + dispatch(setTouAndPpConsent(true)) onNext() } diff --git a/app/navigation/onboarding/CreateWalletStack.tsx b/app/navigation/onboarding/CreateWalletStack.tsx index 2c0f9df75b..7d04d9548b 100644 --- a/app/navigation/onboarding/CreateWalletStack.tsx +++ b/app/navigation/onboarding/CreateWalletStack.tsx @@ -24,6 +24,7 @@ import { } from 'hooks/useBeforeRemoveListener' import { usePostCapture } from 'hooks/usePosthogCapture' import OwlLoader from 'components/OwlLoader' +import { setCoreAnalytics } from 'store/settings/securityPrivacy' import { CreateWalletScreenProps } from '../types' export type CreateWalletStackParamList = { @@ -96,13 +97,13 @@ const CreateWalletScreen = () => { const createWalletContext = useContext(CreateWalletContext) const { navigate } = useNavigation() const { capture } = usePostCapture() - const { userSettingsRepo } = useApplicationContext().repo + const dispatch = useDispatch() useBeforeRemoveListener( useCallback(() => { capture('OnboardingCancelled') - userSettingsRepo.setSetting('CoreAnalytics', undefined) - }, [capture, userSettingsRepo]), + dispatch(setCoreAnalytics(undefined)) + }, [capture, dispatch]), [RemoveEvents.GO_BACK] ) diff --git a/app/navigation/onboarding/EnterWithMnemonicStack.tsx b/app/navigation/onboarding/EnterWithMnemonicStack.tsx index 6e3e1396a1..5fa0033cf0 100644 --- a/app/navigation/onboarding/EnterWithMnemonicStack.tsx +++ b/app/navigation/onboarding/EnterWithMnemonicStack.tsx @@ -23,6 +23,7 @@ import { } from 'hooks/useBeforeRemoveListener' import { usePostCapture } from 'hooks/usePosthogCapture' import OwlLoader from 'components/OwlLoader' +import { setCoreAnalytics } from 'store/settings/securityPrivacy' import { EnterWithMnemonicScreenProps } from '../types' export type EnterWithMnemonicStackParamList = { @@ -86,14 +87,14 @@ const LoginWithMnemonicScreen = () => { const enterWithMnemonicContext = useContext(EnterWithMnemonicContext) const { navigate, goBack } = useNavigation() const { capture } = usePostCapture() - const { userSettingsRepo } = useApplicationContext().repo + const dispatch = useDispatch() const { deleteWallet } = useApplicationContext().appHook useBeforeRemoveListener( useCallback(() => { capture('OnboardingCancelled') - userSettingsRepo.setSetting('CoreAnalytics', undefined) - }, [capture, userSettingsRepo]), + dispatch(setCoreAnalytics(undefined)) + }, [capture, dispatch]), [RemoveEvents.GO_BACK] ) diff --git a/app/screens/drawer/security/SecurityPrivacy.tsx b/app/screens/drawer/security/SecurityPrivacy.tsx index 365e013191..ead02a8690 100644 --- a/app/screens/drawer/security/SecurityPrivacy.tsx +++ b/app/screens/drawer/security/SecurityPrivacy.tsx @@ -6,6 +6,11 @@ import BiometricsSDK from 'utils/BiometricsSDK' import AsyncStorage from '@react-native-async-storage/async-storage' import { SECURE_ACCESS_SET } from 'resources/Constants' import Switch from 'components/Switch' +import { useDispatch, useSelector } from 'react-redux' +import { + selectCoreAnalyticsConsent, + setCoreAnalytics +} from 'store/settings/securityPrivacy' function SecurityPrivacy({ onChangePin, @@ -19,8 +24,8 @@ function SecurityPrivacy({ onShowConnectedDapps: () => void }) { const theme = useApplicationContext().theme - const { setSetting, getSetting } = - useApplicationContext().repo.userSettingsRepo + const dispatch = useDispatch() + const coreAnalyticsConsent = useSelector(selectCoreAnalyticsConsent) const [isBiometricSwitchEnabled, setIsBiometricSwitchEnabled] = useState(false) const [isBiometricEnabled, setIsBiometricEnabled] = useState(false) @@ -44,7 +49,7 @@ function SecurityPrivacy({ } const handleAnalyticsSwitchChange = (value: boolean) => { - setSetting('CoreAnalytics', value) + dispatch(setCoreAnalytics(value)) } return ( @@ -85,7 +90,7 @@ function SecurityPrivacy({ background={theme.background} rightComponent={ } diff --git a/app/screens/onboarding/AnalyticsConsent.tsx b/app/screens/onboarding/AnalyticsConsent.tsx index 4a578e1f8b..d56ba6e760 100644 --- a/app/screens/onboarding/AnalyticsConsent.tsx +++ b/app/screens/onboarding/AnalyticsConsent.tsx @@ -10,6 +10,8 @@ import CheckmarkSVG from 'components/svg/CheckmarkSVG' import { useApplicationContext } from 'contexts/ApplicationContext' import { PRIVACY_POLICY_URL } from 'resources/Constants' import { usePostCapture } from 'hooks/usePosthogCapture' +import { useDispatch } from 'react-redux' +import { setCoreAnalytics } from 'store/settings/securityPrivacy' type Props = { nextScreen: @@ -23,7 +25,8 @@ type Props = { } const AnalyticsConsent = ({ onNextScreen, nextScreen }: Props) => { - const { theme, repo } = useApplicationContext() + const dispatch = useDispatch() + const { theme } = useApplicationContext() const { capture } = usePostCapture() function openPrivacyPolicy() { @@ -32,13 +35,13 @@ const AnalyticsConsent = ({ onNextScreen, nextScreen }: Props) => { function acceptAnalytics() { capture('OnboardingAnalyticsAccepted') - repo.userSettingsRepo.setSetting('CoreAnalytics', true) + dispatch(setCoreAnalytics(true)) onNextScreen(nextScreen) } function rejectAnalytics() { capture('OnboardingAnalyticsRejected') - repo.userSettingsRepo.setSetting('CoreAnalytics', false) + dispatch(setCoreAnalytics(false)) onNextScreen(nextScreen) } diff --git a/app/store/index.ts b/app/store/index.ts index 99b6dbc547..059ca9e702 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -30,7 +30,7 @@ import { WatchlistBlacklistTransform } from './transforms/WatchlistBlacklistTran import { WalletConnectBlacklistTransform } from './transforms/WalletConnectBlacklistTransform' import { AppBlacklistTransform } from './transforms/AppBlacklistTransform' -const VERSION = 5 +const VERSION = 6 // list of reducers that don't need to be persisted // for nested/partial blacklist, please use transform @@ -77,7 +77,13 @@ const rootReducer = (state: any, action: AnyAction) => { // notes: keeping settings and network because watchlist depends on them state = { app: state.app, - settings: state.settings, + settings: { + ...state.settings, + securityPrivacy: { + ...state.settings.securityPrivacy, + consentToTOUnPP: false //don't keep consent to Terms of use and Privacy policy + } + }, network: state.network, watchlist: state.watchlist } diff --git a/app/store/migrations.ts b/app/store/migrations.ts index 7e3fa8a6eb..0b684d9a1f 100644 --- a/app/store/migrations.ts +++ b/app/store/migrations.ts @@ -1,4 +1,5 @@ import { ChainId } from '@avalabs/chains-sdk' +import StorageTools from 'repository/StorageTools' import { initialState as watchlistInitialState } from './watchlist' import { initialState as posthogInitialState } from './posthog' @@ -69,5 +70,25 @@ export const migrations = { active: updatedActive } } + }, + 6: async (state: any) => { + const map = await StorageTools.loadFromStorageAsMap< + 'CoreAnalytics' | 'ConsentToTOU&PP', + boolean | undefined + >('USER_SETTINGS') + + const coreAnalytics = map.get('CoreAnalytics') + const consentToTOUnPP = Boolean(map.get('ConsentToTOU&PP')) + + return { + ...state, + settings: { + ...state.settings, + securityPrivacy: { + coreAnalytics: coreAnalytics, + consentToTOUnPP + } + } + } } } diff --git a/app/store/settings/index.ts b/app/store/settings/index.ts index 125859f8f3..10c1af9280 100644 --- a/app/store/settings/index.ts +++ b/app/store/settings/index.ts @@ -1,8 +1,10 @@ import { combineReducers } from '@reduxjs/toolkit' import { currencyReducer as currency } from './currency' import { advancedReducer as advanced } from './advanced' +import { securityPrivacyReducer as securityPrivacy } from './securityPrivacy' export default combineReducers({ currency, - advanced + advanced, + securityPrivacy }) diff --git a/app/store/settings/securityPrivacy/index.ts b/app/store/settings/securityPrivacy/index.ts new file mode 100644 index 0000000000..1f3c5a2e30 --- /dev/null +++ b/app/store/settings/securityPrivacy/index.ts @@ -0,0 +1,2 @@ +export * from './slice' +export * from './types' diff --git a/app/store/settings/securityPrivacy/slice.ts b/app/store/settings/securityPrivacy/slice.ts new file mode 100644 index 0000000000..b0cf70b2ee --- /dev/null +++ b/app/store/settings/securityPrivacy/slice.ts @@ -0,0 +1,38 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { RootState } from 'store' +import { initialState } from './types' + +const reducerName = 'securityPrivacy' + +export const securityPrivacySlice = createSlice({ + name: reducerName, + initialState, + reducers: { + setCoreAnalytics: (state, action: PayloadAction) => { + state.coreAnalytics = action.payload + }, + /** + * Set Terms of use and Privacy policy consent + */ + setTouAndPpConsent: (state, action: PayloadAction) => { + state.consentToTOUnPP = action.payload + } + } +}) + +// selectors +export const selectCoreAnalyticsConsent = (state: RootState) => + state.settings.securityPrivacy.coreAnalytics + +/** + * Select Terms of use and Privacy policy consent + * @param state + */ +export const selectTouAndPpConsent = (state: RootState) => + state.settings.securityPrivacy.consentToTOUnPP + +// actions +export const { setCoreAnalytics, setTouAndPpConsent } = + securityPrivacySlice.actions + +export const securityPrivacyReducer = securityPrivacySlice.reducer diff --git a/app/store/settings/securityPrivacy/types.ts b/app/store/settings/securityPrivacy/types.ts new file mode 100644 index 0000000000..1b4904ab83 --- /dev/null +++ b/app/store/settings/securityPrivacy/types.ts @@ -0,0 +1,9 @@ +export const initialState: SecurityNPrivacyState = { + coreAnalytics: undefined, + consentToTOUnPP: false +} + +export type SecurityNPrivacyState = { + coreAnalytics: boolean | undefined + consentToTOUnPP: boolean +} diff --git a/patches/redux-persist+6.0.0.patch b/patches/redux-persist+6.0.0.patch new file mode 100644 index 0000000000..726a23c06a --- /dev/null +++ b/patches/redux-persist+6.0.0.patch @@ -0,0 +1,67 @@ +diff --git a/node_modules/redux-persist/lib/createMigrate.js b/node_modules/redux-persist/lib/createMigrate.js +index ef32d94..1ad0efa 100644 +--- a/node_modules/redux-persist/lib/createMigrate.js ++++ b/node_modules/redux-persist/lib/createMigrate.js +@@ -9,17 +9,17 @@ function createMigrate(migrations, config) { + var _ref = config || {}, + debug = _ref.debug; + +- return function (state, currentVersion) { ++ return async function (state, currentVersion) { + if (!state) { + if (process.env.NODE_ENV !== 'production' && debug) console.log('redux-persist: no inbound state, skipping migration'); +- return Promise.resolve(undefined); ++ return undefined; + } + + var inboundVersion = state._persist && state._persist.version !== undefined ? state._persist.version : _constants.DEFAULT_VERSION; + + if (inboundVersion === currentVersion) { + if (process.env.NODE_ENV !== 'production' && debug) console.log('redux-persist: versions match, noop migration'); +- return Promise.resolve(state); ++ return state; + } + + if (inboundVersion > currentVersion) { +@@ -36,14 +36,17 @@ function createMigrate(migrations, config) { + }); + if (process.env.NODE_ENV !== 'production' && debug) console.log('redux-persist: migrationKeys', migrationKeys); + +- try { +- var migratedState = migrationKeys.reduce(function (state, versionKey) { +- if (process.env.NODE_ENV !== 'production' && debug) console.log('redux-persist: running migration for versionKey', versionKey); +- return migrations[versionKey](state); +- }, state); +- return Promise.resolve(migratedState); +- } catch (err) { +- return Promise.reject(err); ++ let migratedState = state ++ ++ for (const versionKey of migrationKeys) { ++ if (process.env.NODE_ENV !== 'production' && debug) ++ console.log( ++ 'redux-persist: running migration for versionKey', ++ versionKey ++ ) ++ migratedState = await migrations[versionKey](migratedState) + } ++ ++ return migratedState + }; + } +\ No newline at end of file +diff --git a/node_modules/redux-persist/types/types.d.ts b/node_modules/redux-persist/types/types.d.ts +index b3733bc..766f219 100644 +--- a/node_modules/redux-persist/types/types.d.ts ++++ b/node_modules/redux-persist/types/types.d.ts +@@ -73,7 +73,9 @@ declare module "redux-persist/es/types" { + } + + interface MigrationManifest { +- [key: string]: (state: PersistedState) => PersistedState; ++ [key: string]: ++ | ((state: PersistedState) => PersistedState) ++ | ((state: PersistedState) => Promise) + } + + /**