Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Login with magic code #1818

Merged
merged 29 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2abe1cb
fix: cannot login with email on PWA
Soxasora Jan 14, 2025
e130705
adjust other email templates
Soxasora Jan 14, 2025
cfc01c5
restore manual url on new user email
Soxasora Jan 14, 2025
c357bc7
no padding on button section
Soxasora Jan 14, 2025
2f29f30
cleanup
Soxasora Jan 14, 2025
33bbdc3
generate 6-digit bechh32 token
Soxasora Jan 14, 2025
8fb7cdb
token needs to be fed as lower case; validator case insensitive
Soxasora Jan 15, 2025
7a0e794
delete token if user has failed 3 times
Soxasora Jan 16, 2025
3efa20d
proposal: context-independent error page
Soxasora Jan 16, 2025
841cd9a
include expiration time on email page message
Soxasora Jan 16, 2025
8d41679
add expiration time to emails
Soxasora Jan 16, 2025
c65e62b
independent checkPWA function
Soxasora Jan 16, 2025
e663b75
restore token deletion if successful auth
Soxasora Jan 16, 2025
5c26cd6
final cleanup: remove unused function
Soxasora Jan 16, 2025
0b6da66
compact useVerificationToken
Soxasora Jan 20, 2025
2ad5d04
email.js: magic code for non-PWA users
Soxasora Jan 21, 2025
6155eef
adjust email templates
Soxasora Jan 21, 2025
9658807
MultiInput component; magic code via MultiInput
Soxasora Jan 29, 2025
a8b8102
Merge branch 'master' into magic_login
Soxasora Jan 29, 2025
5c7e17f
hotfix: revert length testing; larger width for inputs
Soxasora Jan 29, 2025
33a9d47
Merge branch 'master' into magic_login
huumn Jan 30, 2025
908b70e
manual bech32 token generation; no upperCase
Soxasora Jan 30, 2025
4fa221b
reverting to string concatenation
Soxasora Jan 30, 2025
766ca22
layout tweaks, fix error placement
Soxasora Jan 30, 2025
b0b8a38
Merge branch 'master' into magic_login
huumn Jan 31, 2025
390f79b
pastable inputs
Soxasora Jan 31, 2025
8085a71
Merge branch 'master' into magic_login
huumn Feb 2, 2025
dd2e75c
small nit fixes
huumn Feb 3, 2025
6527e22
less ambiguous error path
huumn Feb 4, 2025
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
117 changes: 115 additions & 2 deletions components/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ function FormGroup ({ className, label, children }) {

function InputInner ({
prepend, append, hint, warn, showValid, onChange, onBlur, overrideValue, appendValue,
innerRef, noForm, clear, onKeyDown, inputGroupClassName, debounce: debounceTime, maxLength,
innerRef, noForm, clear, onKeyDown, inputGroupClassName, debounce: debounceTime, maxLength, hideError,
...props
}) {
const [field, meta, helpers] = noForm ? [{}, {}, {}] : useField(props)
Expand Down Expand Up @@ -574,7 +574,7 @@ function InputInner ({
onKeyDown={onKeyDownInner}
onChange={onChangeInner}
onBlur={onBlurInner}
isInvalid={invalid}
isInvalid={!hideError && invalid} // if hideError is true, handle error showing separately
isValid={showValid && meta.initialValue !== meta.value && meta.touched && !meta.error}
/>
{(isClient && clear && field.value && !props.readOnly) &&
Expand Down Expand Up @@ -1241,5 +1241,118 @@ export function PasswordInput ({ newPass, qr, copy, readOnly, append, value: ini
)
}

export function MultiInput ({
name, label, groupClassName, length = 4, charLength = 1, upperCase, showSequence,
onChange, autoFocus, hideError, inputType = 'text',
...props
}) {
const [inputs, setInputs] = useState(new Array(length).fill(''))
const inputRefs = useRef(new Array(length).fill(null))
const [, meta, helpers] = useField({ name })

useEffect(() => {
autoFocus && inputRefs.current[0].focus() // focus the first input if autoFocus is true
}, [autoFocus])

const updateInputs = useCallback((newInputs) => {
setInputs(newInputs)
const combinedValue = newInputs.join('') // join the inputs to get the value
helpers.setValue(combinedValue) // set the value to the formik field
onChange?.(combinedValue)
}, [onChange, helpers])

const handleChange = useCallback((formik, e, index) => { // formik is not used but it's required to get the value
const value = e.target.value.slice(-charLength)
const processedValue = upperCase ? value.toUpperCase() : value // convert the input to uppercase if upperCase is tru

const newInputs = [...inputs]
newInputs[index] = processedValue
updateInputs(newInputs)

// focus the next input if the current input is filled
if (processedValue.length === charLength && index < length - 1) {
inputRefs.current[index + 1].focus()
}
}, [inputs, charLength, upperCase, onChange, length])

const handlePaste = useCallback((e) => {
e.preventDefault()
const pastedValues = e.clipboardData.getData('text').slice(0, length)
const processedValues = upperCase ? pastedValues.toUpperCase() : pastedValues
const chars = processedValues.split('')

const newInputs = [...inputs]
chars.forEach((char, i) => {
newInputs[i] = char.slice(0, charLength)
})

updateInputs(newInputs)
inputRefs.current[length - 1]?.focus() // simulating the paste by focusing the last input
}, [inputs, length, charLength, upperCase, updateInputs])

const handleKeyDown = useCallback((e, index) => {
switch (e.key) {
case 'Backspace': {
e.preventDefault()
const newInputs = [...inputs]
// if current input is empty move focus to the previous input else clear the current input
const targetIndex = inputs[index] === '' && index > 0 ? index - 1 : index
newInputs[targetIndex] = ''
updateInputs(newInputs)
inputRefs.current[targetIndex]?.focus()
break
}
case 'ArrowLeft': {
if (index > 0) { // focus the previous input if it's not the first input
e.preventDefault()
inputRefs.current[index - 1]?.focus()
}
break
}
case 'ArrowRight': {
if (index < length - 1) { // focus the next input if it's not the last input
e.preventDefault()
inputRefs.current[index + 1]?.focus()
}
break
}
}
}, [inputs, length, updateInputs])

return (
<FormGroup label={label} className={groupClassName}>
<div className='d-flex flex-row justify-content-center gap-2'>
{inputs.map((value, index) => (
<InputInner
inputGroupClassName='w-auto'
name={name}
key={index}
type={inputType}
value={value}
innerRef={(el) => { inputRefs.current[index] = el }}
onChange={(formik, e) => handleChange(formik, e, index)}
onKeyDown={e => handleKeyDown(e, index)}
onPaste={e => handlePaste(e, index)}
style={{
textAlign: 'center',
maxWidth: `${charLength * 44}px` // adjusts the max width of the input based on the charLength
}}
prepend={showSequence && <InputGroup.Text>{index + 1}</InputGroup.Text>} // show the index of the input
hideError
{...props}
/>
))}
</div>
<div>
{hideError && meta.touched && meta.error && ( // custom error message is showed if hideError is true
<BootstrapForm.Control.Feedback type='invalid' className='d-block'>
{meta.error}
</BootstrapForm.Control.Feedback>
)}
</div>
</FormGroup>
)
}

export const ClientInput = Client(Input)
export const ClientCheckbox = Client(Checkbox)
3 changes: 2 additions & 1 deletion components/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function EmailLoginForm ({ text, callbackUrl, multiAuth }) {
}}
schema={emailSchema}
onSubmit={async ({ email }) => {
window.sessionStorage.setItem('callback', JSON.stringify({ email, callbackUrl }))
signIn('email', { email, callbackUrl, multiAuth })
}}
>
Expand All @@ -41,7 +42,7 @@ const authErrorMessages = {
OAuthCallback: 'Error handling OAuth response. Try again or choose a different method.',
OAuthCreateAccount: 'Could not create OAuth account. Try again or choose a different method.',
EmailCreateAccount: 'Could not create Email account. Try again or choose a different method.',
Callback: 'Error in callback handler. Try again or choose a different method.',
Callback: 'Try again or choose a different method.',
OAuthAccountNotLinked: 'This auth method is linked to another account. To link to this account first unlink the other account.',
EmailSignin: 'Failed to send email. Make sure you entered your email address correctly.',
CredentialsSignin: 'Auth failed. Try again or choose a different method.',
Expand Down
2 changes: 2 additions & 0 deletions lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,5 @@ export const ZAP_UNDO_DELAY_MS = 5_000

export const WALLET_SEND_PAYMENT_TIMEOUT_MS = 150_000
export const WALLET_CREATE_INVOICE_TIMEOUT_MS = 45_000

export const BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
4 changes: 4 additions & 0 deletions lib/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,10 @@ export const emailSchema = object({
email: string().email('email is no good').required('required')
})

export const emailTokenSchema = object({
token: string().required('required').trim().matches(/^[0-9a-z]{6}$/i, 'must be 6 alphanumeric characters')
})

export const urlSchema = object({
url: string().url().required('required')
})
Expand Down
Loading