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, View,
TouchableOpacity, TouchableOpacity,
TouchableHighlight, TouchableHighlight,
PermissionsAndroid,
AsyncStorage,
} from "react-native" } from "react-native"
import MapView, { Marker } from "react-native-maps" import MapView, { Marker } from "react-native-maps"
import { Icon, Header } from "../ui" import { Icon, Header } from "../ui"
@@ -15,9 +17,12 @@ import { api } from "../API"
import autobind from "autobind-decorator" import autobind from "autobind-decorator"
import { ifIphoneX } from "react-native-iphone-x-helper" import { ifIphoneX } from "react-native-iphone-x-helper"
import { workItemTypeText, pad, regionContainingPoints } from "../util" import { workItemTypeText, pad, regionContainingPoints } from "../util"
import { ensurePermission } from "../App"
import { versionInfo } from "../version"
import pinImage from "./images/pin.png" import pinImage from "./images/pin.png"
const minGPSAccuracy = 20 const minGPSAccuracy = 20
const neverAskForLocationPermissionKeyName = "NeverAskForLocationPermission"
export class Home extends React.Component { export class Home extends React.Component {
constructor(props) { constructor(props) {
@@ -34,10 +39,22 @@ export class Home extends React.Component {
positionInfo: null, positionInfo: null,
} }
this.watchId = navigator.geolocation.watchPosition( ensurePermission(
this.handlePositionChange, PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
null, neverAskForLocationPermissionKeyName,
{ distanceFilter: 10 } {
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 api
@@ -59,6 +76,7 @@ export class Home extends React.Component {
componentWillUnmount() { componentWillUnmount() {
if (this.watchId) { if (this.watchId) {
navigator.geolocation.clearWatch(this.watchId) navigator.geolocation.clearWatch(this.watchId)
this.watchId = null
} }
} }

View File

@@ -1,11 +1,10 @@
import React from "react" import React from "react"
import { View, StyleSheet } from "react-native"
import { import {
ViroARSceneNavigator, View,
ViroARScene, StyleSheet,
ViroARPlane, AsyncStorage,
ViroBox, PermissionsAndroid,
} from "react-viro" } from "react-native"
import { NativeRouter, Route, Link, Switch } from "react-router-native" import { NativeRouter, Route, Link, Switch } from "react-router-native"
import MapView from "react-native-maps" import MapView from "react-native-maps"
import { WorkItem, WorkItemList } from "./WorkItem" import { WorkItem, WorkItemList } from "./WorkItem"
@@ -20,6 +19,53 @@ console.ignoredYellowBox = [
"<ViroSurface>", "<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 { export default class App extends React.Component {
render() { render() {
return ( return (

View File

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

View File

@@ -57,9 +57,12 @@ export const pad = (num, size) => {
} }
export const regionContainingPoints = (points, inset) => { export const regionContainingPoints = (points, inset) => {
let minX, maxX, minY, maxY let minX,
maxX,
minY,
maxY
// init first point // init first point
;((point) => { ;((point) => {
minX = point.latitude minX = point.latitude
maxX = 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 React from "react"
import { api } from 'src/API' import { api } from "src/API"
import PropTypes from 'prop-types' import PropTypes from "prop-types"
import { MessageModal, WaitModal } from '../Modal' import { MessageModal, WaitModal } from "../Modal"
import autobind from 'autobind-decorator' import autobind from "autobind-decorator"
export class ConfirmEmail extends React.Component { export class ConfirmEmail extends React.Component {
static propTypes = { static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]) history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
} }
constructor() { constructor() {
super() super()
this.state = { this.state = {
waitModal: null, waitModal: null,
messageModal: null messageModal: null,
} }
} }
componentDidMount(props) { 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) { if (emailToken) {
api.logout().then(() => { api
return api.confirmEmail(emailToken) .logout()
}).then((response) => { .then(() => {
this.setState({ waitModal: null }) return api.confirmEmail(emailToken)
if (response && response.passwordToken) { })
// API will send a password reset token if this is the first time loggin on .then((response) => {
this.props.history.replace(`/reset-password?password-token=${response.passwordToken}`) this.setState({ waitModal: null })
} else { if (response && response.passwordToken) {
this.props.history.replace('/login') this.setState({
} waitModal: null,
}).catch((err) => { messageModal: {
this.setState({ icon: "thumb",
waitModal: null, message: `Your email is confirmed. You will be redirected to set your password.`,
messageModal: { // API will send a password reset token if this is the first time loggin on
icon: 'hand', redirect: `/reset-password?password-token=${
message: `Please contact ${process.env.REACT_APP_SUPPORT_EMAIL} to request another confirmation email.`, response.passwordToken
detail: err.message }`,
},
})
} 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 { } else {
this.props.history.replace('/') this.props.history.replace("/")
} }
} }
@autobind @autobind
handleMessageModalDismiss() { handleMessageModalDismiss() {
const { redirect } = this.state.messageModal
this.setState({ messageModal: null }) this.setState({ messageModal: null })
this.props.history.replace('/login') this.props.history.replace(redirect || "/login")
} }
render() { render() {
@@ -60,14 +80,16 @@ export class ConfirmEmail extends React.Component {
<div> <div>
<WaitModal <WaitModal
active={!!waitModal} active={!!waitModal}
message={waitModal ? waitModal.message : ''} /> message={waitModal ? waitModal.message : ""}
/>
<MessageModal <MessageModal
open={!!messageModal} open={!!messageModal}
icon={messageModal ? messageModal.icon : ''} icon={messageModal ? messageModal.icon : ""}
message={messageModal ? messageModal.message : ''} message={messageModal ? messageModal.message : ""}
detail={messageModal ? messageModal.title : ''} detail={messageModal ? messageModal.title : ""}
onDismiss={this.handleMessageModalDismiss} /> onDismiss={this.handleMessageModalDismiss}
/>
</div> </div>
) )
} }

View File

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