Skip to content

Commit

Permalink
feat: add password authentication flow (#2023)
Browse files Browse the repository at this point in the history
This commit adds support for logging in via password using the Auth0
Universal Login screen.

It keeps around the existing magic link support, but relegates it to a
secondary (well, stylistically tertiary) button. Native form submit of
the email field now goes to the password login.

To ease users into it, we added explanatory text advertising the
feature and also directing them to the sign up page, although they can
also sign up from the login page itself.

It also replaces a few `this.state` direct-usages with destructured
assignments as a small bit of inline cleanup.

Co-authored-by: Whitman Schorn <whitman.schorn@gmail.com>
  • Loading branch information
reefdog and whitmanschorn committed Aug 23, 2021
1 parent d0b92b5 commit c6361e7
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 31 deletions.
8 changes: 7 additions & 1 deletion assets/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,17 +243,23 @@
"email-label": "Email",
"email-description": "We’ll send you a link to sign in. No password is required.",
"email-invalid": "Oops! That didn’t look like a valid email address. Please try again.",
"password-transition": "👋 {callout} If you don't have a password yet, {signUpLink} with your existing email. (All your streets will carry over.) You can use either method at any time.",
"password-transition-callout": "You can now sign in with a password!",
"password-transition-sign-up-link-label": "sign up here",
"social-heading": "or",
"button": {
"email": "Continue with email",
"twitter": "Continue with Twitter",
"google": "Continue with Google",
"facebook": "Continue with Facebook"
"facebook": "Continue with Facebook",
"magic-link": "Continue with email link",
"password": "Continue with password"
},
"tos": "By clicking one of these buttons, I agree to the {tosLink} and {privacyLink}.",
"tos-link-label": "terms of service",
"privacy-link-label": "privacy policy",
"loading-message": "Signing you in…",
"loading-message-sign-up": "Signing you up…",
"sent-message-with-email": "We’ve sent an email to {email}. Please follow the instructions there to continue signing in!",
"email-unreceived": "Didn’t receive it?",
"resend-email": "Resend email"
Expand Down
12 changes: 12 additions & 0 deletions assets/scripts/app/routing.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AUTH0_SIGN_IN_CALLBACK_PATH } from './config'
import { URL_NEW_STREET, URL_EXAMPLE_STREET } from './constants'
import Authenticate from './auth0'
import store from '../store'

const AUTH0_SIGN_IN_CALLBACK_URL = new URL(
AUTH0_SIGN_IN_CALLBACK_PATH,
Expand Down Expand Up @@ -54,6 +55,17 @@ export function goGoogleSignIn () {
})
}

export function goUniversalSignIn (loginHint, screenHint) {
const auth0 = Authenticate()
auth0.authorize({
responseType: 'code',
redirectUri: AUTH0_SIGN_IN_CALLBACK_URL,
loginHint,
screenHint,
uiLocales: store.getState().locale.locale
})
}

export function goEmailSignIn (email, callback) {
const auth0 = Authenticate()
auth0.passwordlessStart(
Expand Down
131 changes: 107 additions & 24 deletions assets/scripts/dialogs/SignInDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
goEmailSignIn,
goTwitterSignIn,
goFacebookSignIn,
goGoogleSignIn
goGoogleSignIn,
goUniversalSignIn
} from '../app/routing'
import LoadingSpinner from '../ui/LoadingSpinner'
import Icon from '../ui/Icon'
Expand All @@ -20,9 +21,11 @@ export default class SignInDialog extends React.Component {
emailSent: false,
sendingEmail: false,
error: false,
signingIn: false
signingIn: false,
signingUp: false
}

this.emailFormEl = React.createRef()
this.emailInputEl = React.createRef()
}

Expand Down Expand Up @@ -70,6 +73,23 @@ export default class SignInDialog extends React.Component {
goTwitterSignIn()
}

handleMagicLinkClick = (event) => {
event.preventDefault()

// Since this is not the native form submission, we manually trigger the native validation.
if (!this.emailFormEl.current.reportValidity()) {
return
}

const { email } = this.state

goEmailSignIn(email, this.handleGoEmailSignIn)

this.setState({
sendingEmail: true
})
}

handleGoEmailSignIn = (error, res) => {
if (error) {
console.error(error)
Expand All @@ -92,17 +112,34 @@ export default class SignInDialog extends React.Component {
})
}

handleSignUpClick = (event) => {
event.preventDefault()

const { email } = this.state

// Note: We intentionally don't validate the input here, even though we pass the email through
// to pre-fill the sign-up form, because we don't want to throw up a speed bump. The sign-up
// will perform its own validation.

this.setState({
signingUp: true
})

return goUniversalSignIn(email, 'signup')
}

handleSubmit = (event) => {
event.preventDefault()

// Note: we don't validate the input here;
// we let HTML5 <input type="email" required /> do validation
const { email } = this.state

goEmailSignIn(this.state.email, this.handleGoEmailSignIn)
// Note: We don't validate the input here. The browser will perform HTML5 native validation.

this.setState({
sendingEmail: true
signingIn: true
})

return goUniversalSignIn(email)
}

