diff --git a/design/Deighton AR Design.sketch b/design/Deighton AR Design.sketch index 685e011..bf912ce 100644 Binary files a/design/Deighton AR Design.sketch and b/design/Deighton AR Design.sketch differ diff --git a/mobile/src/ARViewer/images/back.png b/mobile/src/ARViewer/images/back.png index 40b09e4..0138b94 100644 Binary files a/mobile/src/ARViewer/images/back.png and b/mobile/src/ARViewer/images/back.png differ diff --git a/mobile/src/Home/Home.js b/mobile/src/Home/Home.js index 7f0a50c..18b49fd 100644 --- a/mobile/src/Home/Home.js +++ b/mobile/src/Home/Home.js @@ -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 } } diff --git a/mobile/src/app.js b/mobile/src/app.js index 9edfa83..01de1a6 100644 --- a/mobile/src/app.js +++ b/mobile/src/app.js @@ -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 = [ "", ] +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 ( diff --git a/mobile/src/development.js b/mobile/src/development.js index c5ab2d3..71e1484 100644 --- a/mobile/src/development.js +++ b/mobile/src/development.js @@ -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 = "" diff --git a/mobile/src/util.js b/mobile/src/util.js index fd629ef..403f011 100644 --- a/mobile/src/util.js +++ b/mobile/src/util.js @@ -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 diff --git a/website/public/500.json b/website/public/500.json new file mode 100644 index 0000000..511d6bf --- /dev/null +++ b/website/public/500.json @@ -0,0 +1,3 @@ +{ + "message": "The service is unavailable" +} diff --git a/website/src/Auth/ConfirmEmail.js b/website/src/Auth/ConfirmEmail.js index eceb687..f85a840 100644 --- a/website/src/Auth/ConfirmEmail.js +++ b/website/src/Auth/ConfirmEmail.js @@ -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 {
+ message={waitModal ? waitModal.message : ""} + /> + icon={messageModal ? messageModal.icon : ""} + message={messageModal ? messageModal.message : ""} + detail={messageModal ? messageModal.title : ""} + onDismiss={this.handleMessageModalDismiss} + />
) } diff --git a/website/src/Auth/ResetPassword.js b/website/src/Auth/ResetPassword.js index 83b4d6a..ab9ffd4 100644 --- a/website/src/Auth/ResetPassword.js +++ b/website/src/Auth/ResetPassword.js @@ -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 { -
- + + @@ -122,32 +139,48 @@ export class ResetPassword extends Component { - + - Reset Password + Reset Password - + - + - + @@ -165,13 +198,16 @@ export class ResetPassword extends Component { + icon={messageModal ? messageModal.icon : ""} + message={messageModal ? messageModal.message : ""} + detail={messageModal ? messageModal.title : ""} + onDismiss={this.handleMessageModalDismiss} + /> - + )