Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
exports[`FormModalHeaderWrapper renders children and buttons correctly 1`] = `
<div>
<div
class="html-div x1ghz6dp x1717udv x1aw8gp0 xzu4p3z x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
class="html-div x1ghz6dp x1717udv xet4sn1 x13zp6ng x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
data-theme="dark"
>

Expand Down Expand Up @@ -40,7 +40,7 @@ exports[`FormModalHeaderWrapper renders children and buttons correctly 1`] = `
exports[`FormModalHeaderWrapper renders with empty content 1`] = `
<div>
<div
class="html-div x1ghz6dp x1717udv x1aw8gp0 xzu4p3z x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
class="html-div x1ghz6dp x1717udv xet4sn1 x13zp6ng x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
data-theme="dark"
>

Expand All @@ -67,7 +67,7 @@ exports[`FormModalHeaderWrapper renders with empty content 1`] = `
exports[`FormModalHeaderWrapper renders without buttons 1`] = `
<div>
<div
class="html-div x1ghz6dp x1717udv x1aw8gp0 xzu4p3z x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
class="html-div x1ghz6dp x1717udv xet4sn1 x13zp6ng x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
data-theme="dark"
>

Expand Down Expand Up @@ -99,7 +99,7 @@ exports[`FormModalHeaderWrapper renders without buttons 1`] = `
exports[`FormModalHeaderWrapper renders without children 1`] = `
<div>
<div
class="html-div x1ghz6dp x1717udv x1aw8gp0 xzu4p3z x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
class="html-div x1ghz6dp x1717udv xet4sn1 x13zp6ng x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
data-theme="dark"
>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
exports[`LoadingOverlay renders correctly 1`] = `
<div>
<div
class="html-div x1ghz6dp x1717udv x1aw8gp0 xzu4p3z x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
class="html-div x1ghz6dp x1717udv xet4sn1 x13zp6ng x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
data-theme="dark"
>
<div
Expand Down
2 changes: 1 addition & 1 deletion src/components/Overlay/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
exports[`Overlay does not render when isRendered is false 1`] = `
<div>
<div
class="html-div x1ghz6dp x1717udv x1aw8gp0 xzu4p3z x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
class="html-div x1ghz6dp x1717udv xet4sn1 x13zp6ng x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
data-theme="dark"
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
exports[`RadioSelect renders the component with title and options 1`] = `
<div>
<div
class="html-div x1ghz6dp x1717udv x1aw8gp0 xzu4p3z x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
class="html-div x1ghz6dp x1717udv xet4sn1 x13zp6ng x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
data-theme="dark"
>
<div
Expand Down
3 changes: 2 additions & 1 deletion src/constants/localStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ export const LOCAL_STORAGE_KEYS = {
AUTO_LOCK_ENABLED: 'auto-lock-enabled',
AUTO_LOCK_TIMEOUT_MS: 'auto-lock-timeout-ms',
NM_CLIENT_PUBLIC_KEY: 'nm-client-public-key',
EXTENSION_DIALOG_DISMISSED: 'extension-dialog-dismissed'
EXTENSION_DIALOG_DISMISSED: 'extension-dialog-dismissed',
LAST_OPENED_VAULT_ID: 'last-opened-vault-id'
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createStyles } from './CreateOrEditVaultModalContent.styles'
import { useGlobalLoading } from '../../../context/LoadingContext'
import { useRouter } from '../../../context/RouterContext'
import { useTranslation } from '../../../hooks/useTranslation'
import { setLastOpenedVaultId } from '../../../utils/lastOpenedVaultStorage'
import { logger } from '../../../utils/logger'

