From cd99889af6a0903f5184b7acc1a0b459fd1cdf68 Mon Sep 17 00:00:00 2001 From: Farhan Ahmad Date: Tue, 5 May 2020 00:11:28 -0500 Subject: [PATCH 01/15] Address lookup (#96) * fix(SearchPage): fix the responsive styling of the results * fix(ClickableMap): switch to calling Google Maps API directly Previously, we were using the react-geocode library which was a server side library and did not support domain-based key restrictions. Now using the API directly because it's simple enough and will allow us to support key restrictions. Closes #94 * chore(*): upgrade a few of the older packages. * refactor(AccountMenu): switch to Link component to allow opening in new window * feat(ClickableMap): allow user to enter address We now allow users to enter address on the new request and volunteer sign up pages. On the volunteer sign up page, the address also populates the separate address fields for the volunteer. Closes #29 * fix(NewRequestPage): fix the .length error and styling Fix an error when the user doesn't have the diplayName filled out. Also default displayName to `firstName lastName` when it's not set. Lastly, fix the styling on small screens. Closes #85 * feat(RequestSuccessfulPage): add link to the newly created request * feat(*): hook up sentry.io. (#95) * fix(Discussion): remove the incorrect limit(1) from private comments. * chore(*): update version * Check version before creating new release (#100) * Only run release logic for production branch * refactor(ClickableMap): switch to async/await as suggested * refactor(useNewRequest): copy the requestLocation object before deleting the unneeded property * refactor(utils/geo): log to Sentry when API is not found Co-authored-by: Scott Prue --- .env.local | 9 +- .env.next | 4 +- .env.prod | 7 +- .env.staging | 4 +- .github/workflows/deploy.yml | 6 + package.json | 17 +- src/components/ClickableMap/ClickableMap.js | 174 ++++++++++---- .../ClickableMap/ClickableMap.styles.js | 37 ++- src/containers/Navbar/AccountMenu.js | 21 +- .../NewRequestPage/NewRequestPage.js | 34 ++- .../NewRequestPage/NewRequestPage.styles.js | 4 +- .../NewRequestPage/useNewRequestPage.js | 37 +-- .../components/NewUserPage/NewUserPage.js | 87 ++++++- .../components/Discussion/Discussion.js | 3 +- .../RequestSuccessfulPage.js | 46 ++-- .../RequestSuccessfulPage.styles.js | 5 + .../components/SearchPage/SearchPage.js | 4 +- src/utils/errorHandler.js | 13 +- src/utils/geo.js | 31 +++ yarn.lock | 224 +++++++++--------- 20 files changed, 514 insertions(+), 253 deletions(-) diff --git a/.env.local b/.env.local index 63ac54e..fb10efd 100644 --- a/.env.local +++ b/.env.local @@ -1,6 +1,4 @@ -REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyBowgXC55EjPKY46v04jH-crZlH_zCaepU -REACT_APP_ENVIRONMENT_BASE_URL=https://cv19assist-next.web.app -# Firebase config +# Begin Firebase config REACT_APP_FIREBASE_API_KEY=AIzaSyBowgXC55EjPKY46v04jH-crZlH_zCaepU REACT_APP_FIREBASE_AUTH_DOMAIN=cv19assist-next.firebaseapp.com REACT_APP_FIREBASE_DATABASE_URL=https://cv19assist-next.firebaseio.com @@ -9,7 +7,10 @@ REACT_APP_FIREBASE_STORAGE_BUCKET=cv19assist-next.appspot.com REACT_APP_FIREBASE_MESSAGING_SENDER_ID=971141009068 REACT_APP_FIREBASE_APP_ID=1:971141009068:web:6d7a40c5742c1c21ac5909 REACT_APP_FIREBASE_MEASUREMENT_ID=G-WWBQXP76C2 +# End Firebase config +REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyBowgXC55EjPKY46v04jH-crZlH_zCaepU REACT_APP_ENVIRONMENT_BASE_URL=https://cv19assist-next.web.app REACT_APP_FIREBASE_PUBLIC_VAPID_KEY="BLMZS5OT_qAjnowS62UiwBHk6SWSqA9RtHLP9MMZ4BEU7ztSBZ1vgBrQFPhI_VnqXntqPNE9MO6itoDa94Ce9O4" -REACT_APP_BLOG_POST_URL=https://cv19assist.github.io/blog/ \ No newline at end of file +REACT_APP_BLOG_POST_URL=https://cv19assist.github.io/blog/ +REACT_APP_SENTRY_DSN=https://877b4891db634277b8f4b95daae7f207@o386953.ingest.sentry.io/5221758 \ No newline at end of file diff --git a/.env.next b/.env.next index e5f628b..e70cbcc 100644 --- a/.env.next +++ b/.env.next @@ -1,4 +1,4 @@ -# Firebase config +# Begin Firebase config REACT_APP_FIREBASE_API_KEY=AIzaSyBowgXC55EjPKY46v04jH-crZlH_zCaepU REACT_APP_FIREBASE_AUTH_DOMAIN=cv19assist-next.firebaseapp.com REACT_APP_FIREBASE_DATABASE_URL=https://cv19assist-next.firebaseio.com @@ -7,8 +7,10 @@ REACT_APP_FIREBASE_STORAGE_BUCKET=cv19assist-next.appspot.com REACT_APP_FIREBASE_MESSAGING_SENDER_ID=971141009068 REACT_APP_FIREBASE_APP_ID=1:971141009068:web:6d7a40c5742c1c21ac5909 REACT_APP_FIREBASE_MEASUREMENT_ID=G-WWBQXP76C2 +# End Firebase config REACT_APP_ENVIRONMENT_BASE_URL=https://cv19assist-next.web.app REACT_APP_FIREBASE_PUBLIC_VAPID_KEY="BLMZS5OT_qAjnowS62UiwBHk6SWSqA9RtHLP9MMZ4BEU7ztSBZ1vgBrQFPhI_VnqXntqPNE9MO6itoDa94Ce9O4" +REACT_APP_SENTRY_DSN=https://877b4891db634277b8f4b95daae7f207@o386953.ingest.sentry.io/5221758 TEST_UID="cXs1hpjX4rgsJB97Q3jS8vY6lP43" \ No newline at end of file diff --git a/.env.prod b/.env.prod index 004abf9..f27558d 100644 --- a/.env.prod +++ b/.env.prod @@ -1,6 +1,4 @@ -REACT_APP_ENVIRONMENT_BASE_URL=https://cv19assist.com - -# Firebase config +# Begin Firebase config REACT_APP_FIREBASE_API_KEY=AIzaSyAwxqcKSQzLALz3mca5z95NpQ5u5tdBUpQ REACT_APP_FIREBASE_AUTH_DOMAIN=cv19assist-bfa5f.firebaseapp.com REACT_APP_FIREBASE_DATABASE_URL=https://cv19assist-bfa5f.firebaseio.com @@ -9,5 +7,8 @@ REACT_APP_FIREBASE_STORAGE_BUCKET=cv19assist-bfa5f.appspot.com REACT_APP_FIREBASE_MESSAGING_SENDER_ID=286751889445 REACT_APP_FIREBASE_APP_ID=1:286751889445:web:93cdb64dca219c5bc987cc REACT_APP_FIREBASE_MEASUREMENT_ID=G-GN6WEKL6EZ +# End Firebase config +REACT_APP_ENVIRONMENT_BASE_URL=https://cv19assist.com REACT_APP_FIREBASE_PUBLIC_VAPID_KEY="BCyMvtntekgW-ZambgF7GlL0t8A95AiFxTCGd2HiX8a9x1qV-NCipd7j6vQp6Jx7kF3dpZwMgJU1zxlMd0hbYB4" +REACT_APP_SENTRY_DSN=https://877b4891db634277b8f4b95daae7f207@o386953.ingest.sentry.io/5221758 \ No newline at end of file diff --git a/.env.staging b/.env.staging index be535c8..b8427c3 100644 --- a/.env.staging +++ b/.env.staging @@ -1,4 +1,4 @@ -# Firebase config +# Begin Firebase config REACT_APP_FIREBASE_API_KEY=AIzaSyBFT5yhCTqcjuJB5BgmOcC52vD_gTmcqr4 REACT_APP_FIREBASE_AUTH_DOMAIN=cv19assist-dev.firebaseapp.com REACT_APP_FIREBASE_DATABASE_URL=https://cv19assist-dev.firebaseio.com @@ -7,7 +7,9 @@ REACT_APP_FIREBASE_STORAGE_BUCKET=cv19assist-dev.appspot.com REACT_APP_FIREBASE_MESSAGING_SENDER_ID=487252002912 REACT_APP_FIREBASE_APP_ID=1:487252002912:web:53e28d338ebd8788215b05 REACT_APP_FIREBASE_MEASUREMENT_ID=G-GB8SL2LGL0 +# End Firebase config REACT_APP_ENVIRONMENT_BASE_URL=https://cv19assist-dev.web.app REACT_APP_BLOG_POST_INDEX=https://cv19assist.github.io/blog/index.json REACT_APP_FIREBASE_PUBLIC_VAPID_KEY="BEVjtpNAmifTvO_Mk9aMRJrFh5ldOb8ZBnFmq-Y5OodxhimT57gEI2eacMLjSrss195Mf4QiQkBV3ksTNzZcMGo" +REACT_APP_SENTRY_DSN=https://877b4891db634277b8f4b95daae7f207@o386953.ingest.sentry.io/5221758 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index dd1ca19..34ddece 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -120,7 +120,13 @@ jobs: echo "Service Account selected for projectId $(echo $SERVICE_ACCOUNT | jq .project_id)" bin/updateEmailTemplates.js + - name: Check if version has been updated + if: github.ref == 'refs/heads/production' + id: check + uses: EndBug/version-check@v1 + - name: Create Release + if: github.ref == 'refs/heads/production' && steps.check.outputs.changed == 'true' id: create_release uses: actions/create-release@latest env: diff --git a/package.json b/package.json index 19528fd..31bd9a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "app", - "version": "0.6.1", + "version": "0.6.2", "description": "The spread of Coronavirus (COVID-19) is having a huge impact on our lives, especially those at high risk. It is more important now than ever for us to come together and support our communities, whether it be material support or emotional support. Our goal is to connect those in need with those that are able to help and spread positive vibes.", "main": "index.js", "scripts": { @@ -30,30 +30,29 @@ "format": "prettier --single-quote --no-semi --trailing-comma none --write \"src/**/*.js\"" }, "dependencies": { - "@material-ui/core": "^4.9.8", + "@material-ui/core": "^4.9.12", "@material-ui/icons": "^4.9.1", - "@material-ui/lab": "^4.0.0-alpha.48", - "@react-google-maps/api": "^1.8.6", - "@sentry/browser": "^5.15.4", + "@material-ui/lab": "^4.0.0-alpha.51", + "@react-google-maps/api": "^1.9.0", + "@sentry/browser": "^5.15.5", "date-fns": "^2.12.0", "firebase": "^7.14.0", "geofirestore": "^3.4.1", "history": "^4.10.1", "lodash": "^4.17.15", "prop-types": "^15.7.2", - "query-string": "^6.12.0", + "query-string": "^6.12.1", "react": "^16.13.0", "react-dom": "^16.13.0", - "react-geocode": "^0.2.1", "react-google-button": "^0.7.0", "react-helmet": "^6.0.0", - "react-hook-form": "^5.2.0", + "react-hook-form": "^5.6.1", "react-places-autocomplete": "^7.2.1", "react-router-dom": "^5.1.2", "react-transition-group": "^4.3.0", "reactfire": "^2.0.3", "rxjs": "^6.5.5", - "yup": "^0.28.3" + "yup": "^0.28.5" }, "devDependencies": { "@googlemaps/google-maps-services-js": "^2.6.0", diff --git a/src/components/ClickableMap/ClickableMap.js b/src/components/ClickableMap/ClickableMap.js index 34f37de..c545b93 100644 --- a/src/components/ClickableMap/ClickableMap.js +++ b/src/components/ClickableMap/ClickableMap.js @@ -1,6 +1,5 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import Geocode from 'react-geocode'; import { GoogleMap, LoadScript, @@ -12,16 +11,26 @@ import { Button, CircularProgress, Backdrop, + TextField, makeStyles, } from '@material-ui/core'; import { useNotifications } from 'modules/notification'; -import { scrambleLocation } from 'utils/geo'; -import { DEFAULT_LATITUDE, DEFAULT_LONGITUDE } from 'constants/geo'; +import { scrambleLocation, reverseGeocode } from 'utils/geo'; +import { + DEFAULT_LATITUDE, + DEFAULT_LONGITUDE, + USED_GOOGLE_MAPS_LIBRARIES, +} from 'constants/geo'; +import PlacesAutocomplete, { + geocodeByAddress, + getLatLng, +} from 'react-places-autocomplete'; +import { MyLocation as DetectLocationIcon } from '@material-ui/icons'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import * as Sentry from '@sentry/browser'; import styles from './ClickableMap.styles'; const useStyles = makeStyles(styles); -Geocode.setApiKey(process.env.REACT_APP_FIREBASE_API_KEY); -Geocode.setRegion('us'); function SelectedLocationMarker({ position: { latitude: lat, longitude: lng }, @@ -78,23 +87,20 @@ function ClickableMap({ onLocationChange, locationInfo }) { const { showSuccess, showError } = useNotifications(); const [map, setMap] = useState(null); const [detectingLocation, setDetectingLocation] = useState(false); + const [currentPlaceLabel, setCurrentPlaceLabel] = useState(''); async function setLocation(location) { const scrambledLocation = scrambleLocation(location, 300); // Roughly within 1,000 feet. // Detect locality try { - const response = await Geocode.fromLatLng( + const response = await reverseGeocode( location.latitude, location.longitude, ); - if (response.status === 'ZERO_RESULTS') { + if (response.status === window.google.maps.GeocoderStatus.ZERO_RESULTS) { showError('Could not find the locality.'); return; } - if (response.status !== 'OK') { - showError(`Geocoding error: ${response.status}`); - return; - } const [result] = response.results; // Find city and state. @@ -129,6 +135,7 @@ function ClickableMap({ onLocationChange, locationInfo }) { latitude: location.latitude, longitude: location.longitude, }, + lookedUpAddress: result, }); // setMarkerLocation(location); } catch (err) { @@ -168,6 +175,7 @@ function ClickableMap({ onLocationChange, locationInfo }) { lat: position.coords.latitude, lng: position.coords.longitude, }); + map.setZoom(15); setLocation(loc); showSuccess('Location detected'); setDetectingLocation(false); @@ -198,50 +206,122 @@ function ClickableMap({ onLocationChange, locationInfo }) { } } + function handlePlaceChange(address) { + setCurrentPlaceLabel(address); + } + + // function handlePlaceSelect(_event, selection) { + // if (!selection) return; + + // geocodeByAddress(selection.description) + // .then((results) => getLatLng(results[0])) + // .then((latLng) => { + // map.setZoom(15); + // setLocation({ latitude: latLng.lat, longitude: latLng.lng }); + // }) + // .catch((error) => { + // showError('Failed to get the location from address.'); + // // eslint-disable-next-line no-console + // console.error('Error', error); + // }); + // } + + async function handlePlaceSelect(_event, selection) { + if (!selection) return; + try { + const [address] = await geocodeByAddress(selection.description); + const loc = await getLatLng(address); + map.setZoom(15); + map.panTo(loc); + setLocation({ latitude: loc.lat, longitude: loc.lng }); + } catch (err) { + // eslint-disable-next-line no-console + console.error('Failed to get the location from address:', err.message); + showError('Failed to get the location from address.'); + Sentry.captureException(err); + } + } + return ( - <> - - - - - Detecting Location... - - - - {preciseLocation && ( - + + + + + Detecting Location... + + +
+ + {({ getInputProps, suggestions, loading }) => ( + <> + {/* {console.log(suggestions)} */} + sug.description} + noOptionsText="No matches" + renderInput={(params) => ( + + ), + }} + /> + )} + /> + )} - + +
+ - OR - +
- {generalLocationName !== '' && ( - - General location: {generalLocationName || 'Not selected yet'} - +
+ + {preciseLocation && ( + )} -
- +
+
); } @@ -257,6 +337,8 @@ ClickableMap.propTypes = { latitude: PropTypes.number.isRequired, longitude: PropTypes.number.isRequired, }), + // If the user enters an address then this will be populated with the entry. + lookedUpAddress: PropTypes.object, }), }; diff --git a/src/components/ClickableMap/ClickableMap.styles.js b/src/components/ClickableMap/ClickableMap.styles.js index caa483c..f7a6b3e 100644 --- a/src/components/ClickableMap/ClickableMap.styles.js +++ b/src/components/ClickableMap/ClickableMap.styles.js @@ -1,7 +1,4 @@ export default (theme) => ({ - detectButton: { - margin: theme.spacing(2), - }, map: { flexGrow: 1, minHeight: '40vh', @@ -19,4 +16,38 @@ export default (theme) => ({ background: '#fff', padding: '5px', }, + entryOptions: { + display: 'flex', + flexWrap: 'wrap', + [theme.breakpoints.down('sm')]: { + flexBasis: '100%', + }, + }, + autocomplete: { + flexGrow: 1, + }, + autocompleteInputContainer: { + padding: theme.spacing(1), + }, + autocompleteInput: { + padding: theme.spacing(0.5), + }, + detectButton: { + margin: theme.spacing(1), + [theme.breakpoints.down('sm')]: { + flexGrow: 1, + }, + }, + entryOptionsSeparator: { + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: theme.palette.text.disabled, + [theme.breakpoints.down('sm')]: { + flexGrow: 1, + width: '100%', + }, + }, }); diff --git a/src/containers/Navbar/AccountMenu.js b/src/containers/Navbar/AccountMenu.js index 4d06549..1f5d44a 100644 --- a/src/containers/Navbar/AccountMenu.js +++ b/src/containers/Navbar/AccountMenu.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useFirebaseApp } from 'reactfire'; -import { useHistory } from 'react-router-dom'; +import { Link, useHistory } from 'react-router-dom'; import Menu from '@material-ui/core/Menu'; import MenuItem from '@material-ui/core/MenuItem'; import IconButton from '@material-ui/core/IconButton'; @@ -31,14 +31,6 @@ function AccountMenu() { await firebase.auth().signOut(); history.replace('/'); } - function goToAccount() { - closeAccountMenu(); - history.push(ACCOUNT_PATH); - } - function goToMyRequests() { - closeAccountMenu(); - history.push(MY_REQUESTS_PATH); - } return ( <> @@ -56,8 +48,15 @@ function AccountMenu() { transformOrigin={{ vertical: 'top', horizontal: 'right' }} open={Boolean(anchorEl)} onClose={closeAccountMenu}> - My Requests - Account + + My Requests + + + Account + Sign Out diff --git a/src/routes/NewRequest/components/NewRequestPage/NewRequestPage.js b/src/routes/NewRequest/components/NewRequestPage/NewRequestPage.js index 8509fd6..3f90724 100644 --- a/src/routes/NewRequest/components/NewRequestPage/NewRequestPage.js +++ b/src/routes/NewRequest/components/NewRequestPage/NewRequestPage.js @@ -245,11 +245,14 @@ function NewRequestPage() { Your Location - + A rough location is needed to allow us to efficiently and - quickly find a match for your need. You can either click on - the "Detect Location" button below the map or click - on the map to specify the location. + quickly find a match for your need. You can do this in three + ways: by entering your address in the address field, by + clicking the "Detect Location" button, or by + clicking on the map. If you decide to enter the address, we + will not save the actual address and instead use it to get the + location. @@ -269,9 +272,16 @@ function NewRequestPage() { Contact Information + + To minimize exposing your contact information, we do not + display it unless a volunteer specifically requests to see it. + To further discourage any abuse, we do not display your last + name and also keep track of all the volunteers who have looked + up your contact information. + - + - + - + - + + + Note: This website and all related work products are provided + "AS IS". The provider of this service makes no other + warranties, express or implied, and hereby disclaims all + implied warranties, including any warranty of merchantability + and warranty of fitness for a particular purpose. + + {dirty && !!Object.keys(errors).length && !isValid && ( Please fix the errors above. diff --git a/src/routes/NewRequest/components/NewRequestPage/NewRequestPage.styles.js b/src/routes/NewRequest/components/NewRequestPage/NewRequestPage.styles.js index cdc2842..33979e1 100644 --- a/src/routes/NewRequest/components/NewRequestPage/NewRequestPage.styles.js +++ b/src/routes/NewRequest/components/NewRequestPage/NewRequestPage.styles.js @@ -6,7 +6,6 @@ export default (theme) => ({ paddingBottom: theme.spacing(3), }, paper: { - // paddingTop: theme.spacing(3), paddingBottom: theme.spacing(1), }, optionalDivider: { @@ -30,4 +29,7 @@ export default (theme) => ({ radio: { marginLeft: theme.spacing(1), }, + warrantyInfo: { + marginTop: theme.spacing(4), + }, }); diff --git a/src/routes/NewRequest/components/NewRequestPage/useNewRequestPage.js b/src/routes/NewRequest/components/NewRequestPage/useNewRequestPage.js index 638e387..8314d1e 100644 --- a/src/routes/NewRequest/components/NewRequestPage/useNewRequestPage.js +++ b/src/routes/NewRequest/components/NewRequestPage/useNewRequestPage.js @@ -3,7 +3,6 @@ import { useHistory } from 'react-router-dom'; import { useFirestore, useUser, useAnalytics } from 'reactfire'; import { useNotifications } from 'modules/notification'; import { - USERS_PRIVILEGED_COLLECTION, USERS_COLLECTION, REQUESTS_COLLECTION, REQUESTS_PUBLIC_COLLECTION, @@ -61,6 +60,7 @@ export default function useNewRequestPage() { alert('Please select a location by clicking on the map above.'); // eslint-disable-line no-alert return; } + const { lookedUpAddress, ...loc } = requestLocation; const { lastName, phone, email, ...publicValues } = values; @@ -74,10 +74,10 @@ export default function useNewRequestPage() { usersWithContactInfoAccess: [], status: 1, generalLocation: new GeoPoint( - requestLocation.generalLocation.latitude, - requestLocation.generalLocation.longitude, + loc.generalLocation.latitude, + loc.generalLocation.longitude, ), - generalLocationName: requestLocation.generalLocationName, + generalLocationName: loc.generalLocationName, }; // Convert needs to an array @@ -99,7 +99,7 @@ export default function useNewRequestPage() { needs: requestPublicInfo.needs, status: 1, createdAt: FieldValue.serverTimestamp(), - ...requestLocation, + ...loc, }; let userInfo = null; @@ -107,25 +107,14 @@ export default function useNewRequestPage() { requestPublicInfo.createdBy = user.uid; requestPrivateInfo.createdBy = user.uid; - const userRef = firestore.doc( - `${USERS_PRIVILEGED_COLLECTION}/${user.uid}`, - ); + const userRef = firestore.doc(`${USERS_COLLECTION}/${user.uid}`); const profile = (await userRef.get()).data(); - let nameParts = user.displayName?.split(' '); - if (nameParts.length < 2) { - nameParts = [user.displayName, '']; - } - if (profile) { - profile.displayName = user.displayName; - [profile.firstName, profile.lastName] = nameParts; - - userInfo = { - uid: user.uid, - firstName: profile.firstName, - displayName: profile.displayName, - }; - requestPublicInfo.createdByInfo = userInfo; - } + userInfo = { + uid: user.uid, + firstName: profile.firstName, + displayName: profile.displayName || '', + }; + requestPublicInfo.createdByInfo = userInfo; } /* eslint-disable no-console */ @@ -163,7 +152,7 @@ export default function useNewRequestPage() { showSuccess('Request submitted!'); analytics.logEvent('new-request', action); - history.replace(REQUEST_SUCCESSFUL_PATH); + history.replace(`${REQUEST_SUCCESSFUL_PATH}?id=${requestRef.id}`); } catch (err) { showError(err.message || 'Error submitting request'); } diff --git a/src/routes/NewUser/components/NewUserPage/NewUserPage.js b/src/routes/NewUser/components/NewUserPage/NewUserPage.js index 53f45b3..03be7bf 100644 --- a/src/routes/NewUser/components/NewUserPage/NewUserPage.js +++ b/src/routes/NewUser/components/NewUserPage/NewUserPage.js @@ -52,6 +52,7 @@ function NewUser() { const [retries, setRetries] = useState(0); const [userRef, setUserRef] = useState(null); + const [userData, setUserData] = useState(null); const [userProfile, setUserProfile] = useState(null); // Because of timing issues, this component will likely get run before the server has applied @@ -63,7 +64,7 @@ function NewUser() { try { const ref = firestore.doc(`${USERS_COLLECTION}/${user.uid}`); // Call it once because this will throw the permission exception. - await ref.get(); + setUserData(await ref.get()); setUserProfile(await ref.get()); setUserRef(ref); } catch (err) { @@ -94,12 +95,67 @@ function NewUser() { errors, register, formState: { isValid, dirty }, + getValues, + setValue, } = useForm({ validationSchema: userProfileSchema, defaultValues, }); const [userLocationInfo, setUserLocationInfo] = useState(null); + /** + * Sets the value for the given field if it isn't already populated. + * @param {string} fieldName name of the field which will be set + * @param {string} allFieldValues current value of all fields + * @param {object} googleResultComponent address component returned by the Google Maps API + * @param {object} addressType type of address component to lookup + * @param {string} addressTypeFormat short_name or long_name + */ + function setValueIfNotAlreadySet( + fieldName, + allFieldValues, + googleResultComponent, + addressType, + addressTypeFormat = 'long_name', + ) { + if ( + googleResultComponent.types.includes(addressType) && + !allFieldValues[fieldName] + ) { + setValue(fieldName, googleResultComponent[addressTypeFormat]); + } + } + + function handleSetUserLocationInfo(locationInfo) { + setUserLocationInfo(locationInfo); + const result = locationInfo.lookedUpAddress; + if (result) { + const values = getValues(); + let streetNumber = ''; + let streetAddress = ''; + result.address_components.forEach((component) => { + setValueIfNotAlreadySet('city', values, component, 'locality'); + setValueIfNotAlreadySet( + 'state', + values, + component, + 'administrative_area_level_1', + 'short_name', + ); + setValueIfNotAlreadySet('zipcode', values, component, 'postal_code'); + if (component.types.includes('street_number')) { + streetNumber = component.short_name; + } + if (component.types.includes('route')) { + streetAddress = component.short_name; + } + }); + if (streetNumber && streetAddress && !values.address1) { + setValue('address1', `${streetNumber} ${streetAddress}`); + } + } + } + if (userProfile && userProfile.preciseLocation) { return ( @@ -140,12 +196,20 @@ function NewUser() { // }; async function handleFormSubmit(values) { + const newValues = values; if (!userLocationInfo) { alert('Please select a location above.'); // eslint-disable-line no-alert return; } - const userUpdates = { ...values, ...userLocationInfo }; + delete userLocationInfo.lookedUpAddress; + + // Default the displayName if it isn't already set (e.g., when signing up using email). + if (!userData.get('displayName')) { + newValues.displayName = `${values.firstName} ${values.lastName}`; + } + + const userUpdates = { ...newValues, ...userLocationInfo }; await userRef.set(userUpdates, { merge: true }); history.replace(SEARCH_PATH); } @@ -218,29 +282,29 @@ function NewUser() { fullWidth inputRef={register} error={!!errors.phone} - helperText={errors.phone && 'phone must be valid'} + helperText={errors.phone && 'Phone must be valid'} /> - Please click or tap on your location + Your Location A rough location is needed to allow us to efficiently and quickly - find a match. You can either click on the "Detect - Location" button below the map or click on the map to specify - the location. + find a match. You can do this in three ways: by entering your + address in the address field, by clicking the "Detect + Location" button, or by clicking on the map.{' '} - Optional – Address + Address – Optional, but recommended @@ -251,6 +315,7 @@ function NewUser() { margin="normal" fullWidth inputRef={register} + InputLabelProps={{ shrink: true }} error={!!errors.address1} helperText={errors.address1 && 'address1 must be valid'} /> @@ -263,6 +328,7 @@ function NewUser() { margin="normal" fullWidth inputRef={register} + InputLabelProps={{ shrink: true }} error={!!errors.address2} helperText={errors.address2 && 'Address 2 must be valid'} /> @@ -277,6 +343,7 @@ function NewUser() { margin="normal" fullWidth inputRef={register} + InputLabelProps={{ shrink: true }} error={!!errors.city} helperText={errors.city && 'City must be valid'} /> @@ -289,6 +356,7 @@ function NewUser() { margin="normal" fullWidth inputRef={register} + InputLabelProps={{ shrink: true }} error={!!errors.state} helperText={errors.state && 'State must be valid'} /> @@ -301,6 +369,7 @@ function NewUser() { margin="normal" fullWidth inputRef={register} + InputLabelProps={{ shrink: true }} error={!!errors.zipcode} helperText={errors.zipcode && 'State must be valid'} /> diff --git a/src/routes/Request/components/Discussion/Discussion.js b/src/routes/Request/components/Discussion/Discussion.js index 11a0bf2..380cc89 100644 --- a/src/routes/Request/components/Discussion/Discussion.js +++ b/src/routes/Request/components/Discussion/Discussion.js @@ -51,8 +51,7 @@ function CommentList({ requestId }) { const dataQuery = firestore .collection(`${REQUESTS_DISCUSSIONS_COLLECTION}`) .where('requestId', '==', requestId) - .orderBy('createdAt', 'asc') - .limit(1); + .orderBy('createdAt', 'asc'); // Call it once because this will throw the permission exception. await dataQuery.get(); setQuery(dataQuery); // Setting this will trigger the subscription useEffect. diff --git a/src/routes/RequestSuccessful/components/RequestSuccessfulPage/RequestSuccessfulPage.js b/src/routes/RequestSuccessful/components/RequestSuccessfulPage/RequestSuccessfulPage.js index 789f389..6f548fc 100644 --- a/src/routes/RequestSuccessful/components/RequestSuccessfulPage/RequestSuccessfulPage.js +++ b/src/routes/RequestSuccessful/components/RequestSuccessfulPage/RequestSuccessfulPage.js @@ -1,19 +1,23 @@ import React from 'react'; import Typography from '@material-ui/core/Typography'; -import { Link } from 'react-router-dom'; +import { Link, useLocation, generatePath } from 'react-router-dom'; +import queryString from 'query-string'; import { makeStyles } from '@material-ui/core'; import HomeIcon from '@material-ui/icons/Home'; import Container from '@material-ui/core/Container'; import Paper from '@material-ui/core/Paper'; -import Grid from '@material-ui/core/Grid'; import Button from '@material-ui/core/Button'; import { Helmet } from 'react-helmet'; +import { REQUEST_PATH } from 'constants/paths'; import styles from './RequestSuccessfulPage.styles'; const useStyles = makeStyles(styles); function RequestSuccessfulPage() { const classes = useStyles(); + const location = useLocation(); + const qs = queryString.parse(location.search); + const requestId = (qs && qs.id) || null; return ( <> @@ -26,7 +30,7 @@ function RequestSuccessfulPage() { - Here’s what to expect: + Here’s what to expect
    @@ -70,19 +74,31 @@ function RequestSuccessfulPage() { until the volunteer has left the premises. */} + + Please note that we will try our best to match you with a volunteer, + but we cannot guarantee that we will find a match. + + + Stay safe!
    - - - - - + {requestId && ( + + )} +
    diff --git a/src/routes/RequestSuccessful/components/RequestSuccessfulPage/RequestSuccessfulPage.styles.js b/src/routes/RequestSuccessful/components/RequestSuccessfulPage/RequestSuccessfulPage.styles.js index bcb1b9a..83da424 100644 --- a/src/routes/RequestSuccessful/components/RequestSuccessfulPage/RequestSuccessfulPage.styles.js +++ b/src/routes/RequestSuccessful/components/RequestSuccessfulPage/RequestSuccessfulPage.styles.js @@ -3,6 +3,11 @@ export default (theme) => ({ padding: theme.spacing(3), }, buttons: { + display: 'flex', + justifyContent: 'center', marginTop: theme.spacing(3), + '& > *': { + marginRight: theme.spacing(2), + }, }, }); diff --git a/src/routes/Search/components/SearchPage/SearchPage.js b/src/routes/Search/components/SearchPage/SearchPage.js index 8e03bba..3843d85 100644 --- a/src/routes/Search/components/SearchPage/SearchPage.js +++ b/src/routes/Search/components/SearchPage/SearchPage.js @@ -316,14 +316,14 @@ function SearchPage() { nearbyRequests.map((result) => ( - + {result.distance}
    miles
    - + {parseInt(result.immediacy, 10) > 5 && ( { + const geocoder = new window.google.maps.Geocoder(); + geocoder.geocode( + { location: { lat: latitude, lng: longitude } }, + (results, status) => { + if (status !== window.google.maps.GeocoderStatus.OK) { + reject(status); + } else { + resolve({ status, results }); + } + }, + ); + }); +} diff --git a/yarn.lock b/yarn.lock index ae33c6f..48efaca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -966,13 +966,20 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.9.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q== dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.8.3", "@babel/runtime@^7.9.6": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f" + integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.4.0", "@babel/template@^7.8.3", "@babel/template@^7.8.6": version "7.8.6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" @@ -1657,20 +1664,21 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^13.0.0" -"@material-ui/core@^4.9.8": - version "4.9.9" - resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.9.9.tgz#902dc37eeb415dd3288feb2c92e0dc27d9caed48" - integrity sha512-Gp0UdJLxPEnkn7O0QpJ2/LOeIuT8nX9e6CjQFuLnOy10rUGjRsOZ2T170Y057xdUmw1VNE+0bvkkO6dOghxt4g== +"@material-ui/core@^4.9.12": + version "4.9.12" + resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.9.12.tgz#0156d87c8b5db5a8aad992d6883a081a32684e2e" + integrity sha512-JtRm1iNw3PRg+bzULS1uRKhdIJ2jhKO3/5ptO6kTADARsv5KmhzMbM+PYmVS09qm9Yu3ilwka4dYrtjqea53Lw== dependencies: "@babel/runtime" "^7.4.4" - "@material-ui/styles" "^4.9.6" - "@material-ui/system" "^4.9.6" - "@material-ui/types" "^5.0.0" - "@material-ui/utils" "^4.9.6" + "@material-ui/react-transition-group" "^4.3.0" + "@material-ui/styles" "^4.9.10" + "@material-ui/system" "^4.9.10" + "@material-ui/types" "^5.0.1" + "@material-ui/utils" "^4.9.12" "@types/react-transition-group" "^4.2.0" - clsx "^1.0.2" + clsx "^1.0.4" hoist-non-react-statics "^3.3.2" - popper.js "^1.14.1" + popper.js "^1.16.1-lts" prop-types "^15.7.2" react-is "^16.8.0" react-transition-group "^4.3.0" @@ -1682,10 +1690,10 @@ dependencies: "@babel/runtime" "^7.4.4" -"@material-ui/lab@^4.0.0-alpha.48": - version "4.0.0-alpha.48" - resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.48.tgz#47a54cc9d80c2a0446506530cec263d98ae64691" - integrity sha512-iGuTGUvIR3LLkd4LPJRl1Sa92TKmoRa3B3p0I+IllAx+uK1h/aXr++6CnYmTS+gubWRQ8yWQVLRADVrPuLnvCA== +"@material-ui/lab@^4.0.0-alpha.51": + version "4.0.0-alpha.51" + resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.51.tgz#c50c429a374a1d7e31591677137c000918985b89" + integrity sha512-X/qv/sZQGhXhKDn83L94gNahGDQj2Rd6r7/9tPpQbSn2A1LAt1+jlTiWD1HUgDXZEPqTsJMajOjWSEmTL7/q7w== dependencies: "@babel/runtime" "^7.4.4" "@material-ui/utils" "^4.9.6" @@ -1693,14 +1701,24 @@ prop-types "^15.7.2" react-is "^16.8.0" -"@material-ui/styles@^4.9.6": - version "4.9.6" - resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.9.6.tgz#924a30bf7c9b91af9c8f19c12c8573b8a4ecd085" - integrity sha512-ijgwStEkw1OZ6gCz18hkjycpr/3lKs1hYPi88O/AUn4vMuuGEGAIrqKVFq/lADmZUNF3DOFIk8LDkp7zmjPxtA== +"@material-ui/react-transition-group@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@material-ui/react-transition-group/-/react-transition-group-4.3.0.tgz#92529142addb5cc179dbf42d246c7e3fe4d6104b" + integrity sha512-CwQ0aXrlUynUTY6sh3UvKuvye1o92en20VGAs6TORnSxUYeRmkX8YeTUN3lAkGiBX1z222FxLFO36WWh6q73rQ== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +"@material-ui/styles@^4.9.10": + version "4.9.10" + resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.9.10.tgz#182ccdd0bc8525a459486499bbaebcd92b0db3ab" + integrity sha512-EXIXlqVyFDnjXF6tj72y6ZxiSy+mHtrsCo3Srkm3XUeu3Z01aftDBy7ZSr3TQ02gXHTvDSBvegp3Le6p/tl7eA== dependencies: "@babel/runtime" "^7.4.4" "@emotion/hash" "^0.8.0" - "@material-ui/types" "^5.0.0" + "@material-ui/types" "^5.0.1" "@material-ui/utils" "^4.9.6" clsx "^1.0.2" csstype "^2.5.2" @@ -1715,24 +1733,24 @@ jss-plugin-vendor-prefixer "^10.0.3" prop-types "^15.7.2" -"@material-ui/system@^4.9.6": - version "4.9.6" - resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.9.6.tgz#fd060540224da4d1740da8ca6e7af288e217717e" - integrity sha512-QtfoAePyqXoZ2HUVSwGb1Ro0kucMCvVjbI0CdYIR21t0Opgfm1Oer6ni9P5lfeXA39xSt0wCierw37j+YES48Q== +"@material-ui/system@^4.9.10": + version "4.9.10" + resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.9.10.tgz#5de6ec7bea0f222b10b45e5bd5bb8b9a7b938926" + integrity sha512-E+t0baX2TBZk6ALm8twG6objpsxLdMM4MDm1++LMt2m7CetCAEc3aIAfDaprk4+tm5hFT1Cah5dRWk8EeIFQYw== dependencies: "@babel/runtime" "^7.4.4" "@material-ui/utils" "^4.9.6" prop-types "^15.7.2" -"@material-ui/types@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-5.0.0.tgz#26d6259dc6b39f4c2e1e9aceff7a11e031941741" - integrity sha512-UeH2BuKkwDndtMSS0qgx1kCzSMw+ydtj0xx/XbFtxNSTlXydKwzs5gVW5ZKsFlAkwoOOQ9TIsyoCC8hq18tOwg== +"@material-ui/types@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-5.0.1.tgz#c4954063cdc196eb327ee62c041368b1aebb6d61" + integrity sha512-wURPSY7/3+MAtng3i26g+WKwwNE3HEeqa/trDBR5+zWKmcjO+u9t7Npu/J1r+3dmIa/OeziN9D/18IrBKvKffw== -"@material-ui/utils@^4.9.6": - version "4.9.6" - resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-4.9.6.tgz#5f1f9f6e4df9c8b6a263293b68c94834248ff157" - integrity sha512-gqlBn0JPPTUZeAktn1rgMcy9Iczrr74ecx31tyZLVGdBGGzsxzM6PP6zeS7FuoLS6vG4hoZP7hWnOoHtkR0Kvw== +"@material-ui/utils@^4.9.12", "@material-ui/utils@^4.9.6": + version "4.9.12" + resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-4.9.12.tgz#0d639f1c1ed83fffb2ae10c21d15a938795d9e65" + integrity sha512-/0rgZPEOcZq5CFA4+4n6Q6zk7fi8skHhH2Bcra8R3epoJEYy5PL55LuMazPtPH1oKeRausDV/Omz4BbgFsn1HQ== dependencies: "@babel/runtime" "^7.4.4" prop-types "^15.7.2" @@ -1804,24 +1822,24 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= -"@react-google-maps/api@^1.8.6": - version "1.8.6" - resolved "https://registry.yarnpkg.com/@react-google-maps/api/-/api-1.8.6.tgz#0a68f5f726f2cb2551f855c9f8493c854fb2782f" - integrity sha512-zioXQwqc6Cuw8hOkuf4TIJdvzgXGT+h7Hj2SVFm9AHvfaYMfhNxVnID/8txaX/iDxKG83qTFwMH3AoRrNSlaFQ== +"@react-google-maps/api@^1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@react-google-maps/api/-/api-1.9.0.tgz#252c411687a490b6f20ad89d699a127d8e144e72" + integrity sha512-rDFJW18zir92dy9AqbT6KvtQt7vTxbR4o6mUg4I8igadEzM8CTH5zR+CIWKfMy89Bc2hZJVMHU/2+zPu63VHRw== dependencies: - "@react-google-maps/infobox" "1.8.6" - "@react-google-maps/marker-clusterer" "1.8.6" + "@react-google-maps/infobox" "1.8.7" + "@react-google-maps/marker-clusterer" "1.8.7" invariant "2.2.4" -"@react-google-maps/infobox@1.8.6": - version "1.8.6" - resolved "https://registry.yarnpkg.com/@react-google-maps/infobox/-/infobox-1.8.6.tgz#16b00052581fcafc3ca073e73100f05cb1f13002" - integrity sha512-q3V1NdVzMLesW2/TEoszzLPOFXpovjrEgyXR0ftZIZBRqXWPl/YKjYUYDDSKf6GDo0d91r8K/yBqkywQBfQENQ== +"@react-google-maps/infobox@1.8.7": + version "1.8.7" + resolved "https://registry.yarnpkg.com/@react-google-maps/infobox/-/infobox-1.8.7.tgz#0e0c293c3f1bd4a07fddda4a762e2b43f45ba3ff" + integrity sha512-QKblFDfiWrBnuGui940YxH7OComeh+fpZ6yr1oDv/bZmWKMxTtOiv64uPmXS5ORCxBX0i5G/JiCCKbznvvMkzQ== -"@react-google-maps/marker-clusterer@1.8.6": - version "1.8.6" - resolved "https://registry.yarnpkg.com/@react-google-maps/marker-clusterer/-/marker-clusterer-1.8.6.tgz#ec26d76e890fa8a7ee7d6a810fce29a39e156e7b" - integrity sha512-aEJikokaGBd6hPhTXDSaQCSUaXrvSoKUmmXRFIq2sQz0hp4oJJnlkSl2Me6cV0inUaboNCK2lunKGCsgDrzOzQ== +"@react-google-maps/marker-clusterer@1.8.7": + version "1.8.7" + resolved "https://registry.yarnpkg.com/@react-google-maps/marker-clusterer/-/marker-clusterer-1.8.7.tgz#7b357cc627056231cd63b75cf383246a174605d4" + integrity sha512-sJf5rhBVcykCZ5dmBCeoScRpXGq10KsDe+9AaztLTltaJaHtIV3oL6JMmUHWV0TA3IN69kz0CpjR8rQkeUH72A== "@samverschueren/stream-to-observable@^0.3.0": version "0.3.0" @@ -1830,56 +1848,56 @@ dependencies: any-observable "^0.3.0" -"@sentry/browser@^5.15.4": - version "5.15.4" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.15.4.tgz#5a7e7bad088556665ed8e69bceb0e18784e4f6c7" - integrity sha512-l/auT1HtZM3KxjCGQHYO/K51ygnlcuOrM+7Ga8gUUbU9ZXDYw6jRi0+Af9aqXKmdDw1naNxr7OCSy6NBrLWVZw== +"@sentry/browser@^5.15.5": + version "5.15.5" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.15.5.tgz#d9a51f1388581067b50d30ed9b1aed2cbb333a36" + integrity sha512-rqDvjk/EvogfdbZ4TiEpxM/lwpPKmq23z9YKEO4q81+1SwJNua53H60dOk9HpRU8nOJ1g84TMKT2Ov8H7sqDWA== dependencies: - "@sentry/core" "5.15.4" - "@sentry/types" "5.15.4" - "@sentry/utils" "5.15.4" + "@sentry/core" "5.15.5" + "@sentry/types" "5.15.5" + "@sentry/utils" "5.15.5" tslib "^1.9.3" -"@sentry/core@5.15.4": - version "5.15.4" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.15.4.tgz#08b617e093a636168be5aebad141d1f744217085" - integrity sha512-9KP4NM4SqfV5NixpvAymC7Nvp36Zj4dU2fowmxiq7OIbzTxGXDhwuN/t0Uh8xiqlkpkQqSECZ1OjSFXrBldetQ== +"@sentry/core@5.15.5": + version "5.15.5" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.15.5.tgz#40ea79bff5272d3fbbeeb4a98cdc59e1adbd2c92" + integrity sha512-enxBLv5eibBMqcWyr+vApqeix8uqkfn0iGsD3piKvoMXCgKsrfMwlb/qo9Ox0lKr71qIlZVt+9/A2vZohdgnlg== dependencies: - "@sentry/hub" "5.15.4" - "@sentry/minimal" "5.15.4" - "@sentry/types" "5.15.4" - "@sentry/utils" "5.15.4" + "@sentry/hub" "5.15.5" + "@sentry/minimal" "5.15.5" + "@sentry/types" "5.15.5" + "@sentry/utils" "5.15.5" tslib "^1.9.3" -"@sentry/hub@5.15.4": - version "5.15.4" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.15.4.tgz#cb64473725a60eec63b0be58ed1143eaaf894bee" - integrity sha512-1XJ1SVqadkbUT4zLS0TVIVl99si7oHizLmghR8LMFl5wOkGEgehHSoOydQkIAX2C7sJmaF5TZ47ORBHgkqclUg== +"@sentry/hub@5.15.5": + version "5.15.5" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.15.5.tgz#f5abbcdbe656a70e2ff02c02a5a4cffa0f125935" + integrity sha512-zX9o49PcNIVMA4BZHe//GkbQ4Jx+nVofqU/Il32/IbwKhcpPlhGX3c1sOVQo4uag3cqd/JuQsk+DML9TKkN0Lw== dependencies: - "@sentry/types" "5.15.4" - "@sentry/utils" "5.15.4" + "@sentry/types" "5.15.5" + "@sentry/utils" "5.15.5" tslib "^1.9.3" -"@sentry/minimal@5.15.4": - version "5.15.4" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.15.4.tgz#113f01fefb86b7830994c3dfa7ad4889ba7b2003" - integrity sha512-GL4GZ3drS9ge+wmxkHBAMEwulaE7DMvAEfKQPDAjg2p3MfcCMhAYfuY4jJByAC9rg9OwBGGehz7UmhWMFjE0tw== +"@sentry/minimal@5.15.5": + version "5.15.5" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.15.5.tgz#a0e4e071f01d9c4d808094ae7203f6c4cca9348a" + integrity sha512-zQkkJ1l9AjmU/Us5IrOTzu7bic4sTPKCatptXvLSTfyKW7N6K9MPIIFeSpZf9o1yM2sRYdK7GV08wS2eCT3JYw== dependencies: - "@sentry/hub" "5.15.4" - "@sentry/types" "5.15.4" + "@sentry/hub" "5.15.5" + "@sentry/types" "5.15.5" tslib "^1.9.3" -"@sentry/types@5.15.4": - version "5.15.4" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.15.4.tgz#37f30e35b06e8e12ad1101f1beec3e9b88ca1aab" - integrity sha512-quPHPpeAuwID48HLPmqBiyXE3xEiZLZ5D3CEbU3c3YuvvAg8qmfOOTI6z4Z3Eedi7flvYpnx3n7N3dXIEz30Eg== +"@sentry/types@5.15.5": + version "5.15.5" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.15.5.tgz#16c97e464cf09bbd1d2e8ce90d130e781709076e" + integrity sha512-F9A5W7ucgQLJUG4LXw1ZIy4iLevrYZzbeZ7GJ09aMlmXH9PqGThm1t5LSZlVpZvUfQ2rYA8NU6BdKJSt7B5LPw== -"@sentry/utils@5.15.4": - version "5.15.4" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.15.4.tgz#02865ab3c9b745656cea0ab183767ec26c96f6e6" - integrity sha512-lO8SLBjrUDGADl0LOkd55R5oL510d/1SaI08/IBHZCxCUwI4TiYo5EPECq8mrj3XGfgCyq9osw33bymRlIDuSQ== +"@sentry/utils@5.15.5": + version "5.15.5" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.15.5.tgz#dec1d4c79037c4da08b386f5d34409234dcbfb15" + integrity sha512-Nl9gl/MGnzSkuKeo3QaefoD/OJrFLB8HmwQ7HUbTXb6E7yyEzNKAQMHXGkwNAjbdYyYbd42iABP6Y5F/h39NtA== dependencies: - "@sentry/types" "5.15.4" + "@sentry/types" "5.15.5" tslib "^1.9.3" "@svgr/babel-plugin-add-jsx-attribute@^4.2.0": @@ -10655,7 +10673,7 @@ pnp-webpack-plugin@1.6.4: dependencies: ts-pnp "^1.1.6" -popper.js@^1.14.1: +popper.js@^1.16.1-lts: version "1.16.1" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ== @@ -11439,7 +11457,7 @@ prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.8.1" -property-expr@^2.0.0: +property-expr@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.2.tgz#fff2a43919135553a3bc2fdd94bdb841965b2330" integrity sha512-bc/5ggaYZxNkFKj374aLbEDqVADdYaLcFo8XBkishUWbaAdjlphaBFns9TvRA2pUseVL/wMFmui9X3IdNDU37g== @@ -11575,7 +11593,7 @@ query-string@^4.1.0: object-assign "^4.1.0" strict-uri-encode "^1.0.0" -query-string@^6.11.0: +query-string@^6.11.0, query-string@^6.12.1: version "6.12.1" resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.12.1.tgz#2ae4d272db4fba267141665374e49a1de09e8a7c" integrity sha512-OHj+zzfRMyj3rmo/6G8a5Ifvw3AleL/EbcHMD27YA31Q+cO5lfmQxECkImuNVjcskLcvBRVHNAB3w6udMs1eAA== @@ -11584,15 +11602,6 @@ query-string@^6.11.0: split-on-first "^1.0.0" strict-uri-encode "^2.0.0" -query-string@^6.12.0: - version "6.12.0" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.12.0.tgz#fa0fe5b3ddf4d040d1236b80672949ab33d5cf80" - integrity sha512-aoiFW9ZU7jP8Itjqfpw80Qe7RoyCIhFrW522sdsp9LG92pat6CCG3d8qNZBaUi71FsEjIfLjx9Ky347FtVoqXA== - dependencies: - decode-uri-component "^0.2.0" - split-on-first "^1.0.0" - strict-uri-encode "^2.0.0" - querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -11722,13 +11731,6 @@ react-fast-compare@^2.0.4: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== -react-geocode@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/react-geocode/-/react-geocode-0.2.1.tgz#c9f58d979097272831a01ef0ebd89add8e15725a" - integrity sha512-kipHpIA1w0XbQ2x6BOpLsup9kui6yjjAaXigCtom85xiKDB2KqDlxYx7dzd68Hxd5KG27nSFiWNRLosjKVg6VQ== - dependencies: - regenerator-runtime "^0.13.3" - react-google-button@^0.7.0: version "0.7.1" resolved "https://registry.yarnpkg.com/react-google-button/-/react-google-button-0.7.1.tgz#e84b4ce270a66e345489dc86e47235e877fcc81a" @@ -11746,10 +11748,10 @@ react-helmet@^6.0.0: react-fast-compare "^2.0.4" react-side-effect "^2.1.0" -react-hook-form@^5.2.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-5.3.1.tgz#96b6f26d823d008e22a6f4ab85628a82d2f0b52e" - integrity sha512-n73nVyBaTBL4r2//6ZWi5x41ZYsKRtrqbNf40KILv4XrNO/DQbzFRdr/s53Ua+M0y76TKS3xmOEnk6hWhVTR/w== +react-hook-form@^5.6.1: + version "5.6.1" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-5.6.1.tgz#8b6017ec01731bde4f6a6bcd94955ff5e69e28cc" + integrity sha512-pqlAiWyZqngdEVUinxPSmhs9k2L/ETsTf8LU7wfwrE1bXJnslRsb4aKlYiL2z0bgGrCuksN8el5bxFbnR1/qEA== react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4: version "16.13.1" @@ -14725,16 +14727,16 @@ yauzl@2.10.0, yauzl@^2.10.0: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" -yup@^0.28.3: - version "0.28.3" - resolved "https://registry.yarnpkg.com/yup/-/yup-0.28.3.tgz#1ca607405a8adf24a5ac51f54bd09d527555f0ba" - integrity sha512-amVkCgFWe5bGjrrUiODkbIzrSwtB8JpZrQYSrfj2YsbRdrV+tn9LquWdZDlfOx2HXyfEA8FGnlwidE/bFDxO7Q== +yup@^0.28.5: + version "0.28.5" + resolved "https://registry.yarnpkg.com/yup/-/yup-0.28.5.tgz#85cabb4000d3623ef69be81551190692e631a8a5" + integrity sha512-7JZcvpUGUxMKoaEtcoMEM8lCWRaueGNH/A3EhL/UWqfbFm3uloiI+x59Yq4nzhbbYWUTwAsCteaZOJ+VbqI1uw== dependencies: - "@babel/runtime" "^7.8.7" + "@babel/runtime" "^7.9.6" fn-name "~3.0.0" lodash "^4.17.15" lodash-es "^4.17.11" - property-expr "^2.0.0" + property-expr "^2.0.2" synchronous-promise "^2.0.10" toposort "^2.0.2" From 8bea3bda97a8823617c90ff505bb75101c574c76 Mon Sep 17 00:00:00 2001 From: Tim Escher Date: Fri, 8 May 2020 17:58:55 -0700 Subject: [PATCH 02/15] Start of user profile --- src/constants/paths.js | 1 + src/containers/Navbar/AccountMenu.js | 9 +- .../UserProfilePage/UserProfilePage.js | 408 ++++++++++++++++++ .../UserProfilePage/UserProfilePage.styles.js | 27 ++ .../components/UserProfilePage/index.js | 3 + src/routes/UserProfile/index.js | 11 + src/routes/index.js | 2 + 7 files changed, 458 insertions(+), 3 deletions(-) create mode 100644 src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js create mode 100644 src/routes/UserProfile/components/UserProfilePage/UserProfilePage.styles.js create mode 100644 src/routes/UserProfile/components/UserProfilePage/index.js create mode 100644 src/routes/UserProfile/index.js diff --git a/src/constants/paths.js b/src/constants/paths.js index 1043cb0..496ad36 100644 --- a/src/constants/paths.js +++ b/src/constants/paths.js @@ -3,6 +3,7 @@ export const ACCOUNT_PATH = '/account'; export const LOGIN_PATH = '/login'; export const LOGOUT_PATH = '/logout'; export const SEARCH_PATH = '/search'; +export const USER_PROFILE_PATH = '/user-profile'; export const PRIVACY_POLICY_PATH = '/privacy-policy'; export const TERMS_OF_SERVICE_PATH = '/terms-of-service'; diff --git a/src/containers/Navbar/AccountMenu.js b/src/containers/Navbar/AccountMenu.js index 1f5d44a..3fa6c24 100644 --- a/src/containers/Navbar/AccountMenu.js +++ b/src/containers/Navbar/AccountMenu.js @@ -6,7 +6,7 @@ import MenuItem from '@material-ui/core/MenuItem'; import IconButton from '@material-ui/core/IconButton'; import AccountCircle from '@material-ui/icons/AccountCircle'; import { makeStyles } from '@material-ui/core/styles'; -import { ACCOUNT_PATH, MY_REQUESTS_PATH } from 'constants/paths'; +import { USER_PROFILE_PATH, MY_REQUESTS_PATH } from 'constants/paths'; const useStyles = makeStyles(() => ({ buttonRoot: { @@ -54,8 +54,11 @@ function AccountMenu() { onClick={closeAccountMenu}> My Requests - - Account + + My Profile Sign Out diff --git a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js new file mode 100644 index 0000000..87cd4a0 --- /dev/null +++ b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js @@ -0,0 +1,408 @@ +import React, { useState, useEffect } from 'react'; +import { + Typography, + TextField, + Paper, + Divider, + Button, + Card, + CardActions, + CardContent, + Container, + Grid, + makeStyles, +} from '@material-ui/core'; +import { + ExitToApp as LogoutIcon, + Person as AccountIcon, +} from '@material-ui/icons'; +import { Link, useHistory } from 'react-router-dom'; +import { Helmet } from 'react-helmet'; +import Stepper from '@material-ui/core/Stepper'; +import Step from '@material-ui/core/Step'; +import StepLabel from '@material-ui/core/StepLabel'; +import Check from '@material-ui/icons/Check'; +import * as Yup from 'yup'; +import { useFirestore, useUser } from 'reactfire'; +import { USERS_COLLECTION } from 'constants/collections'; +import ClickableMap from 'components/ClickableMap'; +import { useForm } from 'react-hook-form'; +import { validateEmail } from 'utils/form'; +import { ACCOUNT_PATH, LOGOUT_PATH, SEARCH_PATH } from 'constants/paths'; +import styles from './UserProfilePage.styles'; + +const useStyles = makeStyles(styles); + +const userProfileSchema = Yup.object().shape({ + firstName: Yup.string().min(2, 'Too Short').required('Required'), + lastName: Yup.string().min(2, 'Too Short').required('Required'), + email: Yup.string() + .email('Please enter a valid email address') + .required('Required'), + phone: Yup.string().required('Required'), + address1: Yup.string(), + address2: Yup.string(), + city: Yup.string(), + state: Yup.string(), + zipcode: Yup.string(), +}); + +function getSteps() { + return ['Email', 'Password', 'Contact Info', 'Location']; +} + +function getStepContent(step) { + switch (step) { + case 0: + return 'Email'; + case 1: + return 'Password'; + case 2: + return 'Contact Info'; + case 3: + return 'Where Are You?'; + default: + return 'Unknown step'; + } +} + +function isLoggedIn(user) { + return user && user.uid; +} + +function isGoogleLoggedIn(user) { + return user && user.providerId === 'google'; +} + +function UserProfile() { + const classes = useStyles(); + const firestore = useFirestore(); + const history = useHistory(); + const user = useUser(); + const defaultValues = {}; + + const [retries, setRetries] = useState(0); + const [userRef, setUserRef] = useState(null); + const [userData, setUserData] = useState(null); + const [userProfile, setUserProfile] = useState(null); + + const [activeStep, setActiveStep] = useState(0); + const steps = getSteps(); + + const handleNext = () => { + setActiveStep((prevActiveStep) => prevActiveStep + 1); + }; + + const handleBack = () => { + setActiveStep((prevActiveStep) => prevActiveStep - 1); + }; + + // Because of timing issues, this component will likely get run before the server has applied + // the requested document access resulting in almost a guranteed permission-denied error. So, + // we use this effect to monitor for permission-denied until the change has propagated, at which + // point, we do the actual doc subscription (next useEffect); + useEffect(() => { + async function getData() { + try { + const ref = firestore.doc(`${USERS_COLLECTION}/${user.uid}`); + // Call it once because this will throw the permission exception. + setUserData(await ref.get()); + setUserProfile(await ref.get()); + setUserRef(ref); + } catch (err) { + // We only try reloading if insufficient permissions. + if (err.code !== 'permission-denied') { + throw err; + } + window.setTimeout(() => { + setRetries(retries + 1); + }, 1000); + } + } + getData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [retries]); + + if (user && user.uid) { + defaultValues.email = user.email; + const nameParts = user.displayName?.split(' '); + if (nameParts) { + [defaultValues.firstName] = nameParts; + defaultValues.lastName = nameParts.length > 1 ? nameParts[1] : ''; + } + } + + const { + handleSubmit, + errors, + register, + formState: { isValid, dirty }, + getValues, + setValue, + } = useForm({ + validationSchema: userProfileSchema, + defaultValues, + }); + const [userLocationInfo, setUserLocationInfo] = useState(null); + + /** + * Sets the value for the given field if it isn't already populated. + * @param {string} fieldName name of the field which will be set + * @param {string} allFieldValues current value of all fields + * @param {object} googleResultComponent address component returned by the Google Maps API + * @param {object} addressType type of address component to lookup + * @param {string} addressTypeFormat short_name or long_name + */ + function setValueIfNotAlreadySet( + fieldName, + allFieldValues, + googleResultComponent, + addressType, + addressTypeFormat = 'long_name', + ) { + if ( + googleResultComponent.types.includes(addressType) && + !allFieldValues[fieldName] + ) { + setValue(fieldName, googleResultComponent[addressTypeFormat]); + } + } + + function handleSetUserLocationInfo(locationInfo) { + setUserLocationInfo(locationInfo); + const result = locationInfo.lookedUpAddress; + if (result) { + const values = getValues(); + let streetNumber = ''; + let streetAddress = ''; + result.address_components.forEach((component) => { + setValueIfNotAlreadySet('city', values, component, 'locality'); + setValueIfNotAlreadySet( + 'state', + values, + component, + 'administrative_area_level_1', + 'short_name', + ); + setValueIfNotAlreadySet('zipcode', values, component, 'postal_code'); + if (component.types.includes('street_number')) { + streetNumber = component.short_name; + } + if (component.types.includes('route')) { + streetAddress = component.short_name; + } + }); + if (streetNumber && streetAddress && !values.address1) { + setValue('address1', `${streetNumber} ${streetAddress}`); + } + } + } + + // const handleLocationChange = (location) => { + // setUserLocation(location); + // }; + + async function handleFormSubmit(values) { + const newValues = values; + if (!userLocationInfo) { + alert('Please select a location above.'); // eslint-disable-line no-alert + return; + } + + delete userLocationInfo.lookedUpAddress; + + // Default the displayName if it isn't already set (e.g., when signing up using email). + if (!userData.get('displayName')) { + newValues.displayName = `${values.firstName} ${values.lastName}`; + } + + const userUpdates = { ...newValues, ...userLocationInfo }; + await userRef.set(userUpdates, { merge: true }); + history.replace(SEARCH_PATH); + } + + function renderFields(step) { + switch (step) { + case 0: + return ( + + + + + + ); + case 1: + return ( + + + + + + + + + ); + case 2: + return ( + + + + + + + + + + + + ); + case 3: + return ( + + + + + + ); + default: + return ''; + } + } + + return ( + + + {isLoggedIn(user) ? 'My Profile' : ' I Want To Help'} + + + {isLoggedIn(user) ? 'My Profile' : ' I Want To Help'} + + +
    + + + {steps.map((label) => { + const stepProps = {}; + const labelProps = {}; + return ( + + {label} + + ); + })} + + + + {renderFields(activeStep)} + + Note: This website and all related work products are provided + "AS IS". The provider of this service makes no other + warranties, express or implied, and hereby disclaims all implied + warranties, including any warranty of merchantability and warranty + of fitness for a particular purpose. + + {dirty && errors && !!Object.keys(errors).length && !isValid && ( + + Please fix the errors above. + + )} +
    +
    + {getStepContent(activeStep)} +
    + + {activeStep === steps.length - 1 ? ( + + ) : ( + + )} +
    +
    +
    +
    +
    +
    +
    + ); +} + +export default UserProfile; diff --git a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.styles.js b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.styles.js new file mode 100644 index 0000000..fbc6a89 --- /dev/null +++ b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.styles.js @@ -0,0 +1,27 @@ +export default (theme) => ({ + buttons: { + display: 'flex', + justifyContent: 'flex-end', + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, + optionalDivider: { + marginTop: theme.spacing(4), + marginBottom: theme.spacing(4), + }, + errorText: { + color: 'red', + fontWeight: 'bold', + display: 'flex', + justifyContent: 'flex-end', + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(1), + }, + paper: { + paddingTop: theme.spacing(3), + paddingBottom: theme.spacing(1), + }, + warrantyInfo: { + marginTop: theme.spacing(3), + }, +}); diff --git a/src/routes/UserProfile/components/UserProfilePage/index.js b/src/routes/UserProfile/components/UserProfilePage/index.js new file mode 100644 index 0000000..e4bfdd3 --- /dev/null +++ b/src/routes/UserProfile/components/UserProfilePage/index.js @@ -0,0 +1,3 @@ +import UserProfilePage from './UserProfilePage'; + +export default UserProfilePage; diff --git a/src/routes/UserProfile/index.js b/src/routes/UserProfile/index.js new file mode 100644 index 0000000..26243ee --- /dev/null +++ b/src/routes/UserProfile/index.js @@ -0,0 +1,11 @@ +import { loadable } from 'utils/router'; +import { USER_PROFILE_PATH as path } from 'constants/paths'; + +export default { + path, + component: loadable(() => + import( + /* webpackChunkName: 'User Profile */ './components/UserProfilePage' + ), + ), +}; diff --git a/src/routes/index.js b/src/routes/index.js index 0b93218..36094df 100755 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -13,6 +13,7 @@ import AboutRoute from './About'; import DonateRoute from './Donate'; import AccountRoute from './Account'; import NewUserRoute from './NewUser'; +import UserProfileRoute from './UserProfile'; import SearchRoute from './Search'; import NotFoundRoute from './NotFound'; import PrivacyPolicyRoute from './PrivacyPolicy'; @@ -39,6 +40,7 @@ export default function createRoutes() { ContactRoute, SearchRoute, NewUserRoute, + UserProfileRoute, AboutRoute, LoginRoute, TermsOfServiceRoute, From 4737781efddf4f2060d45061cf0931c89a6f2560 Mon Sep 17 00:00:00 2001 From: Tim Escher Date: Sun, 10 May 2020 08:16:51 -0700 Subject: [PATCH 03/15] Current state of profile stepper testing --- .../UserProfilePage/UserProfilePage.js | 120 ++++++++---------- src/routes/UserProfile/index.js | 2 +- 2 files changed, 53 insertions(+), 69 deletions(-) diff --git a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js index 87cd4a0..b450b8c 100644 --- a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js +++ b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js @@ -5,30 +5,22 @@ import { Paper, Divider, Button, - Card, - CardActions, - CardContent, Container, Grid, makeStyles, } from '@material-ui/core'; -import { - ExitToApp as LogoutIcon, - Person as AccountIcon, -} from '@material-ui/icons'; import { Link, useHistory } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import Stepper from '@material-ui/core/Stepper'; import Step from '@material-ui/core/Step'; import StepLabel from '@material-ui/core/StepLabel'; -import Check from '@material-ui/icons/Check'; import * as Yup from 'yup'; import { useFirestore, useUser } from 'reactfire'; -import { USERS_COLLECTION } from 'constants/collections'; +import { USERS_PRIVILEGED_COLLECTION } from 'constants/collections'; import ClickableMap from 'components/ClickableMap'; import { useForm } from 'react-hook-form'; import { validateEmail } from 'utils/form'; -import { ACCOUNT_PATH, LOGOUT_PATH, SEARCH_PATH } from 'constants/paths'; +import { SEARCH_PATH } from 'constants/paths'; import styles from './UserProfilePage.styles'; const useStyles = makeStyles(styles); @@ -46,26 +38,12 @@ const userProfileSchema = Yup.object().shape({ state: Yup.string(), zipcode: Yup.string(), }); +const userFields = Object.keys(userProfileSchema.describe().fields); function getSteps() { return ['Email', 'Password', 'Contact Info', 'Location']; } -function getStepContent(step) { - switch (step) { - case 0: - return 'Email'; - case 1: - return 'Password'; - case 2: - return 'Contact Info'; - case 3: - return 'Where Are You?'; - default: - return 'Unknown step'; - } -} - function isLoggedIn(user) { return user && user.uid; } @@ -84,19 +62,11 @@ function UserProfile() { const [retries, setRetries] = useState(0); const [userRef, setUserRef] = useState(null); const [userData, setUserData] = useState(null); - const [userProfile, setUserProfile] = useState(null); + const [formValues, setFormValues] = useState(null); const [activeStep, setActiveStep] = useState(0); const steps = getSteps(); - const handleNext = () => { - setActiveStep((prevActiveStep) => prevActiveStep + 1); - }; - - const handleBack = () => { - setActiveStep((prevActiveStep) => prevActiveStep - 1); - }; - // Because of timing issues, this component will likely get run before the server has applied // the requested document access resulting in almost a guranteed permission-denied error. So, // we use this effect to monitor for permission-denied until the change has propagated, at which @@ -104,10 +74,9 @@ function UserProfile() { useEffect(() => { async function getData() { try { - const ref = firestore.doc(`${USERS_COLLECTION}/${user.uid}`); + const ref = firestore.doc(`${USERS_PRIVILEGED_COLLECTION}/${user.uid}`); // Call it once because this will throw the permission exception. setUserData(await ref.get()); - setUserProfile(await ref.get()); setUserRef(ref); } catch (err) { // We only try reloading if insufficient permissions. @@ -120,18 +89,16 @@ function UserProfile() { } } getData(); + console.log('Got data'); + if (userData) { + userFields.forEach(function getDefaults(key) { + defaultValues[key] = userData.get(key); + }); + console.log('Loaded data into defaults', defaultValues); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [retries]); - if (user && user.uid) { - defaultValues.email = user.email; - const nameParts = user.displayName?.split(' '); - if (nameParts) { - [defaultValues.firstName] = nameParts; - defaultValues.lastName = nameParts.length > 1 ? nameParts[1] : ''; - } - } - const { handleSubmit, errors, @@ -143,6 +110,27 @@ function UserProfile() { validationSchema: userProfileSchema, defaultValues, }); + + console.log('Form values', formValues); + if (formValues) { + Object.keys(formValues).forEach(function getDefaults(key) { + defaultValues[key] = formValues[key]; + }); + console.log('Updated data into defaults', defaultValues); + } + + const handleNext = () => { + console.log('Data from this form (Next)', { ...getValues() }); + setFormValues({ ...formValues, ...getValues() }); + setActiveStep(activeStep + 1); + }; + + const handleBack = () => { + console.log('Data from this form (Back)', { ...getValues() }); + setFormValues({ ...formValues, ...getValues() }); + setActiveStep(activeStep - 1); + }; + const [userLocationInfo, setUserLocationInfo] = useState(null); /** @@ -363,7 +351,25 @@ function UserProfile() { - {renderFields(activeStep)} +
    {renderFields(activeStep)}
    +
    + + {activeStep === steps.length - 1 ? ( + + ) : ( + + )} +
    + Note: This website and all related work products are provided "AS IS". The provider of this service makes no other @@ -376,28 +382,6 @@ function UserProfile() { Please fix the errors above. )} -
    -
    - {getStepContent(activeStep)} -
    - - {activeStep === steps.length - 1 ? ( - - ) : ( - - )} -
    -
    -
    diff --git a/src/routes/UserProfile/index.js b/src/routes/UserProfile/index.js index 26243ee..62cb913 100644 --- a/src/routes/UserProfile/index.js +++ b/src/routes/UserProfile/index.js @@ -5,7 +5,7 @@ export default { path, component: loadable(() => import( - /* webpackChunkName: 'User Profile */ './components/UserProfilePage' + /* webpackChunkName: 'User Profile' */ './components/UserProfilePage' ), ), }; From 2d0d4786f10b3be797dda4f94fa0425afe457964 Mon Sep 17 00:00:00 2001 From: Tim Escher Date: Mon, 11 May 2020 12:14:54 -0700 Subject: [PATCH 04/15] More profile loading --- .../UserProfilePage/UserProfilePage.js | 338 +++++++++--------- 1 file changed, 175 insertions(+), 163 deletions(-) diff --git a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js index b450b8c..ee638e2 100644 --- a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js +++ b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js @@ -62,25 +62,50 @@ function UserProfile() { const [retries, setRetries] = useState(0); const [userRef, setUserRef] = useState(null); const [userData, setUserData] = useState(null); - const [formValues, setFormValues] = useState(null); + // const [formValues, setFormValues] = useState(null); const [activeStep, setActiveStep] = useState(0); const steps = getSteps(); + const { + handleSubmit, + errors, + register, + formState: { isValid, dirty }, + getValues, + setValue, + } = useForm({ + validationSchema: userProfileSchema, + defaultValues, + }); // Because of timing issues, this component will likely get run before the server has applied // the requested document access resulting in almost a guranteed permission-denied error. So, // we use this effect to monitor for permission-denied until the change has propagated, at which // point, we do the actual doc subscription (next useEffect); useEffect(() => { + console.log('User', user.uid); + const ref = firestore.doc(`${USERS_PRIVILEGED_COLLECTION}/${user.uid}`); + let data = {}; async function getData() { try { - const ref = firestore.doc(`${USERS_PRIVILEGED_COLLECTION}/${user.uid}`); + console.log('ref', ref); // Call it once because this will throw the permission exception. - setUserData(await ref.get()); + const snap = await ref.get(); + data = snap.data(); + setUserData(data); setUserRef(ref); + console.log('Got data', data, ref); + if (Object.keys(data).length) { + userFields.forEach(function getDefaults(key) { + defaultValues[key] = data[key]; + setValue(key, data[key]); + }); + console.log('Loaded data into defaults', defaultValues); + } } catch (err) { // We only try reloading if insufficient permissions. if (err.code !== 'permission-denied') { + console.log('permission denied'); throw err; } window.setTimeout(() => { @@ -89,45 +114,16 @@ function UserProfile() { } } getData(); - console.log('Got data'); - if (userData) { - userFields.forEach(function getDefaults(key) { - defaultValues[key] = userData.get(key); - }); - console.log('Loaded data into defaults', defaultValues); - } // eslint-disable-next-line react-hooks/exhaustive-deps }, [retries]); - const { - handleSubmit, - errors, - register, - formState: { isValid, dirty }, - getValues, - setValue, - } = useForm({ - validationSchema: userProfileSchema, - defaultValues, - }); - - console.log('Form values', formValues); - if (formValues) { - Object.keys(formValues).forEach(function getDefaults(key) { - defaultValues[key] = formValues[key]; - }); - console.log('Updated data into defaults', defaultValues); - } - const handleNext = () => { console.log('Data from this form (Next)', { ...getValues() }); - setFormValues({ ...formValues, ...getValues() }); setActiveStep(activeStep + 1); }; const handleBack = () => { console.log('Data from this form (Back)', { ...getValues() }); - setFormValues({ ...formValues, ...getValues() }); setActiveStep(activeStep - 1); }; @@ -192,139 +188,147 @@ function UserProfile() { async function handleFormSubmit(values) { const newValues = values; - if (!userLocationInfo) { - alert('Please select a location above.'); // eslint-disable-line no-alert - return; - } + console.log('In submit', userData, userRef); + // if (!userLocationInfo) { + // alert('Please select a location above.'); // eslint-disable-line no-alert + // return; + // } - delete userLocationInfo.lookedUpAddress; + // delete userLocationInfo.lookedUpAddress; // Default the displayName if it isn't already set (e.g., when signing up using email). - if (!userData.get('displayName')) { + if (!userData.displayName) { newValues.displayName = `${values.firstName} ${values.lastName}`; } - const userUpdates = { ...newValues, ...userLocationInfo }; + // const userUpdates = { ...newValues, ...userLocationInfo }; + const userUpdates = { ...newValues }; + console.log('Updating with', userUpdates); await userRef.set(userUpdates, { merge: true }); + console.log('Updated'); history.replace(SEARCH_PATH); } function renderFields(step) { - switch (step) { - case 0: - return ( - - - - + return ( +
    + + + - ); - case 1: - return ( - - - - - - - + + + + - ); - case 2: - return ( - - - - - - - - - - + + - ); - case 3: - return ( - - - - + + + + - ); - default: - return ''; - } + + + + + + + + + + + + +
    + ); } return ( @@ -351,23 +355,31 @@ function UserProfile() { -
    {renderFields(activeStep)}
    + {renderFields(activeStep)}
    - {activeStep === steps.length - 1 ? ( - - ) : ( - - )} + +
    From e94caec9b6e95879e14ec18a1239ebdb2dfa8f73 Mon Sep 17 00:00:00 2001 From: Tim Escher Date: Mon, 11 May 2020 19:03:38 -0700 Subject: [PATCH 05/15] Intermediate save --- src/components/GoogleSignIn/GoogleSignIn.js | 53 +++++++++++++ .../GoogleSignIn/GoogleSignIn.styles.js | 17 ++++ src/components/GoogleSignIn/index.js | 3 + src/containers/Navbar/AccountMenu.js | 66 ++++++++++++++++ .../UserProfilePage/UserProfilePage.js | 79 +++++++++++-------- 5 files changed, 185 insertions(+), 33 deletions(-) create mode 100644 src/components/GoogleSignIn/GoogleSignIn.js create mode 100644 src/components/GoogleSignIn/GoogleSignIn.styles.js create mode 100644 src/components/GoogleSignIn/index.js create mode 100644 src/containers/Navbar/AccountMenu.js diff --git a/src/components/GoogleSignIn/GoogleSignIn.js b/src/components/GoogleSignIn/GoogleSignIn.js new file mode 100644 index 0000000..f737f07 --- /dev/null +++ b/src/components/GoogleSignIn/GoogleSignIn.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import GoogleButton from 'react-google-button'; +import { makeStyles } from '@material-ui/core/styles'; +import useNotifications from 'modules/notification/useNotifications'; +import firebase from 'firebase/app'; // imported for auth provider +import { useAuth } from 'reactfire'; +import styles from './GoogleSignIn.styles'; + +const useStyles = makeStyles(styles); + +function GoogleSignIn({ label, handleClick }) { + const classes = useStyles(); + const auth = useAuth(); + const { showError } = useNotifications(); + + async function googleLogin(handleSignIn) { + const provider = new firebase.auth.GoogleAuthProvider(); + const isMobile = /iPhone|iPad|iPod|Android/i.test( + window.navigator.userAgent, + ); + const signInMethod = isMobile ? 'signInWithRedirect' : 'signInWithPopup'; + try { + const authState = await auth[signInMethod](provider); + // Write user profile if it doesn't exist, otherwise redirect to search page + await handleSignIn(authState); + } catch (err) { + showError(err.message); + } + } + + return ( + <> +
    or
    +
    + { + googleLogin(handleClick); + }} + data-test="google-auth-button" + /> +
    + + ); +} + +GoogleSignIn.propTypes = { + label: PropTypes.string, + handleClick: PropTypes.func, +}; + +export default GoogleSignIn; diff --git a/src/components/GoogleSignIn/GoogleSignIn.styles.js b/src/components/GoogleSignIn/GoogleSignIn.styles.js new file mode 100644 index 0000000..4e0412c --- /dev/null +++ b/src/components/GoogleSignIn/GoogleSignIn.styles.js @@ -0,0 +1,17 @@ +export default () => ({ + root: { + display: 'flex', + alignItems: 'center', + flexDirection: 'column', + justifyContent: 'flex-start', + paddingTop: '7rem', + height: '100%', + }, + orLabel: { + marginTop: '1rem', + marginBottom: '.5rem', + }, + providers: { + marginTop: '1rem', + }, +}); diff --git a/src/components/GoogleSignIn/index.js b/src/components/GoogleSignIn/index.js new file mode 100644 index 0000000..0cb38b0 --- /dev/null +++ b/src/components/GoogleSignIn/index.js @@ -0,0 +1,3 @@ +import GoogleSignIn from './GoogleSignIn'; + +export default GoogleSignIn; diff --git a/src/containers/Navbar/AccountMenu.js b/src/containers/Navbar/AccountMenu.js new file mode 100644 index 0000000..1f5d44a --- /dev/null +++ b/src/containers/Navbar/AccountMenu.js @@ -0,0 +1,66 @@ +import React, { useState } from 'react'; +import { useFirebaseApp } from 'reactfire'; +import { Link, useHistory } from 'react-router-dom'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import IconButton from '@material-ui/core/IconButton'; +import AccountCircle from '@material-ui/icons/AccountCircle'; +import { makeStyles } from '@material-ui/core/styles'; +import { ACCOUNT_PATH, MY_REQUESTS_PATH } from 'constants/paths'; + +const useStyles = makeStyles(() => ({ + buttonRoot: { + color: 'white', + }, +})); + +function AccountMenu() { + const classes = useStyles(); + const [anchorEl, setMenu] = useState(null); + const history = useHistory(); + const firebase = useFirebaseApp(); + + function closeAccountMenu() { + setMenu(null); + } + function handleMenu(e) { + setMenu(e.target); + } + async function handleLogout() { + closeAccountMenu(); + await firebase.auth().signOut(); + history.replace('/'); + } + + return ( + <> + + + + + + My Requests + + + Account + + Sign Out + + + ); +} + +export default AccountMenu; diff --git a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js index ee638e2..050689d 100644 --- a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js +++ b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js @@ -3,7 +3,6 @@ import { Typography, TextField, Paper, - Divider, Button, Container, Grid, @@ -18,6 +17,7 @@ import * as Yup from 'yup'; import { useFirestore, useUser } from 'reactfire'; import { USERS_PRIVILEGED_COLLECTION } from 'constants/collections'; import ClickableMap from 'components/ClickableMap'; +import GoogleSignIn from 'components/GoogleSignIn'; import { useForm } from 'react-hook-form'; import { validateEmail } from 'utils/form'; import { SEARCH_PATH } from 'constants/paths'; @@ -44,8 +44,12 @@ function getSteps() { return ['Email', 'Password', 'Contact Info', 'Location']; } +function hasAccount(userData) { + return userData && !!userData.email; +} + function isLoggedIn(user) { - return user && user.uid; + return user && !!user.uid; } function isGoogleLoggedIn(user) { @@ -57,7 +61,7 @@ function UserProfile() { const firestore = useFirestore(); const history = useHistory(); const user = useUser(); - const defaultValues = {}; + // const defaultValues = {}; const [retries, setRetries] = useState(0); const [userRef, setUserRef] = useState(null); @@ -75,7 +79,6 @@ function UserProfile() { setValue, } = useForm({ validationSchema: userProfileSchema, - defaultValues, }); // Because of timing issues, this component will likely get run before the server has applied @@ -83,34 +86,36 @@ function UserProfile() { // we use this effect to monitor for permission-denied until the change has propagated, at which // point, we do the actual doc subscription (next useEffect); useEffect(() => { - console.log('User', user.uid); - const ref = firestore.doc(`${USERS_PRIVILEGED_COLLECTION}/${user.uid}`); - let data = {}; async function getData() { - try { - console.log('ref', ref); - // Call it once because this will throw the permission exception. - const snap = await ref.get(); - data = snap.data(); - setUserData(data); - setUserRef(ref); - console.log('Got data', data, ref); - if (Object.keys(data).length) { - userFields.forEach(function getDefaults(key) { - defaultValues[key] = data[key]; - setValue(key, data[key]); - }); - console.log('Loaded data into defaults', defaultValues); - } - } catch (err) { - // We only try reloading if insufficient permissions. - if (err.code !== 'permission-denied') { - console.log('permission denied'); - throw err; + if (isLoggedIn(user)) { + // Already authenticated. See if account data exists + console.log('User', user.uid); + const ref = firestore.doc(`${USERS_PRIVILEGED_COLLECTION}/${user.uid}`); + let data = {}; + try { + console.log('ref', ref); + // Call it once because this will throw the permission exception. + const snap = await ref.get(); + data = snap.data(); + setUserData(data); + setUserRef(ref); + console.log('Got data', data, ref); + if (Object.keys(data).length) { + userFields.forEach(function getDefaults(key) { + setValue(key, data[key]); + }); + console.log('Loaded data into defaults'); + } + } catch (err) { + // We only try reloading if insufficient permissions. + if (err.code !== 'permission-denied') { + console.log('permission denied'); + throw err; + } + window.setTimeout(() => { + setRetries(retries + 1); + }, 1000); } - window.setTimeout(() => { - setRetries(retries + 1); - }, 1000); } } getData(); @@ -152,6 +157,8 @@ function UserProfile() { } } + function handleGoogleSignIn() {} + function handleSetUserLocationInfo(locationInfo) { setUserLocationInfo(locationInfo); const result = locationInfo.lookedUpAddress; @@ -216,7 +223,7 @@ function UserProfile() { style={{ display: step === 0 ? 'block' : 'none' }} container spacing={1} - direction="row"> + direction="column"> + {!hasAccount(userData) ? ( + + ) : null}
    - {isLoggedIn(user) ? 'My Profile' : ' I Want To Help'} + {hasAccount(userData) ? 'My Profile' : ' I Want To Help'} - {isLoggedIn(user) ? 'My Profile' : ' I Want To Help'} + {hasAccount(userData) ? 'My Profile' : ' I Want To Help'}
    From 2af2a7a432e214c510bc6e1df16f5b2131e57268 Mon Sep 17 00:00:00 2001 From: Tim Escher Date: Tue, 12 May 2020 14:24:15 -0700 Subject: [PATCH 06/15] Finished email/pw new user --- src/components/GoogleSignIn/GoogleSignIn.js | 7 +- .../UserProfilePage/UserProfilePage.js | 92 +++++++++++++------ 2 files changed, 65 insertions(+), 34 deletions(-) diff --git a/src/components/GoogleSignIn/GoogleSignIn.js b/src/components/GoogleSignIn/GoogleSignIn.js index f737f07..f4aec41 100644 --- a/src/components/GoogleSignIn/GoogleSignIn.js +++ b/src/components/GoogleSignIn/GoogleSignIn.js @@ -23,9 +23,10 @@ function GoogleSignIn({ label, handleClick }) { try { const authState = await auth[signInMethod](provider); // Write user profile if it doesn't exist, otherwise redirect to search page - await handleSignIn(authState); + handleSignIn(authState); } catch (err) { showError(err.message); + throw err; } } @@ -35,9 +36,7 @@ function GoogleSignIn({ label, handleClick }) {
    { - googleLogin(handleClick); - }} + onClick={() => {googleLogin(handleClick)}} data-test="google-auth-button" />
    diff --git a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js index 050689d..48ef49d 100644 --- a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js +++ b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { Typography, TextField, @@ -14,12 +14,15 @@ import Stepper from '@material-ui/core/Stepper'; import Step from '@material-ui/core/Step'; import StepLabel from '@material-ui/core/StepLabel'; import * as Yup from 'yup'; -import { useFirestore, useUser } from 'reactfire'; -import { USERS_PRIVILEGED_COLLECTION } from 'constants/collections'; +import { useAuth, useFirestore, useUser } from 'reactfire'; +import useNotifications from 'modules/notification/useNotifications'; +import { + USERS_COLLECTION, + USERS_PRIVILEGED_COLLECTION, +} from 'constants/collections'; import ClickableMap from 'components/ClickableMap'; import GoogleSignIn from 'components/GoogleSignIn'; import { useForm } from 'react-hook-form'; -import { validateEmail } from 'utils/form'; import { SEARCH_PATH } from 'constants/paths'; import styles from './UserProfilePage.styles'; @@ -32,6 +35,11 @@ const userProfileSchema = Yup.object().shape({ .email('Please enter a valid email address') .required('Required'), phone: Yup.string().required('Required'), + password: Yup.string().required('Password is required'), + confirmPassword: Yup.string().oneOf( + [Yup.ref('password'), null], + 'Passwords must match', + ), address1: Yup.string(), address2: Yup.string(), city: Yup.string(), @@ -45,22 +53,23 @@ function getSteps() { } function hasAccount(userData) { - return userData && !!userData.email; + return !!userData && !!userData.email; } function isLoggedIn(user) { - return user && !!user.uid; + return !!user && !!user.uid; } function isGoogleLoggedIn(user) { - return user && user.providerId === 'google'; + return !!user && user.providerId === 'google'; } function UserProfile() { const classes = useStyles(); const firestore = useFirestore(); - const history = useHistory(); const user = useUser(); + const auth = useAuth(); + const { showError } = useNotifications(); // const defaultValues = {}; const [retries, setRetries] = useState(0); @@ -86,9 +95,10 @@ function UserProfile() { // we use this effect to monitor for permission-denied until the change has propagated, at which // point, we do the actual doc subscription (next useEffect); useEffect(() => { + console.log(userData, user); async function getData() { - if (isLoggedIn(user)) { - // Already authenticated. See if account data exists + if (isLoggedIn(user) && userData == null) { + // Already authenticated and haven't loaded data previously console.log('User', user.uid); const ref = firestore.doc(`${USERS_PRIVILEGED_COLLECTION}/${user.uid}`); let data = {}; @@ -100,7 +110,7 @@ function UserProfile() { setUserData(data); setUserRef(ref); console.log('Got data', data, ref); - if (Object.keys(data).length) { + if (!!data && Object.keys(data).length) { userFields.forEach(function getDefaults(key) { setValue(key, data[key]); }); @@ -122,6 +132,8 @@ function UserProfile() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [retries]); + // Do step-dependent + const handleNext = () => { console.log('Data from this form (Next)', { ...getValues() }); setActiveStep(activeStep + 1); @@ -157,7 +169,9 @@ function UserProfile() { } } - function handleGoogleSignIn() {} + const handleGoogleSignIn = useCallback((authState) => { + console.log(authState); + }, []); function handleSetUserLocationInfo(locationInfo) { setUserLocationInfo(locationInfo); @@ -195,25 +209,41 @@ function UserProfile() { async function handleFormSubmit(values) { const newValues = values; - console.log('In submit', userData, userRef); - // if (!userLocationInfo) { - // alert('Please select a location above.'); // eslint-disable-line no-alert - // return; - // } - - // delete userLocationInfo.lookedUpAddress; + console.log('In submit', values, userData, userRef); - // Default the displayName if it isn't already set (e.g., when signing up using email). - if (!userData.displayName) { - newValues.displayName = `${values.firstName} ${values.lastName}`; + // New user created with email/password + if (!isLoggedIn(user)) { + try { + // Try creating new user + const authState = await auth.createUserWithEmailAndPassword( + getValues('email'), + getValues('password'), + ); + // Write USERS profile + const userSnap = await firestore + .doc(`${USERS_COLLECTION}/${authState.user.uid}`) + .get(); + newValues.displayName = `${values.firstName} ${values.lastName}`; + let newProfile = { + firstName: newValues.firstName, + lastName: newValues.lastName, + email: newValues.email, + displayName: newValues.displayName, + }; + await userSnap.ref.set(newProfile, { merge: true }); + // Write the USERS_PRIVILEGED profile + newProfile = { ...newValues }; + const userPrivSnap = await firestore + .doc(`${USERS_PRIVILEGED_COLLECTION}/${authState.user.uid}`) + .get(); + console.log('Updating with', newProfile); + await userPrivSnap.ref.set(newProfile, { merge: true }); + console.log('Updated'); + } catch (err) { + showError(err.message); + setActiveStep(0); + } } - - // const userUpdates = { ...newValues, ...userLocationInfo }; - const userUpdates = { ...newValues }; - console.log('Updating with', userUpdates); - await userRef.set(userUpdates, { merge: true }); - console.log('Updated'); - history.replace(SEARCH_PATH); } function renderFields(step) { @@ -240,7 +270,9 @@ function UserProfile() { {!hasAccount(userData) ? ( { + handleGoogleSignIn(authState); + }} /> ) : null} From e3cfc36a404428893c225512b9c78d34f0f926ba Mon Sep 17 00:00:00 2001 From: Tim Escher Date: Tue, 12 May 2020 19:29:58 -0700 Subject: [PATCH 07/15] Add Google login --- src/components/GoogleSignIn/GoogleSignIn.js | 19 ++- .../GoogleSignIn/GoogleSignIn.styles.js | 14 +- .../UserProfilePage/UserProfilePage.js | 132 +++++++++++------- .../UserProfilePage/UserProfilePage.styles.js | 14 ++ 4 files changed, 105 insertions(+), 74 deletions(-) diff --git a/src/components/GoogleSignIn/GoogleSignIn.js b/src/components/GoogleSignIn/GoogleSignIn.js index f4aec41..090f5cd 100644 --- a/src/components/GoogleSignIn/GoogleSignIn.js +++ b/src/components/GoogleSignIn/GoogleSignIn.js @@ -31,16 +31,15 @@ function GoogleSignIn({ label, handleClick }) { } return ( - <> -
    or
    -
    - {googleLogin(handleClick)}} - data-test="google-auth-button" - /> -
    - +
    + { + googleLogin(handleClick); + }} + data-test="google-auth-button" + /> +
    ); } diff --git a/src/components/GoogleSignIn/GoogleSignIn.styles.js b/src/components/GoogleSignIn/GoogleSignIn.styles.js index 4e0412c..473bbb8 100644 --- a/src/components/GoogleSignIn/GoogleSignIn.styles.js +++ b/src/components/GoogleSignIn/GoogleSignIn.styles.js @@ -1,17 +1,7 @@ export default () => ({ - root: { + providers: { display: 'flex', alignItems: 'center', - flexDirection: 'column', - justifyContent: 'flex-start', - paddingTop: '7rem', - height: '100%', - }, - orLabel: { - marginTop: '1rem', - marginBottom: '.5rem', - }, - providers: { - marginTop: '1rem', + justifyContent: 'center', }, }); diff --git a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js index 48ef49d..8c6b425 100644 --- a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js +++ b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js @@ -28,6 +28,10 @@ import styles from './UserProfilePage.styles'; const useStyles = makeStyles(styles); +function isGoogleLoggedIn(user) { + return !!user && user.providerData[0].providerId === 'google.com'; +} + const userProfileSchema = Yup.object().shape({ firstName: Yup.string().min(2, 'Too Short').required('Required'), lastName: Yup.string().min(2, 'Too Short').required('Required'), @@ -35,7 +39,7 @@ const userProfileSchema = Yup.object().shape({ .email('Please enter a valid email address') .required('Required'), phone: Yup.string().required('Required'), - password: Yup.string().required('Password is required'), + password: Yup.string(), confirmPassword: Yup.string().oneOf( [Yup.ref('password'), null], 'Passwords must match', @@ -60,16 +64,12 @@ function isLoggedIn(user) { return !!user && !!user.uid; } -function isGoogleLoggedIn(user) { - return !!user && user.providerId === 'google'; -} - function UserProfile() { const classes = useStyles(); const firestore = useFirestore(); const user = useUser(); const auth = useAuth(); - const { showError } = useNotifications(); + const { showError, showMessage } = useNotifications(); // const defaultValues = {}; const [retries, setRetries] = useState(0); @@ -77,7 +77,7 @@ function UserProfile() { const [userData, setUserData] = useState(null); // const [formValues, setFormValues] = useState(null); - const [activeStep, setActiveStep] = useState(0); + const [activeStep, setActiveStep] = useState(isGoogleLoggedIn(user) ? 2 : 0); const steps = getSteps(); const { handleSubmit, @@ -114,6 +114,8 @@ function UserProfile() { userFields.forEach(function getDefaults(key) { setValue(key, data[key]); }); + if (retries > 999) + showMessage('Looks like you already have an account.'); // HACK: retries > 999 means we hit the Sign Up with Google button console.log('Loaded data into defaults'); } } catch (err) { @@ -170,7 +172,9 @@ function UserProfile() { } const handleGoogleSignIn = useCallback((authState) => { - console.log(authState); + setActiveStep(2); + setRetries(1000); // Force a re-run of the data load + console.log('Google handler', authState); }, []); function handleSetUserLocationInfo(locationInfo) { @@ -209,6 +213,8 @@ function UserProfile() { async function handleFormSubmit(values) { const newValues = values; + let uid = null; + let newUser = false; console.log('In submit', values, userData, userRef); // New user created with email/password @@ -219,41 +225,51 @@ function UserProfile() { getValues('email'), getValues('password'), ); - // Write USERS profile - const userSnap = await firestore - .doc(`${USERS_COLLECTION}/${authState.user.uid}`) - .get(); - newValues.displayName = `${values.firstName} ${values.lastName}`; - let newProfile = { - firstName: newValues.firstName, - lastName: newValues.lastName, - email: newValues.email, - displayName: newValues.displayName, - }; - await userSnap.ref.set(newProfile, { merge: true }); - // Write the USERS_PRIVILEGED profile - newProfile = { ...newValues }; - const userPrivSnap = await firestore - .doc(`${USERS_PRIVILEGED_COLLECTION}/${authState.user.uid}`) - .get(); - console.log('Updating with', newProfile); - await userPrivSnap.ref.set(newProfile, { merge: true }); - console.log('Updated'); + newUser = true; + uid = authState.user.uid; } catch (err) { showError(err.message); setActiveStep(0); + return; } + } else { + uid = user.uid; + } + try { + // Write USERS profile + const userSnap = await firestore.doc(`${USERS_COLLECTION}/${uid}`).get(); + newValues.displayName = `${values.firstName} ${values.lastName}`; + let newProfile = { + firstName: newValues.firstName, + lastName: newValues.lastName, + email: newValues.email, + displayName: newValues.displayName, + }; + await userSnap.ref.set(newProfile, { merge: true }); + // Write USERS_PRIVILEGED profile + newProfile = { ...newValues }; + const userPrivSnap = await firestore + .doc(`${USERS_PRIVILEGED_COLLECTION}/${uid}`) + .get(); + console.log('Updating with', newProfile); + await userPrivSnap.ref.set(newProfile, { merge: true }); + console.log('Updated'); + showMessage(newUser ? 'Account created' : 'Profile updated'); + } catch (err) { + showError(err.message); + setActiveStep(0); + // TODO: If a new user, remove their Firebase account if profile didn't save? } } function renderFields(step) { return ( -
    +
    + justify="center" + spacing={1}> {!hasAccount(userData) ? ( - { - handleGoogleSignIn(authState); - }} - /> + + { + handleGoogleSignIn(authState); + }} + /> + ) : null} + justify="center" + spacing={1}> + justify="center" + spacing={1}> + justify="center" + spacing={1}> - + {steps.map((label) => { @@ -401,8 +424,13 @@ function UserProfile() { {renderFields(activeStep)} -
    - - - -
    +
    + +
    +
    + + + {hasAccount(userData) ? 'My Profile' : ' I Want To Help'} + + + + {hasAccount(userData) ? 'My Profile' : ' I Want To Help'} + + + + + + {steps.map((label) => { + const stepProps = {}; + const labelProps = {}; + return ( + + {label} + + ); + })} + + + + {renderFields(activeStep)} +
    + + + +
    - - Note: This website and all related work products are provided - "AS IS". The provider of this service makes no other - warranties, express or implied, and hereby disclaims all implied - warranties, including any warranty of merchantability and warranty - of fitness for a particular purpose. - - {dirty && errors && !!Object.keys(errors).length && !isValid && ( - - Please fix the errors above. + + Note: This website and all related work products are provided + "AS IS". The provider of this service makes no other + warranties, express or implied, and hereby disclaims all implied + warranties, including any warranty of merchantability and + warranty of fitness for a particular purpose. - )} -
    - -
    + {dirty && errors && !!Object.keys(errors).length && !isValid && ( + + Please fix the errors above. + + )} + + + +
    ); } From d2ca6236a44e1bc50eebc008cf2bcfce5babdd77 Mon Sep 17 00:00:00 2001 From: Tim Escher Date: Wed, 13 May 2020 12:36:24 -0700 Subject: [PATCH 09/15] Added map back in --- src/components/ClickableMap/ClickableMap.js | 2 +- .../UserProfilePage/UserProfilePage.js | 116 ++++++++++++++---- 2 files changed, 92 insertions(+), 26 deletions(-) diff --git a/src/components/ClickableMap/ClickableMap.js b/src/components/ClickableMap/ClickableMap.js index c545b93..89631c5 100644 --- a/src/components/ClickableMap/ClickableMap.js +++ b/src/components/ClickableMap/ClickableMap.js @@ -281,7 +281,7 @@ function ClickableMap({ onLocationChange, locationInfo }) { data-test="address-entry" {...getInputProps({ ...params, - placeholder: 'Enter Address', + placeholder: 'Address Search', })} InputProps={{ ...params.InputProps, diff --git a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js index 1985fdf..271fe0f 100644 --- a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js +++ b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js @@ -3,6 +3,8 @@ import { Typography, TextField, Paper, + Divider, + Card, Button, Container, Grid, @@ -135,8 +137,6 @@ function UserProfile() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [retries]); - // Do step-dependent - const handleNext = () => { console.log('Data from this form (Next)', { ...getValues() }); setActiveStep(activeStep + 1); @@ -157,17 +157,13 @@ function UserProfile() { * @param {object} addressType type of address component to lookup * @param {string} addressTypeFormat short_name or long_name */ - function setValueIfNotAlreadySet( + function setAddress( fieldName, - allFieldValues, googleResultComponent, addressType, addressTypeFormat = 'long_name', ) { - if ( - googleResultComponent.types.includes(addressType) && - !allFieldValues[fieldName] - ) { + if (googleResultComponent.types.includes(addressType)) { setValue(fieldName, googleResultComponent[addressTypeFormat]); } } @@ -182,19 +178,18 @@ function UserProfile() { setUserLocationInfo(locationInfo); const result = locationInfo.lookedUpAddress; if (result) { - const values = getValues(); let streetNumber = ''; let streetAddress = ''; result.address_components.forEach((component) => { - setValueIfNotAlreadySet('city', values, component, 'locality'); - setValueIfNotAlreadySet( + setAddress('city', component, 'locality'); + setAddress( 'state', - values, + component, 'administrative_area_level_1', 'short_name', ); - setValueIfNotAlreadySet('zipcode', values, component, 'postal_code'); + setAddress('zipcode', component, 'postal_code'); if (component.types.includes('street_number')) { streetNumber = component.short_name; } @@ -202,7 +197,7 @@ function UserProfile() { streetAddress = component.short_name; } }); - if (streetNumber && streetAddress && !values.address1) { + if (streetNumber && streetAddress) { setValue('address1', `${streetNumber} ${streetAddress}`); } } @@ -380,18 +375,89 @@ function UserProfile() { container justify="center" spacing={1}> - - + Your Location + + + A rough location is needed to allow us to efficiently and quickly + find a match. Enter address in the address search, click the + "Detect Location" button, or click on the map. You can + also enter your full address in the fields at the bottom.{' '} + + + + + + + + + + + + + + + + + + + + + +
    From a6af1c7c2c03dfb8098a406e5a92c88f3654f49e Mon Sep 17 00:00:00 2001 From: Tim Escher Date: Wed, 13 May 2020 15:40:49 -0700 Subject: [PATCH 10/15] Hook up login screen --- db.json-schema.json | 4 + firestore.rules | 2 +- src/containers/Navbar/AccountMenu.js | 6 +- .../Login/components/LoginForm/LoginForm.js | 11 ++- .../Login/components/LoginPage/LoginPage.js | 73 +++++++------------ .../UserProfilePage/UserProfilePage.js | 20 ++++- 6 files changed, 57 insertions(+), 59 deletions(-) diff --git a/db.json-schema.json b/db.json-schema.json index 169e83c..80fc6f2 100644 --- a/db.json-schema.json +++ b/db.json-schema.json @@ -214,6 +214,10 @@ "generalLocationName": { "type": "string", "description": "A name for the location, generally 'City, State'." + }, + "hasAccount": { + "type": "boolean", + "description": "True if this user has set up a login account and profile" } }, "required": ["generalLocation", "generalLocationName"] diff --git a/firestore.rules b/firestore.rules index 915e8ed..2825aa4 100644 --- a/firestore.rules +++ b/firestore.rules @@ -43,7 +43,7 @@ service cloud.firestore { // Public user profiles match /users_public/{userId} { allow read; - allow write: if false; // only written to by indexUser cloud function + allow write: if isThisUser(userId); } // Private request data diff --git a/src/containers/Navbar/AccountMenu.js b/src/containers/Navbar/AccountMenu.js index 1f5d44a..60edf8f 100644 --- a/src/containers/Navbar/AccountMenu.js +++ b/src/containers/Navbar/AccountMenu.js @@ -6,7 +6,7 @@ import MenuItem from '@material-ui/core/MenuItem'; import IconButton from '@material-ui/core/IconButton'; import AccountCircle from '@material-ui/icons/AccountCircle'; import { makeStyles } from '@material-ui/core/styles'; -import { ACCOUNT_PATH, MY_REQUESTS_PATH } from 'constants/paths'; +import { USER_PROFILE_PATH, MY_REQUESTS_PATH } from 'constants/paths'; const useStyles = makeStyles(() => ({ buttonRoot: { @@ -54,8 +54,8 @@ function AccountMenu() { onClick={closeAccountMenu}> My Requests - - Account + + Profile Sign Out diff --git a/src/routes/Login/components/LoginForm/LoginForm.js b/src/routes/Login/components/LoginForm/LoginForm.js index 05b2da4..b312cc4 100644 --- a/src/routes/Login/components/LoginForm/LoginForm.js +++ b/src/routes/Login/components/LoginForm/LoginForm.js @@ -1,10 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useForm } from 'react-hook-form'; -import TextField from '@material-ui/core/TextField'; -import { makeStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; +import { TextField, makeStyles, Button } from '@material-ui/core'; +import { Link } from 'react-router-dom'; import { validateEmail } from 'utils/form'; +import { USER_PROFILE_PATH } from 'constants/paths'; import styles from './LoginForm.styles'; const useStyles = makeStyles(styles); @@ -50,6 +50,10 @@ function LoginForm({ onSubmit }) { error={!!errors.password} helperText={errors.password && 'Password is required'} /> + Forgot Password{' '} + { + // TODO: Need Forgot Password flow + }
    + I'm new, sign me up!{' '} ); } diff --git a/src/routes/Login/components/LoginPage/LoginPage.js b/src/routes/Login/components/LoginPage/LoginPage.js index 562a900..68d79ae 100644 --- a/src/routes/Login/components/LoginPage/LoginPage.js +++ b/src/routes/Login/components/LoginPage/LoginPage.js @@ -5,8 +5,8 @@ import { useAuth, useFirestore } from 'reactfire'; import Paper from '@material-ui/core/Paper'; import { makeStyles } from '@material-ui/core/styles'; import GoogleButton from 'react-google-button'; -import { NEW_USER_PATH, SEARCH_PATH } from 'constants/paths'; -import { USERS_COLLECTION } from 'constants/collections'; +import { USER_PROFILE_PATH, SEARCH_PATH } from 'constants/paths'; +import { USERS_PUBLIC_COLLECTION } from 'constants/collections'; import useNotifications from 'modules/notification/useNotifications'; import LoadingSpinner from 'components/LoadingSpinner'; import LoginForm from '../LoginForm'; @@ -20,33 +20,18 @@ function LoginPage() { const history = useHistory(); const firestore = useFirestore(); const [isLoading, setLoadingState] = useState(false); - const { showError } = useNotifications(); + const { showError, showMessage } = useNotifications(); - async function updateUserAndRedirect(authState) { - try { - // Write user profile if it doesn't exist, otherwise redirect to search page - const userSnap = await firestore - .doc(`${USERS_COLLECTION}/${authState.user.uid}`) - .get(); - // Redirect to search page if user exists - if (userSnap.get('preciseLocation')) { - history.replace(SEARCH_PATH); - } else { - // Write user object then redirect to new user page - const { email, displayName, photoURL, providerData } = authState.user; - const newProfile = { email, displayName, photoURL }; - if (providerData && providerData.length) { - newProfile.providerData = [{ ...providerData[0] }]; - } - await userSnap.ref.set(newProfile, { merge: true }); - window.setTimeout(() => { - history.replace(NEW_USER_PATH); - }, 1000); - } - } catch (err) { - setLoadingState(false); - showError(err.message); + async function hasAccount(user) { + const userSnap = await firestore + .doc(`${USERS_PUBLIC_COLLECTION}/${user.uid}`) + .get(); + const data = await userSnap.data(); + console.log("Checking account", data); + if (!!data && !!data.d && !!data.d.hasAccount) { + return true; } + return false; } async function googleLogin() { @@ -58,8 +43,12 @@ function LoginPage() { const signInMethod = isMobile ? 'signInWithRedirect' : 'signInWithPopup'; try { const authState = await auth[signInMethod](provider); - // Write user profile if it doesn't exist, otherwise redirect to search page - await updateUserAndRedirect(authState); + if (await hasAccount(authState.user)) { + history.replace(SEARCH_PATH); + return; + } + showMessage('You need to finish filling out your profile.'); + history.replace(USER_PROFILE_PATH); } catch (err) { setLoadingState(false); showError(err.message); @@ -72,26 +61,14 @@ function LoginPage() { creds.email, creds.password, ); - // Write user profile if it doesn't exist, otherwise redirect to search page - await updateUserAndRedirect(authState); - } catch (err) { - try { - // Create user if they do not exist - if (err.code === 'auth/user-not-found') { - const authState = await auth.createUserWithEmailAndPassword( - creds.email, - creds.password, - ); - // Write user profile if it doesn't exist, otherwise redirect to search page - await updateUserAndRedirect(authState); - } - } catch (err2) { - if (err2.message === 'auth/user-exists') { - showError(err.message); - } else { - showError(err2.message); - } + if (await hasAccount(authState.user)) { + history.replace(SEARCH_PATH); + return; } + showMessage('You need to finish filling out your profile.'); + history.replace(USER_PROFILE_PATH); + } catch (err) { + showError(err.message); } } diff --git a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js index 271fe0f..ed3f9a5 100644 --- a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js +++ b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js @@ -3,7 +3,6 @@ import { Typography, TextField, Paper, - Divider, Card, Button, Container, @@ -21,6 +20,7 @@ import useNotifications from 'modules/notification/useNotifications'; import { USERS_COLLECTION, USERS_PRIVILEGED_COLLECTION, + USERS_PUBLIC_COLLECTION, } from 'constants/collections'; import ClickableMap from 'components/ClickableMap'; import LoadingSpinner from 'components/LoadingSpinner'; @@ -38,9 +38,7 @@ function isGoogleLoggedIn(user) { const userProfileSchema = Yup.object().shape({ firstName: Yup.string().min(2, 'Too Short').required('Required'), lastName: Yup.string().min(2, 'Too Short').required('Required'), - email: Yup.string() - .email('Please enter a valid email address') - .required('Required'), + email: Yup.string(), phone: Yup.string().required('Required'), password: Yup.string(), confirmPassword: Yup.string().oneOf( @@ -72,6 +70,7 @@ function UserProfile() { const firestore = useFirestore(); const user = useUser(); const auth = useAuth(); + const history = useHistory(); const { showError, showMessage } = useNotifications(); const [isLoading, setLoadingState] = useState(true); @@ -232,6 +231,11 @@ function UserProfile() { uid = user.uid; } try { + // Get email if we came in through Google. Wouldn't be in the form + if (isGoogleLoggedIn(user)) { + console.log(user, user.email); + newValues.email = user.email; + } // Write USERS profile const userSnap = await firestore.doc(`${USERS_COLLECTION}/${uid}`).get(); newValues.displayName = `${values.firstName} ${values.lastName}`; @@ -249,8 +253,16 @@ function UserProfile() { .get(); console.log('Updating with', newProfile); await userPrivSnap.ref.set(newProfile, { merge: true }); + // Write USERS_PUBLIC profile + newProfile = { d: { hasAccount: true } }; + const userPubSnap = await firestore + .doc(`${USERS_PUBLIC_COLLECTION}/${uid}`) + .get(); + console.log('Updating with', newProfile); + await userPubSnap.ref.set(newProfile, { merge: true }); console.log('Updated'); showMessage(newUser ? 'Account created' : 'Profile updated'); + history.replace(SEARCH_PATH); } catch (err) { showError(err.message); setActiveStep(0); From fedc0ac8dc50ae05a6ace40785adf585be72bf63 Mon Sep 17 00:00:00 2001 From: Tim Escher Date: Wed, 13 May 2020 20:06:56 -0700 Subject: [PATCH 11/15] Fix lints --- src/containers/Navbar/AccountMenu.js | 5 +++- .../Login/components/LoginPage/LoginPage.js | 10 +++---- .../UserProfilePage/UserProfilePage.js | 28 +++++++++---------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/containers/Navbar/AccountMenu.js b/src/containers/Navbar/AccountMenu.js index 60edf8f..a20f8a9 100644 --- a/src/containers/Navbar/AccountMenu.js +++ b/src/containers/Navbar/AccountMenu.js @@ -54,7 +54,10 @@ function AccountMenu() { onClick={closeAccountMenu}> My Requests
    - + Profile Sign Out diff --git a/src/routes/Login/components/LoginPage/LoginPage.js b/src/routes/Login/components/LoginPage/LoginPage.js index 68d79ae..ea74265 100644 --- a/src/routes/Login/components/LoginPage/LoginPage.js +++ b/src/routes/Login/components/LoginPage/LoginPage.js @@ -26,12 +26,12 @@ function LoginPage() { const userSnap = await firestore .doc(`${USERS_PUBLIC_COLLECTION}/${user.uid}`) .get(); - const data = await userSnap.data(); - console.log("Checking account", data); - if (!!data && !!data.d && !!data.d.hasAccount) { - return true; + const data = await userSnap.data(); + console.log('Checking account', data); // eslint-disable-line no-console + if (!!data && !!data.d && !!data.d.hasAccount) { + return true; } - return false; + return false; } async function googleLogin() { diff --git a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js index ed3f9a5..1644f0f 100644 --- a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js +++ b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js @@ -9,7 +9,7 @@ import { Grid, makeStyles, } from '@material-ui/core'; -import { Link, useHistory } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import Stepper from '@material-ui/core/Stepper'; import Step from '@material-ui/core/Step'; @@ -96,7 +96,7 @@ function UserProfile() { // we use this effect to monitor for permission-denied until the change has propagated, at which // point, we do the actual doc subscription (next useEffect); useEffect(() => { - console.log(userData, user); + console.log(userData, user); // eslint-disable-line no-console async function getData() { if (isLoggedIn(user) && userData == null) { // Already authenticated and haven't loaded data previously @@ -104,25 +104,25 @@ function UserProfile() { const ref = firestore.doc(`${USERS_PRIVILEGED_COLLECTION}/${user.uid}`); let data = {}; try { - console.log('ref', ref); + console.log('ref', ref); // eslint-disable-line no-console // Call it once because this will throw the permission exception. const snap = await ref.get(); data = snap.data(); setUserData(data); setUserRef(ref); - console.log('Got data', data, ref); + console.log('Got data', data, ref); // eslint-disable-line no-console if (!!data && Object.keys(data).length) { userFields.forEach(function getDefaults(key) { setValue(key, data[key]); }); if (retries > 999) showMessage('Looks like you already have an account.'); // HACK: retries > 999 means we hit the Sign Up with Google button - console.log('Loaded data into defaults'); + console.log('Loaded data into defaults'); // eslint-disable-line no-console } } catch (err) { // We only try reloading if insufficient permissions. if (err.code !== 'permission-denied') { - console.log('permission denied'); + console.log('permission denied'); // eslint-disable-line no-console throw err; } window.setTimeout(() => { @@ -137,12 +137,12 @@ function UserProfile() { }, [retries]); const handleNext = () => { - console.log('Data from this form (Next)', { ...getValues() }); + console.log('Data from this form (Next)', { ...getValues() }); // eslint-disable-line no-console setActiveStep(activeStep + 1); }; const handleBack = () => { - console.log('Data from this form (Back)', { ...getValues() }); + console.log('Data from this form (Back)', { ...getValues() }); // eslint-disable-line no-console setActiveStep(activeStep - 1); }; @@ -170,7 +170,7 @@ function UserProfile() { const handleGoogleSignIn = useCallback((authState) => { setActiveStep(2); setRetries(1000); // Force a re-run of the data load - console.log('Google handler', authState); + console.log('Google handler', authState); // eslint-disable-line no-console }, []); function handleSetUserLocationInfo(locationInfo) { @@ -210,7 +210,7 @@ function UserProfile() { const newValues = values; let uid = null; let newUser = false; - console.log('In submit', values, userData, userRef); + console.log('In submit', values, userData, userRef); // eslint-disable-line no-console // New user created with email/password if (!isLoggedIn(user)) { @@ -233,7 +233,7 @@ function UserProfile() { try { // Get email if we came in through Google. Wouldn't be in the form if (isGoogleLoggedIn(user)) { - console.log(user, user.email); + console.log(user, user.email); // eslint-disable-line no-console newValues.email = user.email; } // Write USERS profile @@ -251,16 +251,16 @@ function UserProfile() { const userPrivSnap = await firestore .doc(`${USERS_PRIVILEGED_COLLECTION}/${uid}`) .get(); - console.log('Updating with', newProfile); + console.log('Updating with', newProfile); // eslint-disable-line no-console await userPrivSnap.ref.set(newProfile, { merge: true }); // Write USERS_PUBLIC profile newProfile = { d: { hasAccount: true } }; const userPubSnap = await firestore .doc(`${USERS_PUBLIC_COLLECTION}/${uid}`) .get(); - console.log('Updating with', newProfile); + console.log('Updating with', newProfile); // eslint-disable-line no-console await userPubSnap.ref.set(newProfile, { merge: true }); - console.log('Updated'); + console.log('Updated'); // eslint-disable-line no-console showMessage(newUser ? 'Account created' : 'Profile updated'); history.replace(SEARCH_PATH); } catch (err) { From aa54329b88dc1f2658308cead15a217e0867d8bf Mon Sep 17 00:00:00 2001 From: Tim Escher Date: Thu, 14 May 2020 14:53:25 -0700 Subject: [PATCH 12/15] Validation errors to correct form, and handle change email/pw --- db.json-schema.json | 4 +- src/components/GoogleSignIn/GoogleSignIn.js | 51 --- .../GoogleSignIn/GoogleSignIn.styles.js | 7 - src/components/GoogleSignIn/index.js | 3 - .../Login/components/LoginPage/LoginPage.js | 8 +- .../UserProfilePage/UserProfilePage.js | 387 ++++++++++-------- 6 files changed, 226 insertions(+), 234 deletions(-) delete mode 100644 src/components/GoogleSignIn/GoogleSignIn.js delete mode 100644 src/components/GoogleSignIn/GoogleSignIn.styles.js delete mode 100644 src/components/GoogleSignIn/index.js diff --git a/db.json-schema.json b/db.json-schema.json index 80fc6f2..e11816a 100644 --- a/db.json-schema.json +++ b/db.json-schema.json @@ -215,9 +215,9 @@ "type": "string", "description": "A name for the location, generally 'City, State'." }, - "hasAccount": { + "hasProfile": { "type": "boolean", - "description": "True if this user has set up a login account and profile" + "description": "True if this user has set up a profile" } }, "required": ["generalLocation", "generalLocationName"] diff --git a/src/components/GoogleSignIn/GoogleSignIn.js b/src/components/GoogleSignIn/GoogleSignIn.js deleted file mode 100644 index 090f5cd..0000000 --- a/src/components/GoogleSignIn/GoogleSignIn.js +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import GoogleButton from 'react-google-button'; -import { makeStyles } from '@material-ui/core/styles'; -import useNotifications from 'modules/notification/useNotifications'; -import firebase from 'firebase/app'; // imported for auth provider -import { useAuth } from 'reactfire'; -import styles from './GoogleSignIn.styles'; - -const useStyles = makeStyles(styles); - -function GoogleSignIn({ label, handleClick }) { - const classes = useStyles(); - const auth = useAuth(); - const { showError } = useNotifications(); - - async function googleLogin(handleSignIn) { - const provider = new firebase.auth.GoogleAuthProvider(); - const isMobile = /iPhone|iPad|iPod|Android/i.test( - window.navigator.userAgent, - ); - const signInMethod = isMobile ? 'signInWithRedirect' : 'signInWithPopup'; - try { - const authState = await auth[signInMethod](provider); - // Write user profile if it doesn't exist, otherwise redirect to search page - handleSignIn(authState); - } catch (err) { - showError(err.message); - throw err; - } - } - - return ( -
    - { - googleLogin(handleClick); - }} - data-test="google-auth-button" - /> -
    - ); -} - -GoogleSignIn.propTypes = { - label: PropTypes.string, - handleClick: PropTypes.func, -}; - -export default GoogleSignIn; diff --git a/src/components/GoogleSignIn/GoogleSignIn.styles.js b/src/components/GoogleSignIn/GoogleSignIn.styles.js deleted file mode 100644 index 473bbb8..0000000 --- a/src/components/GoogleSignIn/GoogleSignIn.styles.js +++ /dev/null @@ -1,7 +0,0 @@ -export default () => ({ - providers: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - }, -}); diff --git a/src/components/GoogleSignIn/index.js b/src/components/GoogleSignIn/index.js deleted file mode 100644 index 0cb38b0..0000000 --- a/src/components/GoogleSignIn/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import GoogleSignIn from './GoogleSignIn'; - -export default GoogleSignIn; diff --git a/src/routes/Login/components/LoginPage/LoginPage.js b/src/routes/Login/components/LoginPage/LoginPage.js index ea74265..b610dc6 100644 --- a/src/routes/Login/components/LoginPage/LoginPage.js +++ b/src/routes/Login/components/LoginPage/LoginPage.js @@ -22,13 +22,13 @@ function LoginPage() { const [isLoading, setLoadingState] = useState(false); const { showError, showMessage } = useNotifications(); - async function hasAccount(user) { + async function hasProfile(user) { const userSnap = await firestore .doc(`${USERS_PUBLIC_COLLECTION}/${user.uid}`) .get(); const data = await userSnap.data(); console.log('Checking account', data); // eslint-disable-line no-console - if (!!data && !!data.d && !!data.d.hasAccount) { + if (!!data && !!data.d && !!data.d.hasProfile) { return true; } return false; @@ -43,7 +43,7 @@ function LoginPage() { const signInMethod = isMobile ? 'signInWithRedirect' : 'signInWithPopup'; try { const authState = await auth[signInMethod](provider); - if (await hasAccount(authState.user)) { + if (await hasProfile(authState.user)) { history.replace(SEARCH_PATH); return; } @@ -61,7 +61,7 @@ function LoginPage() { creds.email, creds.password, ); - if (await hasAccount(authState.user)) { + if (await hasProfile(authState.user)) { history.replace(SEARCH_PATH); return; } diff --git a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js index 1644f0f..c042d53 100644 --- a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js +++ b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Typography, TextField, @@ -24,8 +24,7 @@ import { } from 'constants/collections'; import ClickableMap from 'components/ClickableMap'; import LoadingSpinner from 'components/LoadingSpinner'; -import GoogleSignIn from 'components/GoogleSignIn'; -import { useForm } from 'react-hook-form'; +import { useForm, FormContext, useFormContext } from 'react-hook-form'; import { SEARCH_PATH } from 'constants/paths'; import styles from './UserProfilePage.styles'; @@ -35,29 +34,7 @@ function isGoogleLoggedIn(user) { return !!user && user.providerData[0].providerId === 'google.com'; } -const userProfileSchema = Yup.object().shape({ - firstName: Yup.string().min(2, 'Too Short').required('Required'), - lastName: Yup.string().min(2, 'Too Short').required('Required'), - email: Yup.string(), - phone: Yup.string().required('Required'), - password: Yup.string(), - confirmPassword: Yup.string().oneOf( - [Yup.ref('password'), null], - 'Passwords must match', - ), - address1: Yup.string(), - address2: Yup.string(), - city: Yup.string(), - state: Yup.string(), - zipcode: Yup.string(), -}); -const userFields = Object.keys(userProfileSchema.describe().fields); - -function getSteps() { - return ['Email', 'Password', 'Contact Info', 'Location']; -} - -function hasAccount(userData) { +function hasProfile(userData) { return !!userData && !!userData.email; } @@ -73,22 +50,102 @@ function UserProfile() { const history = useHistory(); const { showError, showMessage } = useNotifications(); const [isLoading, setLoadingState] = useState(true); - + const [userLocationInfo, setUserLocationInfo] = useState(null); const [retries, setRetries] = useState(0); const [userRef, setUserRef] = useState(null); const [userData, setUserData] = useState(null); - const [activeStep, setActiveStep] = useState(isGoogleLoggedIn(user) ? 2 : 0); - const steps = getSteps(); - const { - handleSubmit, - errors, - register, - formState: { isValid, dirty }, - getValues, - setValue, - } = useForm({ - validationSchema: userProfileSchema, + + const userProfileSchema = useMemo( + () => + Yup.object().shape({ + firstName: Yup.string().min(2, 'Too Short').required('Required'), + lastName: Yup.string().min(2, 'Too Short').required('Required'), + email: Yup.string(), + phone: Yup.string().required('Required'), + password: Yup.string(), + confirmPassword: Yup.string().oneOf( + [Yup.ref('password'), null], + 'Passwords must match', + ), + address1: Yup.string(), + address2: Yup.string(), + city: Yup.string(), + state: Yup.string(), + zipcode: Yup.string(), + }), + [], + ); + + const steps = ['Email', 'Password', 'Contact Info', 'Location']; + + const userFieldsStepMap = { + email: 0, + password: 1, + confirmPassword: 1, + firstName: 2, + lastName: 2, + phone: 2, + address1: 3, + address2: 3, + city: 3, + state: 3, + zipcode: 3, + }; + + const formContext = useFormContext(); + + const useYupValidationResolver = (validationSchema) => + useCallback( + async (data) => { + try { + const values = await validationSchema.validate(data, { + abortEarly: false, + }); + + return { + values, + errors: {}, + }; + } catch (errors) { + const errs = errors.inner.reduce( + (allErrors, currentError) => ({ + ...allErrors, + [currentError.path]: { + type: currentError.type ?? 'validation', + message: currentError.message, + }, + }), + {}, + ); + console.log('Errors: ', errs, 'Form State: ', formContext); // eslint-disable-line no-console + let step = 3; + Object.keys(errs).forEach((field) => { + const errStep = userFieldsStepMap[field]; + step = errStep < step ? errStep : step; + }); + setActiveStep(step); + return { + values: {}, + errors: errs, + }; + } + }, + [validationSchema], + ); + + const validationResolver = useYupValidationResolver(userProfileSchema); + + // const { + // handleSubmit, + // errors, + // register, + // formState: { isValid, dirty }, + // getValues, + // setValue, + // } = useForm({ + const methods = useForm({ + validationResolver, }); // Because of timing issues, this component will likely get run before the server has applied @@ -112,11 +169,9 @@ function UserProfile() { setUserRef(ref); console.log('Got data', data, ref); // eslint-disable-line no-console if (!!data && Object.keys(data).length) { - userFields.forEach(function getDefaults(key) { - setValue(key, data[key]); + Object.keys(userFieldsStepMap).forEach(function getDefaults(key) { + methods.setValue(key, data[key]); }); - if (retries > 999) - showMessage('Looks like you already have an account.'); // HACK: retries > 999 means we hit the Sign Up with Google button console.log('Loaded data into defaults'); // eslint-disable-line no-console } } catch (err) { @@ -137,17 +192,15 @@ function UserProfile() { }, [retries]); const handleNext = () => { - console.log('Data from this form (Next)', { ...getValues() }); // eslint-disable-line no-console + console.log('Data from this form (Next)', { ...methods.getValues() }); // eslint-disable-line no-console setActiveStep(activeStep + 1); }; const handleBack = () => { - console.log('Data from this form (Back)', { ...getValues() }); // eslint-disable-line no-console + console.log('Data from this form (Back)', { ...methods.getValues() }); // eslint-disable-line no-console setActiveStep(activeStep - 1); }; - const [userLocationInfo, setUserLocationInfo] = useState(null); - /** * Sets the value for the given field if it isn't already populated. * @param {string} fieldName name of the field which will be set @@ -163,16 +216,10 @@ function UserProfile() { addressTypeFormat = 'long_name', ) { if (googleResultComponent.types.includes(addressType)) { - setValue(fieldName, googleResultComponent[addressTypeFormat]); + methods.setValue(fieldName, googleResultComponent[addressTypeFormat]); } } - const handleGoogleSignIn = useCallback((authState) => { - setActiveStep(2); - setRetries(1000); // Force a re-run of the data load - console.log('Google handler', authState); // eslint-disable-line no-console - }, []); - function handleSetUserLocationInfo(locationInfo) { setUserLocationInfo(locationInfo); const result = locationInfo.lookedUpAddress; @@ -197,7 +244,7 @@ function UserProfile() { } }); if (streetNumber && streetAddress) { - setValue('address1', `${streetNumber} ${streetAddress}`); + methods.setValue('address1', `${streetNumber} ${streetAddress}`); } } } @@ -217,14 +264,18 @@ function UserProfile() { try { // Try creating new user const authState = await auth.createUserWithEmailAndPassword( - getValues('email'), - getValues('password'), + methods.getValues('email'), + methods.getValues('password'), ); newUser = true; uid = authState.user.uid; } catch (err) { showError(err.message); - setActiveStep(0); + if (err.code === 'auth/weak-password') { + setActiveStep(1); + } else { + setActiveStep(0); + } return; } } else { @@ -235,6 +286,14 @@ function UserProfile() { if (isGoogleLoggedIn(user)) { console.log(user, user.email); // eslint-disable-line no-console newValues.email = user.email; + } else if (isLoggedIn(user)) { + // Check for email/password changes + if (newValues.email !== user.email) { + await user.updateEmail(newValues.email); + } + if (newValues.password) { + await user.updatePassword(newValues.password); + } } // Write USERS profile const userSnap = await firestore.doc(`${USERS_COLLECTION}/${uid}`).get(); @@ -254,7 +313,7 @@ function UserProfile() { console.log('Updating with', newProfile); // eslint-disable-line no-console await userPrivSnap.ref.set(newProfile, { merge: true }); // Write USERS_PUBLIC profile - newProfile = { d: { hasAccount: true } }; + newProfile = { d: { hasProfile: true } }; const userPubSnap = await firestore .doc(`${USERS_PUBLIC_COLLECTION}/${uid}`) .get(); @@ -286,24 +345,11 @@ function UserProfile() { variant="outlined" margin="normal" fullWidth - inputRef={register} - error={!!errors.email} - helperText={errors.email && 'Enter a valid email address'} + inputRef={methods.register} + error={!!methods.errors.email} + helperText={methods.errors.email && 'Enter a valid email address'} /> - {!hasAccount(userData) ? ( - - { - handleGoogleSignIn(authState); - }} - /> - - ) : null} @@ -331,9 +377,11 @@ function UserProfile() { variant="outlined" margin="normal" fullWidth - inputRef={register} - error={!!errors.confirmPassword} - helperText={errors.confirmPassword && 'Passwords must match'} + inputRef={methods.register} + error={!!methods.errors.confirmPassword} + helperText={ + methods.errors.confirmPassword && 'Passwords must match' + } /> @@ -350,9 +398,11 @@ function UserProfile() { variant="outlined" margin="normal" fullWidth - inputRef={register} - error={!!errors.firstName} - helperText={errors.firstName && 'Enter a valid First Name'} + inputRef={methods.register} + error={!!methods.errors.firstName} + helperText={ + methods.errors.firstName && 'Enter a valid First Name' + } /> @@ -363,9 +413,9 @@ function UserProfile() { variant="outlined" margin="normal" fullWidth - inputRef={register} - error={!!errors.lastName} - helperText={errors.lastName && 'Enter a valid Last Name'} + inputRef={methods.register} + error={!!methods.errors.lastName} + helperText={methods.errors.lastName && 'Enter a valid Last Name'} /> @@ -376,9 +426,9 @@ function UserProfile() { variant="outlined" margin="normal" fullWidth - inputRef={register} - error={!!errors.phone} - helperText={errors.phone && 'Enter a valid phone number'} + inputRef={methods.register} + error={!!methods.errors.phone} + helperText={methods.errors.phone && 'Enter a valid phone number'} /> @@ -410,10 +460,10 @@ function UserProfile() { variant="outlined" margin="normal" fullWidth - inputRef={register} + inputRef={methods.register} InputLabelProps={{ shrink: true }} - error={!!errors.address1} - helperText={errors.address1 && 'address1 must be valid'} + error={!!methods.errors.address1} + helperText={methods.errors.address1 && 'address1 must be valid'} /> @@ -423,10 +473,12 @@ function UserProfile() { variant="outlined" margin="normal" fullWidth - inputRef={register} + inputRef={methods.register} InputLabelProps={{ shrink: true }} - error={!!errors.address2} - helperText={errors.address2 && 'Address 2 must be valid'} + error={!!methods.errors.address2} + helperText={ + methods.errors.address2 && 'Address 2 must be valid' + } /> @@ -438,10 +490,10 @@ function UserProfile() { variant="outlined" margin="normal" fullWidth - inputRef={register} + inputRef={methods.register} InputLabelProps={{ shrink: true }} - error={!!errors.city} - helperText={errors.city && 'City must be valid'} + error={!!methods.errors.city} + helperText={methods.errors.city && 'City must be valid'} /> @@ -451,10 +503,10 @@ function UserProfile() { variant="outlined" margin="normal" fullWidth - inputRef={register} + inputRef={methods.register} InputLabelProps={{ shrink: true }} - error={!!errors.state} - helperText={errors.state && 'State must be valid'} + error={!!methods.errors.state} + helperText={methods.errors.state && 'State must be valid'} /> @@ -464,10 +516,10 @@ function UserProfile() { variant="outlined" margin="normal" fullWidth - inputRef={register} + inputRef={methods.register} InputLabelProps={{ shrink: true }} - error={!!errors.zipcode} - helperText={errors.zipcode && 'Zip code must be valid'} + error={!!methods.errors.zipcode} + helperText={methods.errors.zipcode && 'Zip code must be valid'} /> @@ -484,76 +536,77 @@ function UserProfile() {
    - {hasAccount(userData) ? 'My Profile' : ' I Want To Help'} + {hasProfile(userData) ? 'My Profile' : ' I Want To Help'} - {hasAccount(userData) ? 'My Profile' : ' I Want To Help'} + {hasProfile(userData) ? 'My Profile' : ' I Want To Help'} -
    - - - {steps.map((label) => { - const stepProps = {}; - const labelProps = {}; - return ( - - {label} - - ); - })} - - - - {renderFields(activeStep)} -
    - - - -
    + + + + + {steps.map((label) => { + const stepProps = {}; + const labelProps = {}; + return ( + + {label} + + ); + })} + + + + {renderFields(activeStep)} +
    + + + +
    - - Note: This website and all related work products are provided - "AS IS". The provider of this service makes no other - warranties, express or implied, and hereby disclaims all implied - warranties, including any warranty of merchantability and - warranty of fitness for a particular purpose. - - {dirty && errors && !!Object.keys(errors).length && !isValid && ( - - Please fix the errors above. + + Note: This website and all related work products are provided + "AS IS". The provider of this service makes no other + warranties, express or implied, and hereby disclaims all + implied warranties, including any warranty of merchantability + and warranty of fitness for a particular purpose. - )} -
    - +
    + +
    From cb187f9e490affe6ece51d1dbbf05528948103fd Mon Sep 17 00:00:00 2001 From: Tim Escher Date: Fri, 15 May 2020 13:01:43 -0700 Subject: [PATCH 13/15] Password Reset --- src/constants/paths.js | 1 + .../Login/components/LoginForm/LoginForm.js | 4 +- .../PasswordResetPage/PasswordResetPage.js | 102 ++++++++++++++++++ .../PasswordResetPage.styles.js | 23 ++++ .../components/PasswordResetPage/index.js | 3 + src/routes/PasswordReset/index.js | 11 ++ src/routes/index.js | 2 + 7 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 src/routes/PasswordReset/components/PasswordResetPage/PasswordResetPage.js create mode 100644 src/routes/PasswordReset/components/PasswordResetPage/PasswordResetPage.styles.js create mode 100644 src/routes/PasswordReset/components/PasswordResetPage/index.js create mode 100644 src/routes/PasswordReset/index.js diff --git a/src/constants/paths.js b/src/constants/paths.js index 496ad36..088a788 100644 --- a/src/constants/paths.js +++ b/src/constants/paths.js @@ -4,6 +4,7 @@ export const LOGIN_PATH = '/login'; export const LOGOUT_PATH = '/logout'; export const SEARCH_PATH = '/search'; export const USER_PROFILE_PATH = '/user-profile'; +export const PASSWORD_RESET_PATH = '/password-reset'; export const PRIVACY_POLICY_PATH = '/privacy-policy'; export const TERMS_OF_SERVICE_PATH = '/terms-of-service'; diff --git a/src/routes/Login/components/LoginForm/LoginForm.js b/src/routes/Login/components/LoginForm/LoginForm.js index b312cc4..9d0ec68 100644 --- a/src/routes/Login/components/LoginForm/LoginForm.js +++ b/src/routes/Login/components/LoginForm/LoginForm.js @@ -4,7 +4,7 @@ import { useForm } from 'react-hook-form'; import { TextField, makeStyles, Button } from '@material-ui/core'; import { Link } from 'react-router-dom'; import { validateEmail } from 'utils/form'; -import { USER_PROFILE_PATH } from 'constants/paths'; +import { USER_PROFILE_PATH, PASSWORD_RESET_PATH } from 'constants/paths'; import styles from './LoginForm.styles'; const useStyles = makeStyles(styles); @@ -50,7 +50,7 @@ function LoginForm({ onSubmit }) { error={!!errors.password} helperText={errors.password && 'Password is required'} /> - Forgot Password{' '} + Forgot Password{' '} { // TODO: Need Forgot Password flow } diff --git a/src/routes/PasswordReset/components/PasswordResetPage/PasswordResetPage.js b/src/routes/PasswordReset/components/PasswordResetPage/PasswordResetPage.js new file mode 100644 index 0000000..14c70fd --- /dev/null +++ b/src/routes/PasswordReset/components/PasswordResetPage/PasswordResetPage.js @@ -0,0 +1,102 @@ +import React from 'react'; +import { validateEmail } from 'utils/form'; +import { useAuth } from 'reactfire'; +import { useForm } from 'react-hook-form'; +import { + Paper, + Typography, + Grid, + TextField, + makeStyles, + Button, + Container, +} from '@material-ui/core'; +import { Helmet } from 'react-helmet'; +import useNotifications from 'modules/notification/useNotifications'; +import styles from './PasswordResetPage.styles'; + +const useStyles = makeStyles(styles); + +function PasswordReset() { + const classes = useStyles(); + const auth = useAuth(); + const { showError, showMessage } = useNotifications(); + + const { + register, + handleSubmit, + errors, + getValues, + formState: { isSubmitting, isValid }, + } = useForm({ + mode: 'onChange', + nativeValidation: false, + }); + + async function handleLinkSend() { + try { + await auth.sendPasswordResetEmail(getValues('email')); + } catch (err) { + showError(err.message); + return; + } + showMessage('Email sent.'); + } + + return ( + + + Password Reset + + + Password Reset + + +
    + +
    + + + Enter your email address, and if you have an account you will + get emailed a link to reset your password. + + + + + + + +
    + +
    +
    +
    +
    +
    +
    + ); +} + +export default PasswordReset; diff --git a/src/routes/PasswordReset/components/PasswordResetPage/PasswordResetPage.styles.js b/src/routes/PasswordReset/components/PasswordResetPage/PasswordResetPage.styles.js new file mode 100644 index 0000000..60a031b --- /dev/null +++ b/src/routes/PasswordReset/components/PasswordResetPage/PasswordResetPage.styles.js @@ -0,0 +1,23 @@ +export default (theme) => ({ + root: { + ...theme.flexColumnCenter, + justifyContent: 'flex-start', + flexGrow: 1, + height: '100%', + width: '100%', + margin: '.2rem', + }, + submit: { + ...theme.flexColumnCenter, + justifyContent: 'center', + flexGrow: 1, + textAlign: 'center', + padding: '1.25rem', + minWidth: '192px', + marginTop: '1.5rem', + }, + paper: { + paddingTop: theme.spacing(3), + paddingBottom: theme.spacing(1), + }, +}); diff --git a/src/routes/PasswordReset/components/PasswordResetPage/index.js b/src/routes/PasswordReset/components/PasswordResetPage/index.js new file mode 100644 index 0000000..1b7fd2a --- /dev/null +++ b/src/routes/PasswordReset/components/PasswordResetPage/index.js @@ -0,0 +1,3 @@ +import PasswordResetPage from './PasswordResetPage'; + +export default PasswordResetPage; diff --git a/src/routes/PasswordReset/index.js b/src/routes/PasswordReset/index.js new file mode 100644 index 0000000..c425003 --- /dev/null +++ b/src/routes/PasswordReset/index.js @@ -0,0 +1,11 @@ +import { loadable } from 'utils/router'; +import { PASSWORD_RESET_PATH as path } from 'constants/paths'; + +export default { + path, + component: loadable(() => + import( + /* webpackChunkName: 'Password Reset' */ './components/PasswordResetPage' + ), + ), +}; diff --git a/src/routes/index.js b/src/routes/index.js index 36094df..b4f6c2b 100755 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -14,6 +14,7 @@ import DonateRoute from './Donate'; import AccountRoute from './Account'; import NewUserRoute from './NewUser'; import UserProfileRoute from './UserProfile'; +import PasswordResetRoute from './PasswordReset'; import SearchRoute from './Search'; import NotFoundRoute from './NotFound'; import PrivacyPolicyRoute from './PrivacyPolicy'; @@ -41,6 +42,7 @@ export default function createRoutes() { SearchRoute, NewUserRoute, UserProfileRoute, + PasswordResetRoute, AboutRoute, LoginRoute, TermsOfServiceRoute, From 763e446e8c2ce914ac9c605edc2956599939e12e Mon Sep 17 00:00:00 2001 From: Tim Escher Date: Sat, 16 May 2020 14:41:56 -0700 Subject: [PATCH 14/15] Add in location lookup and save based on address --- .../UserProfilePage/UserProfilePage.js | 54 +++++++++++++++++++ src/utils/geo.js | 20 +++++++ 2 files changed, 74 insertions(+) diff --git a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js index c042d53..e7b1760 100644 --- a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js +++ b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js @@ -23,6 +23,7 @@ import { USERS_PUBLIC_COLLECTION, } from 'constants/collections'; import ClickableMap from 'components/ClickableMap'; +import { geocode } from 'utils/geo'; import LoadingSpinner from 'components/LoadingSpinner'; import { useForm, FormContext, useFormContext } from 'react-hook-form'; import { SEARCH_PATH } from 'constants/paths'; @@ -281,6 +282,56 @@ function UserProfile() { } else { uid = user.uid; } + // Get the user's precise location + try { + const address = `${values.address1} ${values.address2} ${values.city}, ${values.state} ${values.zipcode}`; + const locationResult = await geocode(address); + console.log(address, 'Geocode result', locationResult); // eslint-disable-line no-console + const latitude = locationResult.results[0].geometry.location.lat(); + const longitude = locationResult.results[0].geometry.location.lng(); + newValues.preciseLocation = { latitude, longitude }; + newValues.generalLocation = { latitude, longitude }; + let city = null; + let state = null; + for ( + let i = 0; + i < locationResult.results[0].address_components.length && + (!city || !state); + i += 1 + ) { + for ( + let b = 0; + b < locationResult.results[0].address_components[i].types.length && + (!city || !state); + b += 1 + ) { + if ( + locationResult.results[0].address_components[i].types[b] === + 'administrative_area_level_1' + ) { + state = locationResult.results[0].address_components[i].short_name; + } + if ( + locationResult.results[0].address_components[i].types[b] === + 'locality' + ) { + city = locationResult.results[0].address_components[i].long_name; + } + } + } + + newValues.generalLocationName = `${city}, ${state}`; + console.log('After address lookup', newValues); // eslint-disable-line no-console + } catch (err) { + if (err === 'ZERO_RESULTS') { + showError('Could not find your address. Please try again'); + } else { + console.log('Address Error', err); // eslint-disable-line no-console + showError('Address Lookup Error', err); + } + setActiveStep(3); + return; + } try { // Get email if we came in through Google. Wouldn't be in the form if (isGoogleLoggedIn(user)) { @@ -303,6 +354,9 @@ function UserProfile() { lastName: newValues.lastName, email: newValues.email, displayName: newValues.displayName, + preciseLocation: newValues.preciseLocation, + generalLocation: newValues.generalLocation, + generalLocationName: newValues.generalLocationName, }; await userSnap.ref.set(newProfile, { merge: true }); // Write USERS_PRIVILEGED profile diff --git a/src/utils/geo.js b/src/utils/geo.js index 1181777..bf6c493 100644 --- a/src/utils/geo.js +++ b/src/utils/geo.js @@ -107,3 +107,23 @@ export async function reverseGeocode(latitude, longitude) { ); }); } + +export async function geocode(address) { + if (!window.google?.maps?.Geocoder) { + // eslint-disable-next-line no-console + console.error('Google Maps API not found.'); + Sentry.captureMessage('Google Maps API not found', Sentry.Severity.Error); + return null; + } + + return new Promise((resolve, reject) => { + const geocoder = new window.google.maps.Geocoder(); + geocoder.geocode({ address }, (results, status) => { + if (status !== window.google.maps.GeocoderStatus.OK) { + reject(status); + } else { + resolve({ status, results }); + } + }); + }); +} From bb18b13b5a292b59a2202c659477ccceb6846598 Mon Sep 17 00:00:00 2001 From: Tim Escher Date: Sat, 16 May 2020 14:50:32 -0700 Subject: [PATCH 15/15] lint fixes --- src/routes/Login/components/LoginForm/LoginForm.js | 3 --- .../components/PasswordResetPage/PasswordResetPage.js | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/routes/Login/components/LoginForm/LoginForm.js b/src/routes/Login/components/LoginForm/LoginForm.js index 9d0ec68..8b2f96b 100644 --- a/src/routes/Login/components/LoginForm/LoginForm.js +++ b/src/routes/Login/components/LoginForm/LoginForm.js @@ -51,9 +51,6 @@ function LoginForm({ onSubmit }) { helperText={errors.password && 'Password is required'} /> Forgot Password{' '} - { - // TODO: Need Forgot Password flow - }