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"