export type CreateOrEditVaultModalContentProps = {
Expand Down Expand Up @@ -85,13 +86,15 @@ export const CreateOrEditVaultModalContent = ({
try {
setIsLoading(true)

await createVault({
const createdVault = await createVault({
name: formValues.name,
password: ''
})

await addDevice()

setLastOpenedVaultId(createdVault?.id)

onSuccess?.()
if(shouldRedirectToMain) {
navigate('vault', { recordType: 'all' })
Expand Down
3 changes: 3 additions & 0 deletions src/hooks/useVaultSwitch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useVault, type Vault } from '@tetherto/pearpass-lib-vault'
import { VaultPasswordFormModalContent } from '../containers/Modal/VaultPasswordFormModalContent'
import { useLoadingContext } from '../context/LoadingContext'
import { useModal } from '../context/ModalContext'
import { setLastOpenedVaultId } from '../utils/lastOpenedVaultStorage'
import { logger } from '../utils/logger'
/**
* Switch active vault with the same flow everywhere: optional password modal
Expand Down Expand Up @@ -42,6 +43,7 @@ export function useVaultSwitch() {
setIsLoading(true)
try {
await refetchVault(vault.id, { password })
setLastOpenedVaultId(vault.id)
closeModal()
await onSuccess()
} finally {
Expand All @@ -54,6 +56,7 @@ export function useVaultSwitch() {
}

await refetchVault(vault.id)
setLastOpenedVaultId(vault.id)
await onSuccess()
} catch (error) {
logger.error('useVaultSwitch', 'Error switching to vault:', error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

exports[`InputField Component matches snapshot for default variant 1`] = `
<div
class="html-div x1ghz6dp x1717udv x1aw8gp0 xzu4p3z x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
class="html-div x1ghz6dp x1717udv xet4sn1 x13zp6ng x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
data-theme="dark"
>
<div
Expand Down Expand Up @@ -41,7 +41,7 @@ exports[`InputField Component matches snapshot for default variant 1`] = `

exports[`InputField Component matches snapshot for outline variant 1`] = `
<div
class="html-div x1ghz6dp x1717udv x1aw8gp0 xzu4p3z x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
class="html-div x1ghz6dp x1717udv xet4sn1 x13zp6ng x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
data-theme="dark"
>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

exports[`TextArea Component matches snapshot for default variant 1`] = `
<div
class="html-div x1ghz6dp x1717udv x1aw8gp0 xzu4p3z x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
class="html-div x1ghz6dp x1717udv xet4sn1 x13zp6ng x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
data-theme="dark"
>
<div
Expand All @@ -25,7 +25,7 @@ exports[`TextArea Component matches snapshot for default variant 1`] = `

exports[`TextArea Component matches snapshot for report variant 1`] = `
<div
class="html-div x1ghz6dp x1717udv x1aw8gp0 xzu4p3z x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
class="html-div x1ghz6dp x1717udv xet4sn1 x13zp6ng x78zum5 xdt5ytf x1us19tq x5yr21d xh8yej3"
data-theme="dark"
>
<div
Expand Down
4 changes: 3 additions & 1 deletion src/pages/WelcomePage/CardCreateMasterPassword/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { useGlobalLoading } from '../../../context/LoadingContext'
import { useRouter } from '../../../context/RouterContext'
import { clearStaleVaultsDir } from '../../../electron'
import { useTranslation } from '../../../hooks/useTranslation'
import { setLastOpenedVaultId } from '../../../utils/lastOpenedVaultStorage'
import { logger } from '../../../utils/logger'
import { STRENGTH_MAP } from '../../../constants/password'

Expand Down Expand Up @@ -121,8 +122,9 @@ export const CardCreateMasterPassword = () => {
await createMasterPassword(createBuffer)
await logIn({ password: loginBuffer })
await initVaults({ password: loginBuffer })
await createVault({ name: t('Personal') })
const createdVault = await createVault({ name: t('Personal') })
await addDevice()
setLastOpenedVaultId(createdVault?.id)
navigate('vault', { recordType: 'all' })
setIsLoading(false)
} catch (error) {
Expand Down
25 changes: 19 additions & 6 deletions src/pages/WelcomePage/CardUnlockPearPass/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import { NAVIGATION_ROUTES } from '../../../constants/navigation'
import { useGlobalLoading } from '../../../context/LoadingContext'
import { useRouter } from '../../../context/RouterContext'
import { useTranslation } from '../../../hooks/useTranslation'
import {
getLastOpenedVaultId,
setLastOpenedVaultId
} from '../../../utils/lastOpenedVaultStorage'
import { logger } from '../../../utils/logger'
import { sortByName } from '../../../utils/sortByName'
import {
Expand Down Expand Up @@ -71,20 +75,29 @@ export const CardUnlockPearPass = (): React.ReactElement => {
await initVaults({ password: passwordBuffer })

const vaults = await refetchVaults()
const firstVault = sortByName(vaults)[0]
const sortedVaults = sortByName(vaults)
const storedVaultId = getLastOpenedVaultId()
const targetVault =
sortedVaults.find((vault) => vault.id === storedVaultId) ??
sortedVaults[0]

if (firstVault) {
const isProtected = await isVaultProtected(firstVault.id)
if (targetVault) {
const isProtected = await isVaultProtected(targetVault.id)

if (isProtected) {
navigate(currentPage, { state: 'vaultPassword', vaultId: firstVault.id })
navigate(currentPage, {
state: 'vaultPassword',
vaultId: targetVault.id
})
} else {
await refetchVault(firstVault.id)
await refetchVault(targetVault.id)
setLastOpenedVaultId(targetVault.id)
navigate('vault', { recordType: 'all' })
}
} else {
await createVault({ name: t('Personal') })
const createdVault = await createVault({ name: t('Personal') })
await addDevice()
setLastOpenedVaultId(createdVault?.id)
navigate('vault', { recordType: 'all' })
}
} catch (submitError) {
Expand Down
41 changes: 41 additions & 0 deletions src/utils/lastOpenedVaultStorage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { logger } from './logger'
import { LOCAL_STORAGE_KEYS } from '../constants/localStorage'

/**
* Read the id of the vault the user last opened.
* @returns {string | null} the stored vault id, or null if missing/unreadable
*/
export const getLastOpenedVaultId = () => {
try {
return localStorage.getItem(LOCAL_STORAGE_KEYS.LAST_OPENED_VAULT_ID)
} catch (error) {
logger.error(
'lastOpenedVaultStorage',
'Error reading last opened vault id:',
error
)
return null
}
}

/**
* Persist the id of the vault the user just opened. No-ops on falsy ids and
* swallows storage errors so it never breaks the calling flow.
* @param {string} vaultId - id of the vault that became active
*/
export const setLastOpenedVaultId = (vaultId) => {
if (!vaultId) return

try {
localStorage.setItem(
LOCAL_STORAGE_KEYS.LAST_OPENED_VAULT_ID,
String(vaultId)
)
} catch (error) {
logger.error(
'lastOpenedVaultStorage',
'Error storing last opened vault id:',
error
)
}
}
82 changes: 82 additions & 0 deletions src/utils/lastOpenedVaultStorage.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
getLastOpenedVaultId,
setLastOpenedVaultId
} from './lastOpenedVaultStorage'
import { logger } from './logger'
import { LOCAL_STORAGE_KEYS } from '../constants/localStorage'

describe('lastOpenedVaultStorage', () => {
beforeEach(() => {
localStorage.clear()
jest.restoreAllMocks()
})

describe('setLastOpenedVaultId', () => {
it('stores a provided id in localStorage', () => {
setLastOpenedVaultId('vault-123')

expect(
localStorage.getItem(LOCAL_STORAGE_KEYS.LAST_OPENED_VAULT_ID)
).toBe('vault-123')
})

it('coerces non-string ids to strings', () => {
setLastOpenedVaultId(42)

expect(
localStorage.getItem(LOCAL_STORAGE_KEYS.LAST_OPENED_VAULT_ID)
).toBe('42')
})

it('no-ops on falsy ids', () => {
setLastOpenedVaultId('')
setLastOpenedVaultId(null)
setLastOpenedVaultId(undefined)
setLastOpenedVaultId(0)

expect(
localStorage.getItem(LOCAL_STORAGE_KEYS.LAST_OPENED_VAULT_ID)
).toBeNull()
})

it('swallows and logs errors when writing fails', () => {
const loggerSpy = jest.spyOn(logger, 'error').mockImplementation(() => {})
const setItemSpy = jest
.spyOn(Storage.prototype, 'setItem')
.mockImplementation(() => {
throw new Error('quota exceeded')
})

expect(() => setLastOpenedVaultId('vault-123')).not.toThrow()
expect(loggerSpy).toHaveBeenCalled()

setItemSpy.mockRestore()
})
})

describe('getLastOpenedVaultId', () => {
it('returns null when nothing is stored', () => {
expect(getLastOpenedVaultId()).toBeNull()
})

it('returns the stored id', () => {
setLastOpenedVaultId('vault-abc')

expect(getLastOpenedVaultId()).toBe('vault-abc')
})

it('returns null and logs when reading fails', () => {
const loggerSpy = jest.spyOn(logger, 'error').mockImplementation(() => {})
const getItemSpy = jest
.spyOn(Storage.prototype, 'getItem')
.mockImplementation(() => {
throw new Error('read failure')
})

expect(getLastOpenedVaultId()).toBeNull()
expect(loggerSpy).toHaveBeenCalled()

getItemSpy.mockRestore()
})
})
})
Loading