From d3d73ca47537f4e33a6ab3816c20d8eb275936aa Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Sat, 5 Dec 2020 18:16:30 +1100 Subject: [PATCH] issue-129 Update signin/up ui-ux Getting rid of the netlify widgit and rolling our own, gives us the flexibility to also add the username into the signup process as well as allow the user to opt into the newsletter at the same time. Auth is still netlify, via their "gotrue", we're just handling the more of it. --- api/src/functions/identity-signup.js | 4 +- web/package.json | 3 +- web/src/Routes.js | 2 + .../components/InputTextForm/InputTextForm.js | 34 ++++ .../InputTextForm/InputTextForm.stories.js | 7 + .../InputTextForm/InputTextForm.test.js | 11 + .../LandingSection/LandingSection.js | 14 +- web/src/components/LoginModal/LoginModal.js | 189 ++++++++++++++++++ .../LoginModal/LoginModal.stories.js | 7 + .../components/LoginModal/LoginModal.test.js | 11 + web/src/helpers/subscribe.js | 15 ++ web/src/index.js | 12 +- web/src/layouts/MainLayout/MainLayout.js | 46 ++++- .../AccountRecoveryPage.js | 67 +++++++ .../AccountRecoveryPage.stories.js | 7 + .../AccountRecoveryPage.test.js | 11 + .../UpdatePasswordPage/UpdatePasswordPage.js | 78 ++++++++ .../UpdatePasswordPage.stories.js | 7 + .../UpdatePasswordPage.test.js | 11 + yarn.lock | 20 +- 20 files changed, 535 insertions(+), 21 deletions(-) create mode 100644 web/src/components/InputTextForm/InputTextForm.js create mode 100644 web/src/components/InputTextForm/InputTextForm.stories.js create mode 100644 web/src/components/InputTextForm/InputTextForm.test.js create mode 100644 web/src/components/LoginModal/LoginModal.js create mode 100644 web/src/components/LoginModal/LoginModal.stories.js create mode 100644 web/src/components/LoginModal/LoginModal.test.js create mode 100644 web/src/helpers/subscribe.js create mode 100644 web/src/pages/AccountRecoveryPage/AccountRecoveryPage.js create mode 100644 web/src/pages/AccountRecoveryPage/AccountRecoveryPage.stories.js create mode 100644 web/src/pages/AccountRecoveryPage/AccountRecoveryPage.test.js create mode 100644 web/src/pages/UpdatePasswordPage/UpdatePasswordPage.js create mode 100644 web/src/pages/UpdatePasswordPage/UpdatePasswordPage.stories.js create mode 100644 web/src/pages/UpdatePasswordPage/UpdatePasswordPage.test.js diff --git a/api/src/functions/identity-signup.js b/api/src/functions/identity-signup.js index dd24b48..361b195 100644 --- a/api/src/functions/identity-signup.js +++ b/api/src/functions/identity-signup.js @@ -62,12 +62,12 @@ export const handler = async (req, _context) => { db.user.findOne({ where: { userName: seed }, }) - const userNameSeed = enforceAlphaNumeric(email.split('@')[0]) + const userNameSeed = enforceAlphaNumeric(user?.user_metadata?.userName) const userName = await generateUniqueString(userNameSeed, isUniqueCallback) // TODO maybe come up with a better default userName? const input = { email, userName, - name: user.user_metadata && user.user_metadata.full_name, + name: user?.user_metadata?.full_name, id: user.id, } await createUserInsecure({ input }) diff --git a/web/package.json b/web/package.json index b047ab0..46c5198 100644 --- a/web/package.json +++ b/web/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@material-ui/core": "^4.11.0", - "@redwoodjs/auth": "^0.20.0", + "@redwoodjs/auth": "^0.21.0", "@redwoodjs/forms": "^0.20.0", "@redwoodjs/router": "^0.20.0", "@redwoodjs/web": "^0.20.0", @@ -22,6 +22,7 @@ "controlkit": "^0.1.9", "get-active-classes": "^0.0.11", "golden-layout": "^1.5.9", + "gotrue-js": "^0.9.27", "jquery": "^3.5.1", "monaco-editor": "^0.20.0", "monaco-editor-webpack-plugin": "^1.9.1", diff --git a/web/src/Routes.js b/web/src/Routes.js index 51aa93e..dbb6777 100644 --- a/web/src/Routes.js +++ b/web/src/Routes.js @@ -32,6 +32,8 @@ const Routes = () => { ) return ( + + diff --git a/web/src/components/InputTextForm/InputTextForm.js b/web/src/components/InputTextForm/InputTextForm.js new file mode 100644 index 0000000..154a45c --- /dev/null +++ b/web/src/components/InputTextForm/InputTextForm.js @@ -0,0 +1,34 @@ +import { getActiveClasses } from 'get-active-classes' +import { TextField, FieldError } from '@redwoodjs/forms' +import { useFormContext } from 'react-hook-form' + +const InputText = ({ type = 'text', className, name, validation }) => { + const { errors } = useFormContext() + return ( + <> +
+ +
+ +
+ + ) +} + +export default InputText diff --git a/web/src/components/InputTextForm/InputTextForm.stories.js b/web/src/components/InputTextForm/InputTextForm.stories.js new file mode 100644 index 0000000..26941e3 --- /dev/null +++ b/web/src/components/InputTextForm/InputTextForm.stories.js @@ -0,0 +1,7 @@ +import InputTextForm from './InputTextForm' + +export const generated = () => { + return +} + +export default { title: 'Components/InputTextForm' } diff --git a/web/src/components/InputTextForm/InputTextForm.test.js b/web/src/components/InputTextForm/InputTextForm.test.js new file mode 100644 index 0000000..f649e50 --- /dev/null +++ b/web/src/components/InputTextForm/InputTextForm.test.js @@ -0,0 +1,11 @@ +import { render } from '@redwoodjs/testing' + +import InputTextForm from './InputTextForm' + +describe('InputTextForm', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/web/src/components/LandingSection/LandingSection.js b/web/src/components/LandingSection/LandingSection.js index 6391b89..5920f37 100644 --- a/web/src/components/LandingSection/LandingSection.js +++ b/web/src/components/LandingSection/LandingSection.js @@ -7,17 +7,18 @@ import { } from './mockEditorParts' import Svg from 'src/components/Svg' import OutBound from 'src/components/OutBound' -import { useAuth } from '@redwoodjs/auth' import ReactGA from 'react-ga' +import LoginModal from 'src/components/LoginModal' +import { useState } from 'react' const LandingSection = () => { - const { logIn } = useAuth() - const recordedLogin = () => { + const [isLoginModalOpen, setIsLoginModalOpen] = useState(false) + const recordedLogin = async () => { ReactGA.event({ category: 'login', action: 'landing section CTA', }) - logIn() + setIsLoginModalOpen(true) } return (
@@ -181,6 +182,11 @@ const LandingSection = () => { />
+ setIsLoginModalOpen(false)} + shouldStartWithSignup + /> ) } diff --git a/web/src/components/LoginModal/LoginModal.js b/web/src/components/LoginModal/LoginModal.js new file mode 100644 index 0000000..b0080eb --- /dev/null +++ b/web/src/components/LoginModal/LoginModal.js @@ -0,0 +1,189 @@ +import { useState } from 'react' +import Dialog from '@material-ui/core/Dialog' +import Tab from '@material-ui/core/Tab' +import Tabs from '@material-ui/core/Tabs' +import InputTextForm from 'src/components/InputTextForm' +import OutBound from 'src/components/OutBound' +import { Form, Submit } from '@redwoodjs/forms' +import { useAuth } from '@redwoodjs/auth' +import { useFlash } from '@redwoodjs/web' +import { Link, routes } from '@redwoodjs/router' +import { subscribe } from 'src/helpers/subscribe' + +const LoginModal = ({ open, onClose, shouldStartWithSignup = false }) => { + const { logIn, signUp } = useAuth() + const { addMessage } = useFlash() + + const [tab, setTab] = useState(shouldStartWithSignup ? 0 : 1) + const onTabChange = (_, newValue) => { + setTab(newValue) + setError('') + } + const [checkBox, setCheckBox] = useState(true) + const [error, setError] = useState('') + + const onSubmitSignUp = async ({ email, password, name, userName }) => { + try { + setError('') + if (checkBox) { + subscribe({ email, addMessage }) + } + await signUp({ + email, + password, + remember: { full_name: name, userName }, + }) + onClose() + } catch (errorEvent) { + setError(errorEvent?.json?.error_description) + } + } + const onSubmitSignIn = async ({ email, password }) => { + try { + setError('') + await logIn({ email, password, remember: true }) + onClose() + } catch (errorEvent) { + setError(errorEvent?.json?.error_description) + } + } + return ( + +
+ + + + + {error && ( +
+ {error} +
+ )} + {tab === 0 ? ( + + ) : ( + + )} +
+
+ ) +} + +const Field = ({ name, type = 'text', validation }) => ( + <> + + {name}: + + + +) + +const HeroButton = ({ text }) => ( + + {text} + +) + +const SignInForm = ({ onSubmitSignIn }) => ( +
+
+
+ + +
+ + forgot your password? + +
+ + +) + +const SignUpForm = ({ onSubmitSignUp, checkBox, setCheckBox }) => ( +
+
+
+ + + + +
+
+ setCheckBox(!checkBox)} + />{' '} + + Stay up-to-date with CadHub's progress with the founder's ( + + Kurt's + + ) newsletter + +
+
+ + +) + +export default LoginModal diff --git a/web/src/components/LoginModal/LoginModal.stories.js b/web/src/components/LoginModal/LoginModal.stories.js new file mode 100644 index 0000000..9a62186 --- /dev/null +++ b/web/src/components/LoginModal/LoginModal.stories.js @@ -0,0 +1,7 @@ +import LoginModal from './LoginModal' + +export const generated = () => { + return +} + +export default { title: 'Components/LoginModal' } diff --git a/web/src/components/LoginModal/LoginModal.test.js b/web/src/components/LoginModal/LoginModal.test.js new file mode 100644 index 0000000..4da93e4 --- /dev/null +++ b/web/src/components/LoginModal/LoginModal.test.js @@ -0,0 +1,11 @@ +import { render } from '@redwoodjs/testing' + +import LoginModal from './LoginModal' + +describe('LoginModal', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/web/src/helpers/subscribe.js b/web/src/helpers/subscribe.js new file mode 100644 index 0000000..7a21875 --- /dev/null +++ b/web/src/helpers/subscribe.js @@ -0,0 +1,15 @@ +export const subscribe = ({ email, addMessage }) => { + // subscribe to mailchimp newsletter + const path = window.location.hostname + window.location.pathname + try { + fetch( + `https://kurthutten.us10.list-manage.com/subscribe/post-json?u=cbd8888e924bdd99d06c14fa5&id=6a765a8b3d&EMAIL=${email}&FNAME=Kurt&PATHNAME=${path}&c=__jp0` + ) + } catch (e) { + setTimeout(() => { + addMessage('Problem subscribing to newsletter', { + classes: 'bg-red-300 text-red-900', + }) + }, 1000) + } +} diff --git a/web/src/index.js b/web/src/index.js index 20431b5..ee5d31a 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -1,5 +1,6 @@ import { AuthProvider } from '@redwoodjs/auth' -import netlifyIdentity from 'netlify-identity-widget' +import GoTrue from 'gotrue-js' + import ReactDOM from 'react-dom' import { RedwoodProvider, FatalErrorBoundary } from '@redwoodjs/web' import FatalErrorPage from 'src/pages/FatalErrorPage' @@ -16,8 +17,6 @@ import './cascade/css/main.css' import 'monaco-editor/min/vs/editor/editor.main.css' import './index.css' -netlifyIdentity.init() - function initCascadeStudio() { // if ('serviceWorker' in navigator) { // navigator.serviceWorker.register('service-worker.js').then(function(registration) { @@ -49,9 +48,14 @@ function initCascadeStudio() { } initCascadeStudio() +const goTrueClient = new GoTrue({ + APIUrl: 'https://cadhub.xyz/.netlify/identity', + setCookie: true, +}) + ReactDOM.render( - + diff --git a/web/src/layouts/MainLayout/MainLayout.js b/web/src/layouts/MainLayout/MainLayout.js index b3da63c..a195131 100644 --- a/web/src/layouts/MainLayout/MainLayout.js +++ b/web/src/layouts/MainLayout/MainLayout.js @@ -1,12 +1,12 @@ import { useState, useEffect } from 'react' -import { Link, routes } from '@redwoodjs/router' +import { Link, routes, navigate } from '@redwoodjs/router' import { useAuth } from '@redwoodjs/auth' -import { Flash } from '@redwoodjs/web' +import { Flash, useQuery, useFlash } from '@redwoodjs/web' import Tooltip from '@material-ui/core/Tooltip' -import { useQuery } from '@redwoodjs/web' import Popover from '@material-ui/core/Popover' import { getActiveClasses } from 'get-active-classes' import { useLocation } from '@redwoodjs/router' +import LoginModal from 'src/components/LoginModal' import ReactGA from 'react-ga' export const QUERY = gql` @@ -26,11 +26,13 @@ let previousSubmission = '' let previousUserID = '' const MainLayout = ({ children }) => { - const { logIn, logOut, isAuthenticated, currentUser } = useAuth() + const { logOut, isAuthenticated, currentUser, client } = useAuth() + const { addMessage } = useFlash() const { data, loading } = useQuery(QUERY, { skip: !currentUser?.sub, variables: { id: currentUser?.sub }, }) + const [isLoginModalOpen, setIsLoginModalOpen] = useState(false) const [isOpen, setIsOpen] = useState(false) const [anchorEl, setAnchorEl] = useState(null) const [popoverId, setPopoverId] = useState(undefined) @@ -59,7 +61,7 @@ const MainLayout = ({ children }) => { category: 'login', action: 'navbar login', }) - logIn() + setIsLoginModalOpen(true) } const { pathname, params } = useLocation() @@ -85,6 +87,34 @@ const MainLayout = ({ children }) => { previousUserID = currentUser } }, [data, currentUser, isAuthenticated]) + const hash = window.location.hash + useEffect(() => { + const [key, token] = hash.slice(1).split('=') + if (key === 'confirmation_token') { + console.log('confirming with', token) + client + .confirm(token, true) + .then(() => { + addMessage('Email confirmed', { classes: 'rw-flash-success' }) + }) + .catch(() => { + addMessage('Problem confirming email', { + classes: 'bg-red-300 text-red-900', + }) + }) + } else if (key === 'recovery_token') { + client + .recover(token, true) + .then(() => { + navigate(routes.updatePassword()) + }) + .catch(() => { + addMessage('Problem recovering account', { + classes: 'bg-red-300 text-red-900', + }) + }) + } + }, [hash, client]) // complaining about not having addMessage, however adding it puts useEffect into a loop return ( <>
@@ -198,7 +228,11 @@ const MainLayout = ({ children }) => { )}
- + + setIsLoginModalOpen(false)} + />
{children}
) diff --git a/web/src/pages/AccountRecoveryPage/AccountRecoveryPage.js b/web/src/pages/AccountRecoveryPage/AccountRecoveryPage.js new file mode 100644 index 0000000..d7b327b --- /dev/null +++ b/web/src/pages/AccountRecoveryPage/AccountRecoveryPage.js @@ -0,0 +1,67 @@ +import { routes, navigate } from '@redwoodjs/router' +import { useAuth } from '@redwoodjs/auth' +import { Form, Submit } from '@redwoodjs/forms' +import { useFlash } from '@redwoodjs/web' + +import InputTextForm from 'src/components/InputTextForm' +import MainLayout from 'src/layouts/MainLayout' +import Seo from 'src/components/Seo/Seo' + +const AccountRecoveryPage = () => { + const { addMessage } = useFlash() + const { client } = useAuth() + const onSubmit = ({ email }) => { + client + .requestPasswordRecovery(email) + .then(() => { + addMessage('Email sent', { classes: 'rw-flash-success' }) + setTimeout(() => { + navigate(routes.home()) + }, 500) + }) + .catch(() => { + addMessage('Problem sending email', { + classes: 'bg-red-300 text-red-900', + }) + }) + } + return ( + + + +
+

Send recovery email

+
+
+ + email: + + +
+ + Send email + +
+
+
+ ) +} + +export default AccountRecoveryPage diff --git a/web/src/pages/AccountRecoveryPage/AccountRecoveryPage.stories.js b/web/src/pages/AccountRecoveryPage/AccountRecoveryPage.stories.js new file mode 100644 index 0000000..4c577ca --- /dev/null +++ b/web/src/pages/AccountRecoveryPage/AccountRecoveryPage.stories.js @@ -0,0 +1,7 @@ +import AccountRecoveryPage from './AccountRecoveryPage' + +export const generated = () => { + return +} + +export default { title: 'Pages/AccountRecoveryPage' } diff --git a/web/src/pages/AccountRecoveryPage/AccountRecoveryPage.test.js b/web/src/pages/AccountRecoveryPage/AccountRecoveryPage.test.js new file mode 100644 index 0000000..a1b04a5 --- /dev/null +++ b/web/src/pages/AccountRecoveryPage/AccountRecoveryPage.test.js @@ -0,0 +1,11 @@ +import { render } from '@redwoodjs/testing' + +import AccountRecoveryPage from './AccountRecoveryPage' + +describe('AccountRecoveryPage', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/web/src/pages/UpdatePasswordPage/UpdatePasswordPage.js b/web/src/pages/UpdatePasswordPage/UpdatePasswordPage.js new file mode 100644 index 0000000..48008d1 --- /dev/null +++ b/web/src/pages/UpdatePasswordPage/UpdatePasswordPage.js @@ -0,0 +1,78 @@ +import { routes, navigate } from '@redwoodjs/router' +import { useAuth } from '@redwoodjs/auth' +import { Form, Submit } from '@redwoodjs/forms' +import { useFlash } from '@redwoodjs/web' + +import InputTextForm from 'src/components/InputTextForm' +import MainLayout from 'src/layouts/MainLayout' +import Seo from 'src/components/Seo/Seo' + +const UpdatePasswordPage = () => { + const { addMessage } = useFlash() + const { client } = useAuth() + const onSubmit = ({ password, confirm }) => { + if (password !== confirm || !password) { + addMessage("Passwords don't match, try again", { + classes: 'bg-red-300 text-red-900', + }) + return + } + client + .currentUser() + .update({ password }) + .then(() => { + addMessage('Password updated', { classes: 'rw-flash-success' }) + setTimeout(() => { + navigate(routes.home()) + }, 500) + }) + .catch(() => { + addMessage('Problem updating password', { + classes: 'bg-red-300 text-red-900', + }) + }) + } + return ( + + + +
+

Reset Password

+
+
+ + password: + + + + confirm: + + +
+ + Update + +
+
+
+ ) +} + +export default UpdatePasswordPage diff --git a/web/src/pages/UpdatePasswordPage/UpdatePasswordPage.stories.js b/web/src/pages/UpdatePasswordPage/UpdatePasswordPage.stories.js new file mode 100644 index 0000000..1633909 --- /dev/null +++ b/web/src/pages/UpdatePasswordPage/UpdatePasswordPage.stories.js @@ -0,0 +1,7 @@ +import UpdatePasswordPage from './UpdatePasswordPage' + +export const generated = () => { + return +} + +export default { title: 'Pages/UpdatePasswordPage' } diff --git a/web/src/pages/UpdatePasswordPage/UpdatePasswordPage.test.js b/web/src/pages/UpdatePasswordPage/UpdatePasswordPage.test.js new file mode 100644 index 0000000..d589bee --- /dev/null +++ b/web/src/pages/UpdatePasswordPage/UpdatePasswordPage.test.js @@ -0,0 +1,11 @@ +import { render } from '@redwoodjs/testing' + +import UpdatePasswordPage from './UpdatePasswordPage' + +describe('UpdatePasswordPage', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/yarn.lock b/yarn.lock index 36dde25..fe2cbd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2366,10 +2366,10 @@ lodash.omitby "^4.6.0" merge-graphql-schemas "^1.7.6" -"@redwoodjs/auth@^0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@redwoodjs/auth/-/auth-0.20.0.tgz#c08c0b735a0b09ef84dc6d357fa2803d61f1d389" - integrity sha512-M1rPtiXzU7YagQ120zEkp5qvJruWRcFLUfBhgexGYgnEkqFIKFGLQL9pBywoZ6kxNjI7x/pbghz6FS7H/lzw/g== +"@redwoodjs/auth@^0.21.0": + version "0.21.0" + resolved "https://registry.yarnpkg.com/@redwoodjs/auth/-/auth-0.21.0.tgz#967deef0d0421ea9f6bc205857ad9265d2f5df55" + integrity sha512-o3HuRTs79BqmnZX2zvK6+ffebxJE+T/nqwDDdOmCjXUitSsYbLSJkG4ffUuMtSWCFCxf/ytaFB245nS8Vin3XQ== "@redwoodjs/cli@^0.20.0": version "0.20.0" @@ -8671,6 +8671,13 @@ good-listener@^1.2.2: dependencies: delegate "^3.1.2" +gotrue-js@^0.9.27: + version "0.9.27" + resolved "https://registry.yarnpkg.com/gotrue-js/-/gotrue-js-0.9.27.tgz#ed01b47e97781f10f26458ff77d83d97bcc65e08" + integrity sha512-XSQ9XGELrnf6bKYaGk+2z1E6aW6+wZ3S6ns3JQz9tXesBuwVPDv/xOTTZ/Qrtu85GJgxTjhJH447w09a73Tneg== + dependencies: + micro-api-client "^3.2.1" + graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" @@ -11331,6 +11338,11 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= +micro-api-client@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/micro-api-client/-/micro-api-client-3.3.0.tgz#52dd567d322f10faffe63d19d4feeac4e4ffd215" + integrity sha512-y0y6CUB9RLVsy3kfgayU28746QrNMpSm9O/AYGNsBgOkJr/X/Jk0VLGoO8Ude7Bpa8adywzF+MzXNZRFRsNPhg== + microevent.ts@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0"