Improve sign-up process for new users

This commit is contained in:
John Lyon-Smith
2018-04-15 16:06:05 -07:00
parent 8c729b604b
commit 6134c3be0f
9 changed files with 247 additions and 119 deletions

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -8,6 +8,8 @@ import {
View,
TouchableOpacity,
TouchableHighlight,
PermissionsAndroid,
AsyncStorage,
} from "react-native"
import MapView, { Marker } from "react-native-maps"
import { Icon, Header } from "../ui"
@@ -15,9 +17,12 @@ import { api } from "../API"
import autobind from "autobind-decorator"
import { ifIphoneX } from "react-native-iphone-x-helper"
import { workItemTypeText, pad, regionContainingPoints } from "../util"
import { ensurePermission } from "../App"
import { versionInfo } from "../version"
import pinImage from "./images/pin.png"
const minGPSAccuracy = 20
const neverAskForLocationPermissionKeyName = "NeverAskForLocationPermission"
export class Home extends React.Component {
constructor(props) {
@@ -34,10 +39,22 @@ export class Home extends React.Component {
positionInfo: null,
}
this.watchId = navigator.geolocation.watchPosition(
this.handlePositionChange,
null,
{ distanceFilter: 10 }
ensurePermission(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
neverAskForLocationPermissionKeyName,
{
title: versionInfo.title,
message:
"This app needs access to your location so that " +
"you can find and access the geo located items.",
},
() => {
this.watchId = navigator.geolocation.watchPosition(
this.handlePositionChange,
null,
{ distanceFilter: 10 }
)
}
)
api
@@ -59,6 +76,7 @@ export class Home extends React.Component {
componentWillUnmount() {
if (this.watchId) {
navigator.geolocation.clearWatch(this.watchId)
this.watchId = null
}
}

View File

@@ -1,11 +1,10 @@
import React from "react"
import { View, StyleSheet } from "react-native"
import {
ViroARSceneNavigator,
ViroARScene,
ViroARPlane,
ViroBox,
} from "react-viro"
View,
StyleSheet,
AsyncStorage,
PermissionsAndroid,
} from "react-native"
import { NativeRouter, Route, Link, Switch } from "react-router-native"
import MapView from "react-native-maps"
import { WorkItem, WorkItemList } from "./WorkItem"
@@ -20,6 +19,53 @@ console.ignoredYellowBox = [
"<ViroSurface>",
]
export const ensurePermission = (
permission,
neverAskKeyName,
rationale,
onSuccess,
onError
) => {
PermissionsAndroid.check(permission)
.then((flag) => {
if (flag) {
if (onSuccess) {
onSuccess()
}
return
}
return AsyncStorage.getItem(neverAskKeyName)
})
.then((value) => {
if (value === "YES") {
return
} else {
return PermissionsAndroid.request(permission, rationale)
}
})
.then((result) => {
if (result === PermissionsAndroid.RESULTS.GRANTED) {
if (onSuccess) {
onSuccess()
}
return
}
if (result === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
AsyncStorage.setItem(neverAskKeyName, "YES")
}
if (onError) {
onError()
}
})
.catch((err) => {
if (onError) {
onError()
}
})
}
export default class App extends React.Component {
render() {
return (

View File

@@ -1,3 +1,3 @@
export const localIPAddr = "192.168.1.175"
export const defaultUser = "john@lyon-smith.org"
// export const defaultUser = ""
//export const defaultUser = "john@lyon-smith.org"
export const defaultUser = ""

View File

@@ -57,9 +57,12 @@ export const pad = (num, size) => {
}
export const regionContainingPoints = (points, inset) => {
let minX, maxX, minY, maxY
let minX,
maxX,
minY,
maxY
// init first point
// init first point
;((point) => {
minX = point.latitude
maxX = point.latitude

3
website/public/500.json Normal file
View File

@@ -0,0 +1,3 @@
{
"message": "The service is unavailable"
}

View File

@@ -1,56 +1,76 @@
import React from 'react'
import { api } from 'src/API'
import PropTypes from 'prop-types'
import { MessageModal, WaitModal } from '../Modal'
import autobind from 'autobind-decorator'
import React from "react"
import { api } from "src/API"
import PropTypes from "prop-types"
import { MessageModal, WaitModal } from "../Modal"
import autobind from "autobind-decorator"
export class ConfirmEmail extends React.Component {
static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}
constructor() {
super()
this.state = {
waitModal: null,
messageModal: null
messageModal: null,
}
}
componentDidMount(props) {
const emailToken = new URLSearchParams(decodeURIComponent(window.location.search)).get('email-token')
const emailToken = new URLSearchParams(
decodeURIComponent(window.location.search)
).get("email-token")
this.setState({ waitModal: { message: "Validating Email..." } })
this.setState({ waitModal: { message: 'Validating Email...' } })
if (emailToken) {
api.logout().then(() => {
return api.confirmEmail(emailToken)
}).then((response) => {
this.setState({ waitModal: null })
if (response && response.passwordToken) {
// API will send a password reset token if this is the first time loggin on
this.props.history.replace(`/reset-password?password-token=${response.passwordToken}`)
} else {
this.props.history.replace('/login')
}
}).catch((err) => {
this.setState({
waitModal: null,
messageModal: {
icon: 'hand',
message: `Please contact ${process.env.REACT_APP_SUPPORT_EMAIL} to request another confirmation email.`,
detail: err.message
api
.logout()
.then(() => {
return api.confirmEmail(emailToken)
})
.then((response) => {
this.setState({ waitModal: null })
if (response && response.passwordToken) {
this.setState({
waitModal: null,
messageModal: {
icon: "thumb",
message: `Your email is confirmed. You will be redirected to set your password.`,
// API will send a password reset token if this is the first time loggin on
redirect: `/reset-password?password-token=${
response.passwordToken
}`,
},
})
} else {
this.props.history.replace("/login")
}
})
})
.catch((err) => {
this.setState({
waitModal: null,
messageModal: {
icon: "hand",
message: `Please contact ${
process.env.REACT_APP_SUPPORT_EMAIL
} to request another confirmation email.`,
detail: err.message,
},
})
})
} else {
this.props.history.replace('/')
this.props.history.replace("/")
}
}
@autobind
handleMessageModalDismiss() {
const { redirect } = this.state.messageModal
this.setState({ messageModal: null })
this.props.history.replace('/login')
this.props.history.replace(redirect || "/login")
}
render() {
@@ -60,14 +80,16 @@ export class ConfirmEmail extends React.Component {
<div>
<WaitModal
active={!!waitModal}
message={waitModal ? waitModal.message : ''} />
message={waitModal ? waitModal.message : ""}
/>
<MessageModal
open={!!messageModal}
icon={messageModal ? messageModal.icon : ''}
message={messageModal ? messageModal.message : ''}
detail={messageModal ? messageModal.title : ''}
onDismiss={this.handleMessageModalDismiss} />
icon={messageModal ? messageModal.icon : ""}
message={messageModal ? messageModal.message : ""}
detail={messageModal ? messageModal.title : ""}
onDismiss={this.handleMessageModalDismiss}
/>
</div>
)
}

View File

@@ -1,30 +1,30 @@
import React, { Component, Fragment } from 'react'
import PropTypes from 'prop-types'
import { Box, Text, Image, Column, Row, BoundInput, BoundButton } from 'ui'
import { MessageModal, WaitModal } from '../Modal'
import { api } from 'src/API'
import { FormBinder } from 'react-form-binder'
import { sizeInfo, colorInfo } from 'ui/style'
import autobind from 'autobind-decorator'
import headerLogo from 'images/deighton.png'
import React, { Component, Fragment } from "react"
import PropTypes from "prop-types"
import { Box, Text, Image, Column, Row, BoundInput, BoundButton } from "ui"
import { MessageModal, WaitModal } from "../Modal"
import { api } from "src/API"
import { FormBinder } from "react-form-binder"
import { sizeInfo, colorInfo } from "ui/style"
import autobind from "autobind-decorator"
import headerLogo from "images/deighton.png"
export class ResetPassword extends Component {
static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}
static bindings = {
newPassword: {
alwaysGet: true,
isValid: (r, v) => (v.length >= 6)
isValid: (r, v) => v.length >= 6,
},
reenteredNewPassword: {
isValid: (r, v) => (v !== '' && v === r.getFieldValue('newPassword'))
isValid: (r, v) => v !== "" && v === r.getFieldValue("newPassword"),
},
submit: {
noValue: true,
isDisabled: (r) => (!r.anyModified || !r.allValid)
}
isDisabled: (r) => !r.anyModified || !r.allValid,
},
}
constructor(props) {
@@ -38,30 +38,33 @@ export class ResetPassword extends Component {
}
componentDidMount(props) {
if (this.state.tokenConfirmed) {
return
}
const passwordToken = new URLSearchParams(
decodeURIComponent(window.location.search)
).get("password-token")
const passwordToken = new URLSearchParams(decodeURIComponent(window.location.search)).get('password-token')
this.setState({ waitModal: { message: "Confirming password reset..." } })
this.setState({ waitModal: { message: 'Confirming password reset...' } })
if (passwordToken) {
api.logout().then(() => {
return api.confirmResetPassword(passwordToken)
}).then((response) => {
this.setState({ waitModal: null, tokenConfirmed: true })
}).catch((err) => {
this.setState({
waitModal: null,
messageModal: {
icon: 'hand',
message: `We were unable to confirm you requested a password reset. Please request another reset email.`,
detail: err.message
}
api
.logout()
.then(() => {
return api.confirmResetPassword(passwordToken)
})
.then((response) => {
this.setState({ waitModal: null })
})
.catch((err) => {
this.setState({
waitModal: null,
messageModal: {
icon: "hand",
message: `We were unable to confirm you requested a password reset. Please request another reset.`,
detail: err.message,
},
})
})
})
} else {
this.props.history.replace('/')
this.props.history.replace("/")
}
}
@@ -71,33 +74,42 @@ export class ResetPassword extends Component {
e.stopPropagation()
const obj = this.state.binder.getModifiedFieldValues()
const passwordToken = new URLSearchParams(decodeURIComponent(window.location.search)).get('password-token')
const passwordToken = new URLSearchParams(
decodeURIComponent(window.location.search)
).get("password-token")
this.setState({ waitModal: { message: 'Setting Password...' } })
api.resetPassword({ newPassword: obj.newPassword, passwordToken }).then(() => {
this.setState({ waitModal: null })
this.props.history.replace('/login')
}).catch((err) => {
this.setState({
binder: new FormBinder({}, ResetPassword.bindings), // Reset to avoid accidental rapid retries
waitModal: null,
messageModal: {
icon: 'hand',
message: 'There was a problem changing your password. Please request another reset email.',
detail: err.message,
noRetry: true,
}
this.setState({ waitModal: { message: "Setting Password..." } })
api
.resetPassword({ newPassword: obj.newPassword, passwordToken })
.then(() => {
this.setState({
waitModal: null,
messageModal: {
icon: "thumb",
message: `Your password has been reset. You can now login to the system with your new password.`,
},
})
})
.catch((err) => {
this.setState({
binder: new FormBinder({}, ResetPassword.bindings), // Reset to avoid accidental rapid retries
waitModal: null,
messageModal: {
icon: "hand",
message:
"There was a problem changing your password. Please request another reset email.",
detail: err.message,
},
})
})
})
}
@autobind
handleMessageModalDismiss() {
if (this.state.messageModal.noRetry) {
this.props.history.replace('/login')
} else {
this.setState({ messageModal: null })
}
const { redirect } = this.state.messageModal
this.setState({ messageModal: null })
this.props.history.replace(redirect || "/login")
}
render() {
@@ -111,8 +123,13 @@ export class ResetPassword extends Component {
<Row.Item grow />
<Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item width={sizeInfo.modalWidth}>
<form onSubmit={this.handleSubmit} id='resetPasswordForm'>
<Box border={{ width: sizeInfo.headerBorderWidth, color: colorInfo.headerBorder }} radius={sizeInfo.formBoxRadius}>
<form onSubmit={this.handleSubmit} id="resetPasswordForm">
<Box
border={{
width: sizeInfo.headerBorderWidth,
color: colorInfo.headerBorder,
}}
radius={sizeInfo.formBoxRadius}>
<Row>
<Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item>
@@ -122,32 +139,48 @@ export class ResetPassword extends Component {
<Row>
<Row.Item grow />
<Row.Item>
<Image source={headerLogo} width={sizeInfo.loginLogoWidth} />
<Image
source={headerLogo}
width={sizeInfo.loginLogoWidth}
/>
</Row.Item>
<Row.Item grow />
</Row>
</Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item>
<Text size='large'>Reset Password</Text>
<Text size="large">Reset Password</Text>
</Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item>
<BoundInput label='New Password' password name='newPassword'
message='A new password, cannot be blank or the same as your old password'
binder={binder} />
<BoundInput
label="New Password"
password
name="newPassword"
message="A new password, cannot be blank or the same as your old password"
binder={binder}
/>
</Column.Item>
<Column.Item>
<BoundInput label='Re-enter New Password' password name='reenteredNewPassword'
message='The new password again, must match and cannot be blank'
binder={binder} />
<BoundInput
label="Re-enter New Password"
password
name="reenteredNewPassword"
message="The new password again, must match and cannot be blank"
binder={binder}
/>
</Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item minHeight={sizeInfo.buttonHeight}>
<Row>
<Row.Item grow />
<Row.Item>
<BoundButton text='Submit' name='submit' submit='resetPasswordForm' binder={binder} />
<BoundButton
text="Submit"
name="submit"
submit="resetPasswordForm"
binder={binder}
/>
</Row.Item>
</Row>
</Column.Item>
@@ -165,13 +198,16 @@ export class ResetPassword extends Component {
<Column.Item grow>
<MessageModal
open={!!messageModal}
icon={messageModal ? messageModal.icon : ''}
message={messageModal ? messageModal.message : ''}
detail={messageModal ? messageModal.title : ''}
onDismiss={this.handleMessageModalDismiss} />
icon={messageModal ? messageModal.icon : ""}
message={messageModal ? messageModal.message : ""}
detail={messageModal ? messageModal.title : ""}
onDismiss={this.handleMessageModalDismiss}
/>
<WaitModal active={!!waitModal}
message={waitModal ? waitModal.message : ''} />
<WaitModal
active={!!waitModal}
message={waitModal ? waitModal.message : ""}
/>
</Column.Item>
</Fragment>
)