-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui): allow variable-length PINs (#4083)
VxSuite PINs are still always 6 digits. RAVE PINs are variable, and this is ported from RAVE since it may be useful in the future here and will make RAVE rebases easier.
- Loading branch information
1 parent
ded2954
commit 99d51df
Showing
9 changed files
with
344 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
import { PinLength } from './utils/pin_length'; | ||
|
||
export const CHECK_ICON = '✓'; | ||
|
||
export const FONT_SIZES = [22, 28, 36, 56]; | ||
export const DEFAULT_FONT_SIZE = 1; | ||
export const LARGE_DISPLAY_FONT_SIZE = 3; | ||
|
||
export const SECURITY_PIN_LENGTH = 6; | ||
export const SECURITY_PIN_LENGTH = PinLength.exactly(6); | ||
|
||
export const DEFAULT_NUMBER_POLL_REPORT_COPIES = 2; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { act, renderHook } from '@testing-library/react'; | ||
import { usePinEntry } from './use_pin_entry'; | ||
import { PinLength } from '../utils/pin_length'; | ||
|
||
test('defaults', () => { | ||
const pinEntry = renderHook(() => | ||
usePinEntry({ pinLength: PinLength.exactly(6) }) | ||
); | ||
|
||
expect(pinEntry.result.current).toEqual( | ||
expect.objectContaining({ | ||
current: '', | ||
display: '- - - - - -', | ||
}) | ||
); | ||
}); | ||
|
||
test('PIN entry', () => { | ||
const pinEntry = renderHook(() => | ||
usePinEntry({ pinLength: PinLength.exactly(6) }) | ||
); | ||
|
||
act(() => { | ||
pinEntry.result.current.handleDigit(1); | ||
}); | ||
act(() => { | ||
pinEntry.result.current.handleDigit(2); | ||
}); | ||
act(() => { | ||
pinEntry.result.current.handleDigit(3); | ||
}); | ||
act(() => { | ||
pinEntry.result.current.handleDigit(4); | ||
}); | ||
act(() => { | ||
pinEntry.result.current.handleDigit(5); | ||
}); | ||
act(() => { | ||
pinEntry.result.current.handleDigit(6); | ||
}); | ||
|
||
expect(pinEntry.result.current).toEqual( | ||
expect.objectContaining({ | ||
current: '123456', | ||
display: '• • • • • •', | ||
}) | ||
); | ||
|
||
act(() => { | ||
pinEntry.result.current.handleBackspace(); | ||
}); | ||
|
||
expect(pinEntry.result.current).toEqual( | ||
expect.objectContaining({ | ||
current: '12345', | ||
display: '• • • • • -', | ||
}) | ||
); | ||
|
||
act(() => { | ||
pinEntry.result.current.reset(); | ||
}); | ||
|
||
expect(pinEntry.result.current).toEqual( | ||
expect.objectContaining({ | ||
current: '', | ||
display: '- - - - - -', | ||
}) | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import React, { useCallback, useMemo, useState } from 'react'; | ||
import { PinLength } from '../utils/pin_length'; | ||
|
||
/** | ||
* Options for the {@link usePinEntry} hook. | ||
*/ | ||
export interface UsePinEntryOptions { | ||
pinLength: PinLength; | ||
} | ||
|
||
/** | ||
* Returned by the {@link usePinEntry} hook. | ||
*/ | ||
export interface PinEntry { | ||
current: string; | ||
display: string; | ||
setCurrent: React.Dispatch<React.SetStateAction<string>>; | ||
reset: () => string; | ||
handleDigit: (digit: number) => string; | ||
handleBackspace: () => string; | ||
} | ||
|
||
/** | ||
* A hook for managing PIN entry. The returned object contains the current PIN, | ||
* a display string for the PIN, and methods for updating the PIN. | ||
*/ | ||
export function usePinEntry({ pinLength }: UsePinEntryOptions): PinEntry { | ||
const [current, setCurrent] = useState(''); | ||
|
||
const reset = useCallback(() => { | ||
setCurrent(''); | ||
return ''; | ||
}, []); | ||
|
||
const handleDigit = useCallback( | ||
(digit: number) => { | ||
const pin = `${current}${digit}`.slice(0, pinLength.max); | ||
setCurrent(pin); | ||
return pin; | ||
}, | ||
[current, pinLength.max] | ||
); | ||
|
||
const handleBackspace = useCallback(() => { | ||
const pin = current.slice(0, -1); | ||
setCurrent(pin); | ||
return pin; | ||
}, [current]); | ||
|
||
const display = '•' | ||
.repeat(current.length) | ||
.padEnd(pinLength.max, '-') | ||
.split('') | ||
.join(' '); | ||
|
||
return useMemo( | ||
() => ({ | ||
current, | ||
display, | ||
setCurrent, | ||
reset, | ||
handleDigit, | ||
handleBackspace, | ||
}), | ||
[current, display, setCurrent, reset, handleDigit, handleBackspace] | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { Meta, StoryObj } from '@storybook/react'; | ||
import React from 'react'; | ||
|
||
import { | ||
UnlockMachineScreen, | ||
UnlockMachineScreenProps, | ||
} from './unlock_machine_screen'; | ||
import { SECURITY_PIN_LENGTH } from './globals'; | ||
import { PinLength } from './utils/pin_length'; | ||
|
||
type PropsAndCustomArgs = React.ComponentProps<typeof UnlockMachineScreen> & { | ||
minPinLength: number; | ||
maxPinLength: number; | ||
}; | ||
|
||
const initialProps: PropsAndCustomArgs = { | ||
auth: { | ||
user: { | ||
role: 'election_manager', | ||
electionHash: 'deadbeef', | ||
jurisdiction: 'jxn', | ||
}, | ||
status: 'checking_pin', | ||
}, | ||
checkPin: async () => {}, | ||
minPinLength: SECURITY_PIN_LENGTH.min, | ||
maxPinLength: SECURITY_PIN_LENGTH.max, | ||
}; | ||
|
||
const meta: Meta<UnlockMachineScreenProps> = { | ||
title: 'libs-ui/UnlockMachineScreen', | ||
component: UnlockMachineScreen, | ||
args: initialProps, | ||
argTypes: { | ||
minPinLength: { | ||
control: { | ||
type: 'range', | ||
min: 1, | ||
max: 10, | ||
step: 1, | ||
}, | ||
}, | ||
maxPinLength: { | ||
control: { | ||
type: 'range', | ||
min: 1, | ||
max: 10, | ||
step: 1, | ||
}, | ||
}, | ||
pinLength: { | ||
if: { global: 'showPinLength', exists: true }, | ||
}, | ||
}, | ||
}; | ||
|
||
export default meta; | ||
|
||
export const Primary: StoryObj<PropsAndCustomArgs> = { | ||
render: ({ minPinLength, maxPinLength, ...rest }) => ( | ||
<UnlockMachineScreen | ||
{...rest} | ||
pinLength={PinLength.range(minPinLength, maxPinLength)} | ||
/> | ||
), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.