Skip to content

Commit

Permalink
Merge pull request #22 from ibi-group/signup-form-validation
Browse files Browse the repository at this point in the history
Signup form validation
  • Loading branch information
binh-dam-ibigroup authored Oct 1, 2020
2 parents c1d34fa + eee3104 commit 3a3aadc
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 143 deletions.
284 changes: 148 additions & 136 deletions components/ApiUserForm.js
Original file line number Diff line number Diff line change
@@ -1,170 +1,182 @@
import clone from 'clone'
import { Field, Formik } from 'formik'
import { Component } from 'react'
import { Button, Card, Col, Container, Form, Row } from 'react-bootstrap'
import { withAuth } from 'use-auth0-hooks'
import * as yup from 'yup'

import { AUTH0_SCOPE } from '../util/constants'

/**
* The basic form for creating an ApiUser. This can also be used to show a
* disabled view of the form (for viewing user details).
*
* TODO: Add the ability to update a user?
*/
class ApiUserForm extends Component {
constructor (props) {
super(props)
this.state = {
apiUser: {
appName: null,
appPurpose: null,
appUrl: null,
company: null,
hasConsentedToTerms: false,
name: null
}
}
}
// The validation schema for the form fields.
const validationSchema = yup.object({
appName: yup.string().required('Please enter your application name.'),
appPurpose: yup.string(),
appUrl: yup.string().url('Please enter a valid URL (should start with http:// or https://), or leave blank if unknown.'),
company: yup.string().required('Please enter your company name.'),
hasConsentedToTerms: yup.boolean().oneOf([true], 'You must agree to the terms to continue.'),
name: yup.string().required('Please enter your name.')
})

handleChange = field => e => {
const newData = {}
newData[field] = e.target.value
this.updateUserState(newData)
// Field layout (assumes all text fields)
const fieldLayout = [
{
title: 'Developer information',
fields: [
{
title: 'Developer name',
field: 'name'
},
{
title: 'Company',
field: 'company'
}
]
},
{
title: 'Application information',
fields: [
{
title: 'Application name',
field: 'appName'
},
{
title: 'Application purpose',
field: 'appPurpose'
},
{
title: 'Application URL',
field: 'appUrl'
}
]
}
]

handleTermsChange = e => {
this.updateUserState({ hasConsentedToTerms: e.target.checked })
/**
* Creates a blank ApiUser object to be filled out.
*/
function createBlankApiUser () {
return {
appName: '',
appPurpose: '',
appUrl: '',
company: '',
hasConsentedToTerms: false,
name: ''
}
}

handleCreateAccount = async e => {
/**
* The basic form for creating an ApiUser, including input validation.
* This can also be used to show a disabled view of the form (for viewing user details).
*
* TODO: Add the ability to update a user?
*/
class ApiUserForm extends Component {
handleCreateAccount = async apiUserData => {
const { auth, createUser } = this.props
if (auth.user) {
const { apiUser } = this.state
const apiUser = clone(apiUserData)

// Add required attributes for middleware storage.
apiUser.auth0UserId = auth.user.sub
apiUser.email = auth.user.email

createUser(apiUser)
} else {
alert('Could not save your data (Auth0 id was not available).')
}
}

updateUserState = newUserData => {
const { apiUser } = this.state
this.setState({
apiUser: {
...apiUser,
...newUserData
}
})
}

render () {
const { createUser } = this.props
// Default values to apiUser passed from props. Otherwise, use original state.
// It is assumed that if coming from props, the apiUser already exists.
const apiUser = this.props.apiUser || this.state.apiUser
const {
appName,
appPurpose,
appUrl,
company,
hasConsentedToTerms,
name
} = apiUser
// If the ApiUser already exists, it is passed from props.
// Otherwise, it is a new ApiUser, and a blank one is created.
const apiUser = this.props.apiUser || createBlankApiUser()

// We display validation for a particular field on blur (after the user finishes typing in it),
// so it is not too disruptive to the user.
// The onBlur/onHandleBlur and touched props are used to that effect.
// All field validation errors are also shown when the user clicks Create Account.
return (
<div>
{createUser && <h1>Sign up for API access</h1>}
<Form>
<Container>
<Row>
<Col>
<Card>
<Card.Header>Developer information</Card.Header>
<Card.Body>
<Form.Group>
<Form.Label>Developer name</Form.Label>
<Form.Control
disabled={!createUser}
onChange={this.handleChange('name')}
type='text'
value={name} />
</Form.Group>

<Form.Group>
<Form.Label>Company</Form.Label>
<Form.Control
disabled={!createUser}
onChange={this.handleChange('company')}
type='text'
value={company} />
</Form.Group>
</Card.Body>
</Card>
</Col>
<Col>
<Card>
<Card.Header>Application information</Card.Header>
<Card.Body>
<Form.Group>
<Form.Label>Application name</Form.Label>
<Form.Control
disabled={!createUser}
onChange={this.handleChange('appName')}
type='text'
value={appName} />
</Form.Group>

<Form.Group>
<Form.Label>Application purpose</Form.Label>
<Form.Control
disabled={!createUser}
onChange={this.handleChange('appPurpose')}
type='text'
value={appPurpose} />
</Form.Group>
<Formik
validateOnChange={false}
validateOnBlur
validationSchema={validationSchema}
onSubmit={this.handleCreateAccount}
initialValues={apiUser}
>
{({
handleSubmit,
touched,
errors
}) => (

<Form.Group>
<Form.Label>Application URL</Form.Label>
<Form.Control
disabled={!createUser}
onChange={this.handleChange('appUrl')}
type='text'
value={appUrl} />
</Form.Group>
</Card.Body>
</Card>
</Col>
</Row>
</Container>
<Form noValidate onSubmit={handleSubmit}>
<Container style={{paddingLeft: 0, paddingRight: 0}}>
<Row>
{
fieldLayout.map((col, colIndex) => (
<Col key={colIndex}>
<Card>
<Card.Header>{col.title}</Card.Header>
<Card.Body>
{
col.fields.map((field, fieldIndex) => {
const fieldName = field.field
return (
<Form.Group key={fieldIndex}>
<Form.Label>{field.title}</Form.Label>
<Field
as={Form.Control}
disabled={!createUser}
isInvalid={touched[fieldName] && !!errors[fieldName]}
name={fieldName}
// onBlur, onChange, and value are passed automatically.
/>
<Form.Control.Feedback type='invalid'>
{errors[fieldName]}
</Form.Control.Feedback>
</Form.Group>
)
})
}
</Card.Body>
</Card>
</Col>
))
}
</Row>
</Container>

<Form.Group>
<Form.Check
disabled={!createUser}
id='hasConsentedToTerms'
label={
<>
I have read and consent to the{' '}
<a href='/' target='_blank' rel='noopener noreferrer'>Terms of Service</a>{' '}
for using the {process.env.API_NAME}.
</>
<div className='mt-3'>
<Form.Group>
<Field
as={Form.Check}
disabled={!createUser}
feedback={errors.hasConsentedToTerms}
isInvalid={touched.hasConsentedToTerms && !!errors.hasConsentedToTerms}
label={
<>
I have read and consent to the{' '}
<a href='/' target='_blank' rel='noopener noreferrer'>Terms of Service</a>{' '}
for using the {process.env.API_NAME}.
</>
}
name='hasConsentedToTerms'
// onBlur, onChange, and value are passed automatically.
/>
</Form.Group>
</div>
{createUser &&
<Button type='submit' variant='primary'>
Create account
</Button>
}
onChange={this.handleTermsChange}
type='checkbox'
checked={hasConsentedToTerms}
/>
<Form.Text muted>You must agree to the terms to continue.</Form.Text>
</Form.Group>
{createUser &&
<Button
disabled={!hasConsentedToTerms}
onClick={this.handleCreateAccount}
variant='primary'
>
Create account
</Button>
}
</Form>
</Form>
)}
</Formik>
</div>
)
}
Expand Down
16 changes: 14 additions & 2 deletions components/LayoutWithAuth0.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,20 @@ class LayoutWithAuth0 extends Component {
isUserRequested: true
})
// TODO: Combine into a single fetch fromToken or use SWR
const adminUser = await secureFetch(`${ADMIN_USER_URL}/fromtoken`, accessToken)
const apiUser = await secureFetch(`${API_USER_URL}/fromtoken`, accessToken)
const adminUserFetchResult = await secureFetch(`${ADMIN_USER_URL}/fromtoken`, accessToken)
const apiUserFetchResult = await secureFetch(`${API_USER_URL}/fromtoken`, accessToken)

// Check that the contents of the fetch result for admin user and api user is valid
// This means for instance checking for existence of a data.id field.
// If the user was not found, something else is returned of the form
// data: {
// "result": "ERR",
// "message": "No user with id=000000 found.",
// "code": 404,
// "detail": null
// }
const adminUser = adminUserFetchResult.data.id ? adminUserFetchResult.data : null
const apiUser = apiUserFetchResult.data.id ? apiUserFetchResult.data : null

this.setState({
...state,
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"bootstrap": "^4.5.0",
"clone": "^2.1.2",
"dotenv": "^8.2.0",
"formik": "^2.1.5",
"isomorphic-unfetch": "^3.0.0",
"moment": "^2.24.0",
"next": "^9.3.2",
Expand All @@ -25,7 +26,8 @@
"styled-components": "^5.0.1",
"styled-icons": "^10.2.1",
"swr": "^0.3.2",
"use-auth0-hooks": "^0.7.0"
"use-auth0-hooks": "^0.7.0",
"yup": "^0.29.3"
},
"devDependencies": {
"mastarm": "^5.3.1",
Expand Down
3 changes: 2 additions & 1 deletion util/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export async function createOrUpdateUser (url, userData, isNew, accessToken) {
}

// TODO: improve the UI feedback messages for this.
if (result.status === 'success' && result.data) {
// A successful call has the user record (with id) in the data field.
if (result.data.id) {
return result.data
} else {
alert(`An error was encountered:\n${JSON.stringify(result)}`)
Expand Down
Loading

1 comment on commit 3a3aadc

@vercel
Copy link

@vercel vercel bot commented on 3a3aadc Oct 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.