diff --git a/index.html b/index.html index cfeefdf..e3f92fa 100644 --- a/index.html +++ b/index.html @@ -53,7 +53,7 @@

What this tool can't do:

diff --git a/src/App.tsx b/src/App.tsx index c3f9a5d..0af46ad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,16 +3,22 @@ import {NumberInput, TextInput} from './Input'; import OTPOutput, {HashAlgorithm} from './OTPOutput'; import Select from './Select'; import Collapsible from './Collapsible'; -import ActionLink from './ActionLink.tsx'; +import ActionLink from './ActionLink'; +import {type State, defaults, serializeState, deserializeState} from './state'; + +function parseState() { + if (window.location.hash.startsWith('#!')) { + return deserializeState(window.location.hash.slice(2)); + } + return null; +} function App() { const [advanced, setAdvanced] = useState(false); - const [secret, setSecret] = React.useState(''); - const [step, setStep] = React.useState(30); + const [state, setState] = React.useState(() => parseState() || defaults); + const {secret, step, digits, algorithm} = state; const [offset, setOffset] = React.useState(0); - const [digits, setDigits] = React.useState(6); - const [algorithm, setAlgorithm] = React.useState('sha1'); const validStep = step > 0; const validDigits = digits > 0 && digits <= 10; @@ -41,12 +47,47 @@ function App() { setAdvanced(false); }, []); - const onReset = React.useCallback(() => { - setStep(30); - setDigits(6); - setAlgorithm('sha1'); + const setSecret = React.useCallback( + (secret: string) => setState((state) => ({...state, secret})), + [], + ); + + const setStep = React.useCallback( + (step: number) => setState((state) => ({...state, step})), + [], + ); + + const setDigits = React.useCallback( + (digits: number) => setState((state) => ({...state, digits})), + [], + ); + + const setAlgorithm = React.useCallback( + (algorithm: string) => setState((state) => ({...state, algorithm})), + [], + ); + + const onReset = React.useCallback( + () => setState((state) => ({...state, step: 30, digits: 6, algorithm: 'sha1'})), + [], + ); + + React.useEffect(() => { + const value = serializeState(state); + console.log(value); + history.replaceState(null, '', window.location.pathname + window.location.search + (value && `#!${value}`)); + }, [state]); + + const onHashChange = React.useCallback(() => { + const state = parseState(); + state && setState(state); }, []); + React.useEffect(() => { + window.addEventListener('hashchange', onHashChange); + return () => window.removeEventListener('hashchange', onHashChange); + }, [onHashChange]); + return (
diff --git a/src/OTPOutput.tsx b/src/OTPOutput.tsx index 1727af9..1739433 100644 --- a/src/OTPOutput.tsx +++ b/src/OTPOutput.tsx @@ -4,7 +4,7 @@ import {createDigest} from '@otplib/plugin-crypto-js'; import classNames from 'classnames'; import CopyButton from './CopyButton.tsx'; -const ALGORITHMS = { +export const ALGORITHMS = { sha1: HashAlgorithms.SHA1, sha256: HashAlgorithms.SHA256, sha512: HashAlgorithms.SHA512, diff --git a/src/state.tsx b/src/state.tsx new file mode 100644 index 0000000..3cd8e2e --- /dev/null +++ b/src/state.tsx @@ -0,0 +1,35 @@ +import {ALGORITHMS, HashAlgorithm} from './OTPOutput'; + +export type State = { + secret: string; + step: number; + digits: number; + algorithm: HashAlgorithm; +}; + +export const defaults: State = { + secret: '', + step: 30, + digits: 6, + algorithm: 'sha1', +} as const; + +export function serializeState(state: State): string { + const values = ['secret', 'step', 'digits', 'algorithm'].map( + (key) => state[key] !== defaults[key] ? encodeURIComponent(state[key]) : '', + ); + while (values[values.length - 1] === '') { + values.pop(); + } + return values.join('/'); +} + +export function deserializeState(data: string): State { + const values = data.split('/').map(decodeURIComponent); + return { + secret: values[0] || defaults.secret, + step: +values[1] || defaults.step, + digits: +values[2] || defaults.digits, + algorithm: ALGORITHMS[values[3]] !== undefined ? values[3] : defaults.algorithm, + }; +}