Skip to content

Commit

Permalink
US-1872 Security - Can reset the wallet without auth (#734)
Browse files Browse the repository at this point in the history
* feat: retry unlock if cancelled

* feat: RetryLogin screen + Delete Wallet Modal refactor

* chore: remove console.log

* chore: put !__DEV__ back into unlockApp

* fix: remove src/ alias from import

* fix: android going back to CreateKeys
  • Loading branch information
TravellerOnTheRun authored Sep 13, 2023
1 parent ae203be commit d652009
Show file tree
Hide file tree
Showing 14 changed files with 207 additions and 90 deletions.
101 changes: 101 additions & 0 deletions src/components/modal/deleteWalletModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Dispatch, SetStateAction, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'

import { resetApp } from 'store/slices/settingsSlice'
import { useAppDispatch } from 'store/storeUtils'
import { sharedColors } from 'shared/constants'

import { ConfirmationModal, ConfirmationModalConfig } from '..'

interface Props {
isVisible: boolean
setVisible: Dispatch<SetStateAction<boolean>>
}

export const DeleteWalletModal = ({ isVisible, setVisible }: Props) => {
const dispatch = useAppDispatch()
const { t } = useTranslation()

const eraseWallet = useCallback(() => {
dispatch(resetApp())
}, [dispatch])

const createDeleteDefinitiveConfirmationConfig = useCallback(
(
createDeleteConfirmationConfigFn: () => ConfirmationModalConfig,
): ConfirmationModalConfig => {
return {
color: sharedColors.dangerLight,
title: t(
'wallet_backup_definitive_delete_confirmation_title',
) as string,
titleColor: sharedColors.black,
description: t(
'wallet_backup_definitive_delete_confirmation_description',
),
descriptionColor: sharedColors.black,
okText: t('Delete'),
cancelText: t('Cancel'),
buttons: [
{ color: sharedColors.black, textColor: sharedColors.white },
{ color: sharedColors.black, textColor: sharedColors.black },
],
onOk: eraseWallet,
onCancel: () => {
console.log('ON CANCEL IN createDeleteDefinitiveConfirmationConfig')
setVisible(false)
setConfirmationModalConfig(createDeleteConfirmationConfigFn())
},
}
},
[t, eraseWallet, setVisible],
)

const createDeleteConfirmationConfig =
useCallback((): ConfirmationModalConfig => {
return {
color: sharedColors.dangerLight,
title: t('wallet_backup_delete_confirmation_title') as string,
titleColor: sharedColors.black,
description: t(
'wallet_backup_delete_confirmation_description',
) as string,
descriptionColor: sharedColors.black,
okText: t('Delete') as string,
cancelText: t('Cancel') as string,
buttons: [
{ color: sharedColors.black, textColor: sharedColors.white },
{ color: sharedColors.black, textColor: sharedColors.black },
],
onOk: () => {
setConfirmationModalConfig(
createDeleteDefinitiveConfirmationConfig(
createDeleteConfirmationConfig,
),
)
},
onCancel: () => {
setVisible(false)
},
}
}, [t, createDeleteDefinitiveConfirmationConfig, setVisible])

const [confirmationModalConfig, setConfirmationModalConfig] =
useState<ConfirmationModalConfig>(createDeleteConfirmationConfig)

return (
<ConfirmationModal
isVisible={isVisible}
color={confirmationModalConfig.color}
title={confirmationModalConfig.title}
titleColor={confirmationModalConfig.titleColor}
description={confirmationModalConfig.description}
descriptionColor={confirmationModalConfig.descriptionColor}
okText={confirmationModalConfig.okText}
cancelText={confirmationModalConfig.cancelText}
buttons={confirmationModalConfig.buttons}
onOk={confirmationModalConfig.onOk}
onCancel={confirmationModalConfig.onCancel}
/>
)
}
6 changes: 3 additions & 3 deletions src/core/Core.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const Core = () => {

const { unlocked, active } = useStateSubscription()

const unlockAppSetMnemonic = useCallback(async () => {
const unlockAppFn = useCallback(async () => {
try {
await dispatch(unlockApp({ isOffline })).unwrap()
} catch (err) {
Expand All @@ -48,8 +48,8 @@ export const Core = () => {
}, [dispatch, isOffline])

useEffect(() => {
unlockAppSetMnemonic()
}, [unlockAppSetMnemonic])
unlockAppFn()
}, [unlockAppFn])

return (
<SafeAreaProvider>
Expand Down
2 changes: 2 additions & 0 deletions src/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ const resources = {
'Register your username to allow others to send you funds more easily. In case you do not have any RIF funds you can ask a friend to send you some RIF.',
info_box_close_button: 'close',
initial_screen_title: 'Wallet',
initial_screen_button_retry_login: 'Retry unlock',
initial_screen_button_erase_wallet: 'Erase the Wallet',
initial_screen_button_create: 'Create a wallet',
initial_screen_button_import: 'Import existing wallet',
initial_screen_welcome_footer:
Expand Down
25 changes: 17 additions & 8 deletions src/navigation/createKeysNavigator/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import {
ConfirmNewMasterKeyScreen,
ImportMasterKeyScreen,
SecurityInformation,
RetryLogin,
} from 'screens/createKeys'
import { selectIsUnlocked } from 'store/slices/settingsSlice'
import { selectIsUnlocked, selectKeysExist } from 'store/slices/settingsSlice'
import { useAppSelector } from 'store/storeUtils'
import { PinScreen } from 'screens/pinScreen'

Expand All @@ -21,19 +22,27 @@ const Stack = createStackNavigator<CreateKeysStackParamList>()
const screensOptions = { headerShown: false }

export const CreateKeysNavigation = () => {
const keysExist = useAppSelector(selectKeysExist)
const { top } = useSafeAreaInsets()
const { t } = useTranslation()
const unlocked = useAppSelector(selectIsUnlocked)

return (
<Stack.Navigator initialRouteName={createKeysRouteNames.CreateKeys}>
{!unlocked && (
<Stack.Screen
name={createKeysRouteNames.CreateKeys}
component={CreateKeysScreen}
options={screensOptions}
/>
)}
{!unlocked &&
(!keysExist ? (
<Stack.Screen
name={createKeysRouteNames.CreateKeys}
component={CreateKeysScreen}
options={screensOptions}
/>
) : (
<Stack.Screen
name={createKeysRouteNames.RetryLogin}
component={RetryLogin}
options={screensOptions}
/>
))}
<Stack.Screen
name={createKeysRouteNames.NewMasterKey}
component={NewMasterKeyScreen}
Expand Down
2 changes: 2 additions & 0 deletions src/navigation/createKeysNavigator/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export enum createKeysRouteNames {
ImportMasterKey = 'ImportMasterKey',
RevealMasterKey = 'RevealMasterKey',
CreatePIN = 'CreatePIN',
RetryLogin = 'RetryLogin',
}

export type CreateKeysStackParamList = {
Expand All @@ -30,6 +31,7 @@ export type CreateKeysStackParamList = {
isChangeRequested: true
backScreen?: null
}
[createKeysRouteNames.RetryLogin]: undefined
}

export type CreateKeysScreenProps<T extends keyof CreateKeysStackParamList> =
Expand Down
8 changes: 7 additions & 1 deletion src/redux/rootReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ const migrations = {

const settingsPersistConfig: PersistConfig<SettingsSlice> = {
key: 'settings',
whitelist: ['pin', 'chainId', 'isFirstLaunch', 'usedBitcoinAddresses'],
whitelist: [
'pin',
'chainId',
'keysExist',
'isFirstLaunch',
'usedBitcoinAddresses',
],
storage: reduxStorage,
}

Expand Down
14 changes: 14 additions & 0 deletions src/redux/slices/settingsSlice/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export const createWallet = createAsyncThunk<
// unclock the app
thunkAPI.dispatch(setUnlocked(true))

thunkAPI.dispatch(setKeysExist(true))

// create fetcher
//@TODO: refactor socket initialization, it repeats several times
thunkAPI.dispatch(setChainId(chainId))
Expand Down Expand Up @@ -149,10 +151,16 @@ export const unlockApp = createAsyncThunk<

const serializedKeys = await getKeys()
const { chainId } = thunkAPI.getState().settings

if (!serializedKeys) {
// if keys do not exist, set to false
thunkAPI.dispatch(setKeysExist(false))
return thunkAPI.rejectWithValue('No Existing Keys')
}

// if keys do exist, set to true
thunkAPI.dispatch(setKeysExist(true))

const { pinUnlocked, isOffline } = payload
const supportedBiometry = await getSupportedBiometryType()

Expand Down Expand Up @@ -258,6 +266,7 @@ export const resetApp = createAsyncThunk(
thunkAPI.dispatch(deleteProfile())
thunkAPI.dispatch(setPreviouslyUnlocked(false))
thunkAPI.dispatch(setPinState(null))
thunkAPI.dispatch(setKeysExist(false))
resetMainStorage()
return 'deleted'
} catch (err) {
Expand Down Expand Up @@ -300,6 +309,7 @@ export const resetApp = createAsyncThunk(
// )

const initialState: SettingsSlice = {
keysExist: false,
isFirstLaunch: true,
isSetup: false,
topColor: sharedColors.primary,
Expand All @@ -324,6 +334,9 @@ const settingsSlice = createSlice({
name: 'settings',
initialState,
reducers: {
setKeysExist: (state, { payload }: PayloadAction<boolean>) => {
state.keysExist = payload
},
setIsFirstLaunch: (state, { payload }: PayloadAction<boolean>) => {
state.isFirstLaunch = payload
},
Expand Down Expand Up @@ -468,6 +481,7 @@ const settingsSlice = createSlice({
})

export const {
setKeysExist,
setIsFirstLaunch,
setIsSetup,
changeTopColor,
Expand Down
2 changes: 2 additions & 0 deletions src/redux/slices/settingsSlice/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { RootState } from 'store/store'

export const selectKeysExist = ({ settings }: RootState) => settings.keysExist

export const selectIsFirstLaunch = ({ settings }: RootState) =>
settings.isFirstLaunch

Expand Down
1 change: 1 addition & 0 deletions src/redux/slices/settingsSlice/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export interface Bitcoin {
}

export interface SettingsSlice {
keysExist: boolean
isFirstLaunch: boolean
isSetup: boolean
requests: RequestWithBitcoin[]
Expand Down
2 changes: 1 addition & 1 deletion src/screens/createKeys/CreateKeysScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const styles = StyleSheet.create({
position: 'absolute',
width: 185,
lineHeight: 15.6,
bottom: WINDOW_HEIGHT * 0.2,
bottom: WINDOW_HEIGHT * 0.26,
left: 24,
}),
importWalletButton: castStyle.view({
Expand Down
1 change: 1 addition & 0 deletions src/screens/createKeys/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './new/index'
export * from './import/index'
export * from './CreateKeysScreen'
export * from './SecurityInformation'
export * from './retryLogin'
49 changes: 49 additions & 0 deletions src/screens/createKeys/retryLogin/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useCallback, useState } from 'react'
import { StyleSheet, View } from 'react-native'
import { useTranslation } from 'react-i18next'

import { sharedColors, sharedStyles } from 'shared/constants'
import { AppButton } from 'components/index'
import { unlockApp } from 'store/slices/settingsSlice'
import { castStyle } from 'shared/utils'
import { DeleteWalletModal } from 'components/modal/deleteWalletModal'
import { useAppDispatch } from 'store/storeUtils'

export const RetryLogin = () => {
const dispatch = useAppDispatch()
const { t } = useTranslation()
const [isDeleteConfirmationVisible, setIsDeleteConfirmationVisible] =
useState<boolean>(false)

const retryLogin = useCallback(() => {
dispatch(unlockApp({}))
}, [dispatch])

return (
<View style={[sharedStyles.screen, sharedStyles.contentCenter]}>
<AppButton
onPress={retryLogin}
title={t('initial_screen_button_retry_login')}
color={sharedColors.white}
textColor={sharedColors.black}
/>
<AppButton
onPress={() => setIsDeleteConfirmationVisible(true)}
title={t('initial_screen_button_erase_wallet')}
style={styles.btn}
color={sharedColors.danger}
textColor={sharedColors.white}
/>
<DeleteWalletModal
isVisible={isDeleteConfirmationVisible}
setVisible={setIsDeleteConfirmationVisible}
/>
</View>
)
}

const styles = StyleSheet.create({
btn: castStyle.view({
marginTop: 12,
}),
})
Loading

0 comments on commit d652009

Please sign in to comment.