Skip to content

Commit ba39f5b

Browse files
committed
Open the app to the most recently used vault
1 parent 12db1b3 commit ba39f5b

8 files changed

Lines changed: 150 additions & 7 deletions

File tree

src/constants/asyncStorageKeys.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export const ASYNC_STORAGE_KEYS = {
22
FIRST_LAUNCH_KEY: 'hasAppLaunchedBefore',
33
FAILED_KEYS_KEY: 'failedSecureStoreKeys',
4-
LAST_ACTIVITY_AT: 'lastActivityAt'
4+
LAST_ACTIVITY_AT: 'lastActivityAt',
5+
LAST_OPENED_VAULT_ID: 'lastOpenedVaultId'
56
}

src/containers/Auth/UnlockVault.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
Text
1515
} from 'react-native'
1616
import { colors } from 'src/utils/colors'
17+
import { setLastOpenedVaultId } from 'src/utils/lastOpenedVaultStorage'
1718

1819
import {
1920
ButtonPrimary,
@@ -56,6 +57,7 @@ export const UnlockVault = ({ vaultId }) => {
5657
setIsLoading(true)
5758

5859
await refetchVault(vaultId, { password: values.password })
60+
await setLastOpenedVaultId(vaultId)
5961

6062
setIsLoading(false)
6163
navigation.replace('MainTabNavigator')

src/containers/Auth/VaultWizard/index.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Toast from 'react-native-toast-message'
99

1010
import { StepIdentity } from './StepIdentity'
1111
import { TOAST_CONFIG } from '../../../constants/toast'
12+
import { setLastOpenedVaultId } from '../../../utils/lastOpenedVaultStorage'
1213
import { logger } from '../../../utils/logger'
1314

1415
export const VaultWizard = () => {
@@ -34,11 +35,12 @@ export const VaultWizard = () => {
3435
try {
3536
setIsLoading(true)
3637
setFormData(data)
37-
await createVault({
38+
const createdVault = await createVault({
3839
name: data.name,
3940
password: data.usePassword ? data.password : undefined
4041
})
4142
await addDevice()
43+
await setLastOpenedVaultId(createdVault?.id)
4244
Toast.show({
4345
type: 'baseToast',
4446
text1: t`Vault created`,

src/hooks/useVaultSwitch.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useVault } from '@tetherto/pearpass-lib-vault'
55
import { VaultPasswordFormModalContent } from '../containers/Modal/VaultPasswordFormModalContent'
66
import { useGlobalLoading } from '../context/LoadingContext'
77
import { useModal } from '../context/ModalContext'
8+
import { setLastOpenedVaultId } from '../utils/lastOpenedVaultStorage'
89

910
/**
1011
* @param {{
@@ -53,6 +54,7 @@ export const useVaultSwitch = ({
5354
setIsLoading(true)
5455
try {
5556
await refetchVault(vault.id, { password })
57+
await setLastOpenedVaultId(vault.id)
5658
closeModal()
5759
onSwitchComplete?.()
5860
} finally {
@@ -63,6 +65,7 @@ export const useVaultSwitch = ({
6365
)
6466
} else {
6567
await refetchVault(vault.id)
68+
await setLastOpenedVaultId(vault.id)
6669
onSwitchComplete?.()
6770
}
6871
} finally {

src/screens/Auth/hooks/useAutoSelectVault.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { useNavigation } from '@react-navigation/native'
44
import { useVault, useVaults } from '@tetherto/pearpass-lib-vault'
55

66
import { NAVIGATION_ROUTES } from '../../../constants/navigation'
7+
import {
8+
getLastOpenedVaultId,
9+
setLastOpenedVaultId
10+
} from '../../../utils/lastOpenedVaultStorage'
711

812
export const useAutoSelectVault = () => {
913
const navigation = useNavigation()
@@ -28,18 +32,22 @@ export const useAutoSelectVault = () => {
2832
return
2933
}
3034

31-
const firstVault = vaults[0]
35+
// Prefer the vault the user last opened; fall back to the first one.
36+
const lastOpenedVaultId = await getLastOpenedVaultId()
37+
const targetVault =
38+
vaults.find((vault) => vault.id === lastOpenedVaultId) ?? vaults[0]
3239

33-
const isProtected = await isVaultProtected(firstVault.id)
40+
const isProtected = await isVaultProtected(targetVault.id)
3441
if (isProtected) {
3542
navigation.replace('Welcome', {
3643
state: NAVIGATION_ROUTES.UNLOCK,
37-
vaultId: firstVault.id
44+
vaultId: targetVault.id
3845
})
3946
return
4047
}
4148

42-
await refetchVault(firstVault.id)
49+
await refetchVault(targetVault.id)
50+
await setLastOpenedVaultId(targetVault.id)
4351
navigation.replace('MainTabNavigator')
4452
}, [refetchVaults, isVaultProtected, refetchVault, navigation])
4553

src/screens/Onboarding/hooks/usePasswordCreation.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import Toast from 'react-native-toast-message'
1818

1919
import { TOAST_CONFIG } from '../../../constants/toast'
2020
import { clearStaleVaultsDir } from '../../../utils/clearStaleVaultsDir'
21+
import { setLastOpenedVaultId } from '../../../utils/lastOpenedVaultStorage'
2122
import { logger } from '../../../utils/logger'
2223
import {
2324
getPasswordIndicatorVariant,
@@ -166,8 +167,9 @@ export const usePasswordCreation = () => {
166167
await logIn({ password: loginBuffer })
167168
await refetchUser()
168169
await initVaults({ password: loginBuffer })
169-
await createVault({ name: t`Personal` })
170+
const createdVault = await createVault({ name: t`Personal` })
170171
await addDevice()
172+
await setLastOpenedVaultId(createdVault?.id)
171173
clearBuffer(loginBuffer)
172174

173175
clearBuffer(passwordBuffer)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import AsyncStorage from '@react-native-async-storage/async-storage'
2+
3+
import { logger } from './logger'
4+
import { ASYNC_STORAGE_KEYS } from '../constants/asyncStorageKeys'
5+
6+
const { LAST_OPENED_VAULT_ID } = ASYNC_STORAGE_KEYS
7+
8+
export const setLastOpenedVaultId = async (vaultId) => {
9+
if (!vaultId) {
10+
return
11+
}
12+
try {
13+
await AsyncStorage.setItem(LAST_OPENED_VAULT_ID, String(vaultId))
14+
} catch (error) {
15+
logger.error('Error saving last opened vault id:', error)
16+
}
17+
}
18+
19+
export const getLastOpenedVaultId = async () => {
20+
try {
21+
return await AsyncStorage.getItem(LAST_OPENED_VAULT_ID)
22+
} catch (error) {
23+
logger.error('Error loading last opened vault id:', error)
24+
return null
25+
}
26+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import AsyncStorage from '@react-native-async-storage/async-storage'
2+
3+
import {
4+
getLastOpenedVaultId,
5+
setLastOpenedVaultId
6+
} from './lastOpenedVaultStorage'
7+
import { logger } from './logger'
8+
import { ASYNC_STORAGE_KEYS } from '../constants/asyncStorageKeys'
9+
10+
jest.mock('@react-native-async-storage/async-storage', () => ({
11+
setItem: jest.fn(),
12+
getItem: jest.fn()
13+
}))
14+
15+
jest.mock('./logger', () => ({
16+
logger: {
17+
error: jest.fn()
18+
}
19+
}))
20+
21+
describe('lastOpenedVaultStorage', () => {
22+
const { LAST_OPENED_VAULT_ID } = ASYNC_STORAGE_KEYS
23+
24+
beforeEach(() => {
25+
jest.clearAllMocks()
26+
})
27+
28+
describe('setLastOpenedVaultId', () => {
29+
it('stores the provided vault id', async () => {
30+
AsyncStorage.setItem.mockResolvedValue()
31+
32+
await setLastOpenedVaultId('vault-123')
33+
34+
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
35+
LAST_OPENED_VAULT_ID,
36+
'vault-123'
37+
)
38+
})
39+
40+
it('coerces a non-string vault id to a string', async () => {
41+
AsyncStorage.setItem.mockResolvedValue()
42+
43+
await setLastOpenedVaultId(42)
44+
45+
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
46+
LAST_OPENED_VAULT_ID,
47+
'42'
48+
)
49+
})
50+
51+
it.each([undefined, null, '', 0])(
52+
'does not store when vault id is falsy (%p)',
53+
async (vaultId) => {
54+
await setLastOpenedVaultId(vaultId)
55+
56+
expect(AsyncStorage.setItem).not.toHaveBeenCalled()
57+
}
58+
)
59+
60+
it('logs when saving fails', async () => {
61+
const error = new Error('write failed')
62+
AsyncStorage.setItem.mockRejectedValue(error)
63+
64+
await setLastOpenedVaultId('vault-123')
65+
66+
expect(logger.error).toHaveBeenCalledWith(
67+
'Error saving last opened vault id:',
68+
error
69+
)
70+
})
71+
})
72+
73+
describe('getLastOpenedVaultId', () => {
74+
it('returns the stored vault id', async () => {
75+
AsyncStorage.getItem.mockResolvedValue('vault-123')
76+
77+
await expect(getLastOpenedVaultId()).resolves.toBe('vault-123')
78+
expect(AsyncStorage.getItem).toHaveBeenCalledWith(LAST_OPENED_VAULT_ID)
79+
})
80+
81+
it('returns null when no value is stored', async () => {
82+
AsyncStorage.getItem.mockResolvedValue(null)
83+
84+
await expect(getLastOpenedVaultId()).resolves.toBeNull()
85+
})
86+
87+
it('logs when loading fails and returns null', async () => {
88+
const error = new Error('read failed')
89+
AsyncStorage.getItem.mockRejectedValue(error)
90+
91+
await expect(getLastOpenedVaultId()).resolves.toBeNull()
92+
93+
expect(logger.error).toHaveBeenCalledWith(
94+
'Error loading last opened vault id:',
95+
error
96+
)
97+
})
98+
})
99+
})

0 commit comments

Comments
 (0)