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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased (develop)

- fixed: Password reminder no longer stops reappearing after unlocking Account Settings

## 4.51.0 (staging)

- added: Push info-server attestation tokens into edge-core-js via `setAttestationToken` so the login server can skip CAPTCHA for attested devices, and allow `LOGIN_SERVER` / `INFO_SERVER` env overrides for local E2E stacks.
Expand Down
39 changes: 39 additions & 0 deletions src/__tests__/reducers/passwordReminderReducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import {
initialState,
MAX_NON_PASSWORD_DAYS_LIMIT,
MAX_NON_PASSWORD_LOGINS_LIMIT,
NON_PASSWORD_DAYS_GROWTH_RATE,
NON_PASSWORD_LOGINS_GROWTH_RATE,
passwordReminder,
type PasswordReminderReducerAction,
untranslatedReducer as uut
} from '../../reducers/PasswordReminderReducer'
import type { Action } from '../../types/reduxTypes'
import { daysBetween, MILLISECONDS_PER_DAY } from '../../util/utils'

describe('PasswordReminder', () => {
Expand Down Expand Up @@ -263,4 +267,39 @@ describe('PasswordReminder', () => {
})
})
})
describe('Account Settings unlock', () => {
// Unlocking Account Settings runs the password through `validatePassword`,
// which dispatches `PASSWORD_USED`, and then dispatches
// `UI/SETTINGS/SET_SETTINGS_LOCK` to record the unlocked state. Only the
// first of those may count as a password use: counting both squares the
// reminder thresholds and the reminder stops reappearing.
test('Counts one password use per unlock', () => {
const unlockActions: Action[] = [
{ type: 'PASSWORD_USED' },
{ type: 'UI/SETTINGS/SET_SETTINGS_LOCK', data: false }
]

const actual = unlockActions.reduce(
(state, action) => passwordReminder(state, action),
initialState
)

expect(actual.passwordUseCount).toEqual(1)
expect(actual.nonPasswordLoginsLimit).toEqual(
NON_PASSWORD_LOGINS_GROWTH_RATE
)
expect(actual.nonPasswordDaysLimit).toEqual(NON_PASSWORD_DAYS_GROWTH_RATE)
})

test('Lock state changes alone are not password uses', () => {
const action: Action = {
type: 'UI/SETTINGS/SET_SETTINGS_LOCK',
data: false
}

const actual = passwordReminder(initialState, action)

expect(actual).toEqual(initialState)
})
})
})
5 changes: 1 addition & 4 deletions src/components/scenes/SettingsScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,8 @@ export const SettingsScene: React.FC<Props> = props => {
}
)
if (password == null) return true
// `showUnlockSettingsModal` already unlocked the settings.
setValidatedPassword(password)
dispatch({
type: 'UI/SETTINGS/SET_SETTINGS_LOCK',
data: false
})
}
return false
}
Expand Down
12 changes: 4 additions & 8 deletions src/reducers/PasswordReminderReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,10 @@ function translateAction(action: Action): PasswordReminderReducerAction {
}
}

// `validatePassword` dispatches this for every successful password entry,
// including the Account Settings unlock. It is the only signal counted here:
// each password entry must raise `passwordUseCount` exactly once, because the
// reminder thresholds grow as `GROWTH_RATE ** passwordUseCount`.
if (action.type === 'PASSWORD_USED') {
return {
type: 'PASSWORD_USED',
Expand All @@ -245,14 +249,6 @@ function translateAction(action: Action): PasswordReminderReducerAction {
}
}
}
if (action.type === 'UI/SETTINGS/SET_SETTINGS_LOCK' && !action.data) {
return {
type: 'PASSWORD_USED',
data: {
lastPasswordUseDate: Date.now()
}
}
}
if (action.type === 'PASSWORD_REMINDER_MODAL/CHECK_PASSWORD_SUCCESS') {
return {
type: 'PASSWORD_USED',
Expand Down
Loading