renderErrorMessage = () => {
Expand All @@ -117,15 +154,20 @@ export default class SignInDialog extends React.Component {
}

renderSignInWaiting = () => {
const { signingUp } = this.state

const loadingMessageKey = signingUp ? 'loading-message-sign-up' : 'loading-message'
const defaultLoadingMessage = signingUp ? 'Signing you up…' : 'Signing you in…'

return (
<Dialog>
{() => (
<div className="sign-in-dialog">
<header>
<h1 className="sign-in-loading-message">
<FormattedMessage
id="dialogs.sign-in.loading-message"
defaultMessage="Signing you in…"
id={`dialogs.sign-in.${loadingMessageKey}`}
defaultMessage={defaultLoadingMessage}
/>
</h1>
</header>
Expand All @@ -143,6 +185,8 @@ export default class SignInDialog extends React.Component {
}

renderEmailSent = () => {
const { email } = this.state

return (
<Dialog>
{() => (
Expand All @@ -162,7 +206,7 @@ export default class SignInDialog extends React.Component {
defaultMessage="We’ve sent an email to {email}. Please follow the instructions there to continue signing in!"
values={{
email: (
<span className="sign-in-email">{this.state.email}</span>
<span className="sign-in-email">{email}</span>
)
}}
/>
Expand All @@ -188,9 +232,16 @@ export default class SignInDialog extends React.Component {
}

render () {
const { sendingEmail, emailSent, signingIn } = this.state

if (sendingEmail || signingIn) {
const {
sendingEmail,
emailSent,
signingIn,
signingUp,
email,
error
} = this.state

if (sendingEmail || signingIn || signingUp) {
return this.renderSignInWaiting()
} else if (emailSent) {
return this.renderEmailSent()
Expand All @@ -216,7 +267,10 @@ export default class SignInDialog extends React.Component {
/>
</p>

<form onSubmit={this.handleSubmit}>
<form
ref={this.emailFormEl}
onSubmit={this.handleSubmit}
>
<label
htmlFor="sign-in-email-input"
className="sign-in-email-label"
Expand All @@ -231,35 +285,63 @@ export default class SignInDialog extends React.Component {
type="email"
id="sign-in-email-input"
ref={this.emailInputEl}
value={this.state.email}
value={email}
className={
'sign-in-input ' +
(this.state.error ? 'sign-in-input-error' : '')
(error ? 'sign-in-input-error' : '')
}
name="email"
onChange={this.handleChange}
placeholder="test@test.com"
required={true}
/>

{this.state.error && this.renderErrorMessage()}
{error && this.renderErrorMessage()}

<p className="sign-in-email-password-note">
<small>
<FormattedMessage
id="dialogs.sign-in.email-description"
defaultMessage="We’ll send you a link to sign in. No password is required."
/>
</small>
<FormattedMessage
id="dialogs.sign-in.password-transition"
defaultMessage="👋 {callout} If you don't have a password yet, {signUpLink} with
your existing email. (All your streets will carry over.) You can use either
method at any time."
values={{
callout: (
<strong>
<FormattedMessage
id="dialogs.sign-in.password-transition-callout"
defaultMessage="You can now sign in with a password!"
/>
</strong>
),
signUpLink: (
<a onClick={this.handleSignUpClick}>
<FormattedMessage
id="dialogs.sign-in.password-transition-sign-up-link-label"
defaultMessage="sign up here"
/>
</a>
)
}}
/>
</p>

<button
type="submit"
className="button-primary sign-in-button sign-in-email-button"
>
<FormattedMessage
id="dialogs.sign-in.button.email"
defaultMessage="Continue with email"
id="dialogs.sign-in.button.password"
defaultMessage="Continue with password"
/>
</button>

<button
className="button-tertiary sign-in-button sign-in-email-button"
onClick={this.handleMagicLinkClick}
>
<FormattedMessage
id="dialogs.sign-in.button.magic-link"
defaultMessage="Continue with email link"
/>
</button>
</form>
Expand Down Expand Up @@ -306,6 +388,7 @@ export default class SignInDialog extends React.Component {
defaultMessage="Continue with Facebook"
/>
</button>

</div>

<footer>
Expand Down
2 changes: 0 additions & 2 deletions assets/scripts/dialogs/SignInDialog.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@

.sign-in-email-password-note {
color: rgb(120, 120, 120); /* todo: standardize small colors */
margin-top: 0.25em;
margin-bottom: 0.25em;
line-height: 1.3;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,26 @@ exports[`SignInDialog renders 1`] = `
<p
class="sign-in-email-password-note"
>
<small>
We’ll send you a link to sign in. No password is required.
</small>
👋
<strong>
You can now sign in with a password!
</strong>
If you don't have a password yet,
<a>
sign up here
</a>
with your existing email. (All your streets will carry over.) You can use either method at any time.
</p>
<button
class="button-primary sign-in-button sign-in-email-button"
type="submit"
>
Continue with email
Continue with password
</button>
<button
class="button-tertiary sign-in-button sign-in-email-button"
>
Continue with email link
</button>
</form>
<div
Expand Down

0 comments on commit c6361e7

Please sign in to comment.