diff --git a/client/src/assets/img/logo.svg b/client/src/assets/img/logo.svg deleted file mode 100644 index fa893090..00000000 --- a/client/src/assets/img/logo.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/client/src/components/AuthClient.ts b/client/src/components/AuthClient.ts index ba55a3d6..dd201763 100644 --- a/client/src/components/AuthClient.ts +++ b/client/src/components/AuthClient.ts @@ -100,7 +100,7 @@ const resetPasswordRequest = async (username?: string) => { } else { const params = new URLSearchParams(window.location.search); await postAuthRequest( - `${BASE_URL}auth/reset_password_request_with_token?u=${params.get("token")}` + `${BASE_URL}auth/reset_password_request_with_token?token=${params.get("token")}` ); } return true; diff --git a/client/src/components/EmailAlertSignup.tsx b/client/src/components/EmailAlertSignup.tsx index 003472f0..ab8ac195 100644 --- a/client/src/components/EmailAlertSignup.tsx +++ b/client/src/components/EmailAlertSignup.tsx @@ -65,7 +65,6 @@ const BuildingSubscribeWithoutI18n = (props: BuildingSubscribeProps) => { {!isEmailResent && Didn’t get the link?} AuthClient.resendVerifyEmail()} /> diff --git a/client/src/components/JFLogo.tsx b/client/src/components/JFLogo.tsx new file mode 100644 index 00000000..c0b2f056 --- /dev/null +++ b/client/src/components/JFLogo.tsx @@ -0,0 +1,41 @@ +import React, { SVGProps } from "react"; + +export const JFLogo = (props: SVGProps) => ( + + + + + + + + + +); diff --git a/client/src/components/Login.tsx b/client/src/components/Login.tsx index 4b0fc596..5dc87169 100644 --- a/client/src/components/Login.tsx +++ b/client/src/components/Login.tsx @@ -2,7 +2,6 @@ import React, { useState, useContext } from "react"; import { Trans, t } from "@lingui/macro"; import { I18n } from "@lingui/core"; import { withI18n } from "@lingui/react"; -import classNames from "classnames"; import { Button, Link as JFCLLink } from "@justfixnyc/component-library"; import "styles/Login.css"; @@ -28,6 +27,7 @@ enum Step { RegisterAccount, RegisterUserType, VerifyEmail, + LoginSuccess, } type LoginProps = { @@ -35,7 +35,6 @@ type LoginProps = { addr?: AddressRecord; onBuildingPage?: boolean; onSuccess?: (user: JustfixUser) => void; - handleRedirect?: () => void; registerInModal?: boolean; setLoginRegisterInProgress?: React.Dispatch>; showForgotPassword?: boolean; @@ -47,13 +46,12 @@ const LoginWithoutI18n = (props: LoginProps) => { addr, onBuildingPage, onSuccess, - handleRedirect, registerInModal, setLoginRegisterInProgress, } = props; const userContext = useContext(UserContext); - const { account } = createWhoOwnsWhatRoutePaths(); + const { home, account } = createWhoOwnsWhatRoutePaths(); const [showRegisterModal, setShowRegisterModal] = useState(false); @@ -63,6 +61,7 @@ const LoginWithoutI18n = (props: LoginProps) => { const isRegisterAccountStep = step === Step.RegisterAccount; const isRegisterUserTypeStep = step === Step.RegisterUserType; const isVerifyEmailStep = step === Step.VerifyEmail; + const isLoginSuccessStep = step === Step.LoginSuccess; const { value: email, @@ -157,7 +156,7 @@ const LoginWithoutI18n = (props: LoginProps) => { const renderFooter = () => { return ( -
+
{isRegisterAccountStep && ( @@ -219,18 +218,27 @@ const LoginWithoutI18n = (props: LoginProps) => { }; const renderResendVerifyEmail = () => ( - <> +
{!isEmailResent && ( - + Didn’t get the link? )} AuthClient.resendVerifyEmail()} /> +
+ ); + + const renderLoginSuccess = () => ( + <> + You are logged in + + Search for an address to add to Building Updates + ); @@ -291,12 +299,17 @@ const LoginWithoutI18n = (props: LoginProps) => { return; } - !!setLoginRegisterInProgress && setLoginRegisterInProgress(false); - if (!onBuildingPage) { - handleRedirect && handleRedirect(); + if (resp?.user?.verified) { + setStep(Step.LoginSuccess); + } else { + await AuthClient.resendVerifyEmail(); + setStep(Step.VerifyEmail); + } return; } + + !!setLoginRegisterInProgress && setLoginRegisterInProgress(false); }; const onAccountSubmit = async () => { @@ -433,12 +446,15 @@ const LoginWithoutI18n = (props: LoginProps) => { const renderLoginFlow = () => { return (
- {!!headerText && ( -

{headerText}

- )} - {!!subHeaderText &&
{subHeaderText}
} + {!!headerText && (onBuildingPage ?

{headerText}

:

{headerText}

)} + {!!subHeaderText && + (onBuildingPage ? ( +
{subHeaderText}
+ ) : ( +

{subHeaderText}

+ ))} {renderAlert()} - {!isVerifyEmailStep && ( + {!isVerifyEmailStep && !isLoginSuccessStep && (
{ @@ -468,7 +484,7 @@ const LoginWithoutI18n = (props: LoginProps) => { setError={setPasswordError} onChange={onChangePassword} showPasswordRules={isRegisterAccountStep} - autoFocus={showRegisterModal && !!email && !password} + autoFocus={!!email && !password} /> )} {isRegisterUserTypeStep && ( @@ -483,7 +499,7 @@ const LoginWithoutI18n = (props: LoginProps) => {
); diff --git a/client/src/components/SendNewLink.tsx b/client/src/components/SendNewLink.tsx index d166fee4..2c21c128 100644 --- a/client/src/components/SendNewLink.tsx +++ b/client/src/components/SendNewLink.tsx @@ -9,12 +9,13 @@ import { CheckIcon } from "./Icons"; type SendNewLinkProps = withI18nProps & { setParentState: React.Dispatch>; onClick: () => void; - variant: "primary" | "secondary" | "tertiary"; + variant?: "primary" | "secondary" | "tertiary"; + size?: "small" | "large"; className?: string; }; const SendNewLinkWithoutI18n = (props: SendNewLinkProps) => { - const { setParentState, onClick, variant, className, i18n } = props; + const { setParentState, onClick, variant = "secondary", size = "small", className, i18n } = props; const [linkSent, setLinkSent] = useState(false); @@ -30,7 +31,7 @@ const SendNewLinkWithoutI18n = (props: SendNewLinkProps) => {
diff --git a/client/src/containers/App.tsx b/client/src/containers/App.tsx index 7a6ecd66..596f888e 100644 --- a/client/src/containers/App.tsx +++ b/client/src/containers/App.tsx @@ -1,8 +1,6 @@ import React, { useEffect, useState, useContext } from "react"; import { BrowserRouter as Router, Switch, Route, useLocation } from "react-router-dom"; import { Trans, t } from "@lingui/macro"; -import logo from "../assets/img/logo.svg"; -import logoDivider from "../assets/img/logo-divider.svg"; import "styles/App.css"; @@ -49,6 +47,8 @@ import ResetPasswordPage from "./ResetPasswordPage"; import ForgotPasswordPage from "./ForgotPasswordPage"; import UnsubscribePage from "./UnsubscribePage"; import LoginPage from "./LoginPage"; +import { JFLogo } from "components/JFLogo"; +import logoDivider from "../assets/img/logo-divider.svg"; const HomeLink = withI18n()((props: withI18nProps) => { const { i18n } = props; @@ -65,7 +65,7 @@ const HomeLink = withI18n()((props: withI18nProps) => { }} to={isLegacyPath(pathname) ? legacy.home : home} > - JustFix + { const userContext = useContext(UserContext); - return ( + const standalonePages = [ + "forgot-password", + "login", + "verify-email", + "forgot-password", + "reset-password", + "unsubscribe", + ]; + const hideNavbar = standalonePages.some((v) => pathname.includes(`account/${v}`)); + + return hideNavbar ? null : (
{ const { i18n } = props; const { search } = useLocation(); + const { account } = createWhoOwnsWhatRoutePaths(); const params = new URLSearchParams(search); const [requestSent, setRequestSent] = React.useState(false); @@ -46,59 +46,58 @@ const ForgotPasswordPage = withI18n()((props: withI18nProps) => { }; return ( - -
-
-
- - Reset Password - - {!requestSent ? ( - <> - - Review your email address below. You’ll receive a "Reset password" email to this - address. - -
- -
- -
- + + )} + ); }); diff --git a/client/src/containers/LoginPage.tsx b/client/src/containers/LoginPage.tsx index 119ceb77..bfe4f5f2 100644 --- a/client/src/containers/LoginPage.tsx +++ b/client/src/containers/LoginPage.tsx @@ -1,32 +1,16 @@ -import { useHistory } from "react-router-dom"; -import LegalFooter from "../components/LegalFooter"; - -import Page from "../components/Page"; import { withI18n, withI18nProps } from "@lingui/react"; import { t } from "@lingui/macro"; + +import StandalonePage from "components/StandalonePage"; import Login from "components/Login"; -import { createWhoOwnsWhatRoutePaths } from "routes"; const LoginPage = withI18n()((props: withI18nProps) => { const { i18n } = props; - const history = useHistory(); - const { account } = createWhoOwnsWhatRoutePaths(); - - const redirect = () => { - window.setTimeout(() => { - history.push(account.settings); - }, 1000); - }; return ( - -
-
- -
- -
-
+ + + ); }); diff --git a/client/src/containers/ResetPasswordPage.tsx b/client/src/containers/ResetPasswordPage.tsx index 7994894d..f95c0ec6 100644 --- a/client/src/containers/ResetPasswordPage.tsx +++ b/client/src/containers/ResetPasswordPage.tsx @@ -1,19 +1,17 @@ import React, { useEffect } from "react"; import { useLocation } from "react-router-dom"; -import LegalFooter from "../components/LegalFooter"; - -import Page from "../components/Page"; import { withI18n, withI18nProps } from "@lingui/react"; import { Trans, t } from "@lingui/macro"; +import { Button } from "@justfixnyc/component-library"; import AuthClient, { ResetStatusCode } from "components/AuthClient"; import PasswordInput from "components/PasswordInput"; import { useInput } from "util/helpers"; import { FixedLoadingLabel } from "components/Loader"; import { createWhoOwnsWhatRoutePaths } from "routes"; -import { LocaleLink } from "i18n"; +import { JFCLLocaleLink, LocaleLink } from "i18n"; import SendNewLink from "components/SendNewLink"; -import { Button } from "@justfixnyc/component-library"; +import StandalonePage from "components/StandalonePage"; const ResetPasswordPage = withI18n()((props: withI18nProps) => { const { i18n } = props; @@ -33,23 +31,6 @@ const ResetPasswordPage = withI18n()((props: withI18nProps) => { onChange: onChangePassword, } = useInput(""); - const delaySeconds = 5; - const baseUrl = window.location.origin; - const redirectUrl = `${baseUrl}/${i18n.language}/account/login`; - - const updateCountdown = () => { - let timeLeft = delaySeconds; - const delayInterval = delaySeconds * 100; - - setInterval(() => { - timeLeft && timeLeft--; // prevents counter from going below 0 - document.getElementById("countdown")!.textContent = timeLeft.toString(); - if (timeLeft <= 0) { - document.location.href = redirectUrl; - } - }, delayInterval); - }; - useEffect(() => { const asyncCheckToken = async () => { return await AuthClient.resetPasswordCheck(); @@ -75,16 +56,17 @@ const ResetPasswordPage = withI18n()((props: withI18nProps) => { const expiredPage = () => ( <> + Reset Password {emailIsResent ? ( - Click the link we sent to your email to reset your password. + Click the link we sent to your email to reset your password. ) : ( - The password reset link that we sent you is no longer valid. + The password reset link that we sent you is no longer valid. )} { - setEmailIsResent(await AuthClient.resendVerifyEmail()); + setEmailIsResent(await AuthClient.resetPasswordRequest()); }} /> @@ -93,8 +75,12 @@ const ResetPasswordPage = withI18n()((props: withI18nProps) => { const invalidPage = () => { return ( <> - Sorry, something went wrong with the password reset. - + Reset Password + Sorry, something went wrong with the password reset. + Request new link @@ -103,7 +89,7 @@ const ResetPasswordPage = withI18n()((props: withI18nProps) => { const resetPasswordPage = () => ( <> - Reset your password + Reset Password
{
+ Unsubscribe + + You are signed up for Building Updates for {subscriptionsNumber}{" "} + . Click below to + unsubscribe from all and stop receiving Building Updates. + +
-
); diff --git a/client/src/containers/VerifyEmailPage.tsx b/client/src/containers/VerifyEmailPage.tsx index 153077cc..c18131f8 100644 --- a/client/src/containers/VerifyEmailPage.tsx +++ b/client/src/containers/VerifyEmailPage.tsx @@ -3,10 +3,9 @@ import { withI18n, withI18nProps } from "@lingui/react"; import { Trans, t } from "@lingui/macro"; import { useLocation } from "react-router-dom"; -import "styles/VerifyEmailPage.css"; -import Page from "components/Page"; import AuthClient, { VerifyStatusCode } from "../components/AuthClient"; import SendNewLink from "components/SendNewLink"; +import StandalonePage from "components/StandalonePage"; const VerifyEmailPage = withI18n()((props: withI18nProps) => { const { i18n } = props; @@ -44,33 +43,17 @@ const VerifyEmailPage = withI18n()((props: withI18nProps) => { }); }, []); - const delaySeconds = 5; - const baseUrl = window.location.origin; - const redirectUrl = `${baseUrl}/${i18n.language}`; - - const updateCountdown = () => { - let timeLeft = delaySeconds; - const delayInterval = delaySeconds * 100; - - setInterval(() => { - timeLeft && timeLeft--; // prevents counter from going below 0 - document.getElementById("countdown")!.textContent = timeLeft.toString(); - if (timeLeft <= 0) { - document.location.href = redirectUrl; - } - }, delayInterval); - }; - const expiredLinkPage = () => (
{isEmailResent ? ( - Click the link we sent to your email to start receiving emails. + Click the link we sent to your email to start receiving emails. ) : ( - The verification link that we sent you is no longer valid. + The verification link that we sent you is no longer valid. )} { setIsEmailResent(await AuthClient.resendVerifyEmail(token)); }} @@ -80,56 +63,34 @@ const VerifyEmailPage = withI18n()((props: withI18nProps) => { // TODO add error logging const errorPage = () => ( -
- We’re having trouble verifying your email at this time. -
- + <> + We’re having trouble verifying your email at this time. + Please try again later. If you’re still having issues, contact support@justfix.org. -
+ ); const successPage = () => ( -
- Email address verified -
- You can now start receiving Building Updates -
+ <> + Email address verified + You can now start receiving Building Updates + ); - const alreadyVerifiedPage = () => ( -
- Your email is already verified -
- You will be redirected back to Who Owns What in: -
-
{updateCountdown()}
- - {delaySeconds} seconds - -
-
- If you are not redirected, please click this link: -
- {redirectUrl} -
- ); + const alreadyVerifiedPage = () => Your email is already verified; return ( - -
-
- {!loading && - (isVerified - ? isAlreadyVerified - ? alreadyVerifiedPage() - : successPage() - : isExpired - ? expiredLinkPage() - : unknownError && errorPage())} -
-
-
+ + {!loading && + (isVerified + ? isAlreadyVerified + ? alreadyVerifiedPage() + : successPage() + : isExpired + ? expiredLinkPage() + : unknownError && errorPage())} + ); }); diff --git a/client/src/styles/App.scss b/client/src/styles/App.scss index 179154a3..806237dd 100644 --- a/client/src/styles/App.scss +++ b/client/src/styles/App.scss @@ -165,6 +165,7 @@ align-items: center; .jf-logo { + color: $justfix-white; margin-right: 1.5rem; @include for-phone-only { display: none; diff --git a/client/src/styles/EmailInput.scss b/client/src/styles/EmailInput.scss index d0c1c219..75511386 100644 --- a/client/src/styles/EmailInput.scss +++ b/client/src/styles/EmailInput.scss @@ -8,14 +8,7 @@ justify-content: space-between; label { - @include body-standard; - // vars marked !important bc typography mixin is not available for inconsolata - // font-weight: 600 !important; - font-family: $wow-font !important; - font-size: 0.8125rem !important; - line-height: 120%; - color: $justfix-black; - + @include wow-body-standard; text-align: left; } diff --git a/client/src/styles/Login.scss b/client/src/styles/Login.scss index 6e51523c..b154111c 100644 --- a/client/src/styles/Login.scss +++ b/client/src/styles/Login.scss @@ -1,9 +1,10 @@ @import "_vars.scss"; @import "_typography.scss"; -.Login, -.ForgotPasswordPage, -.ResetPasswordPage { +// Login via Nav styled by StandalonePage + +.EmailAlertSignup .Login, +.ReactModalPortal .Login { display: flex; flex-direction: column; gap: 1.5rem; @@ -51,7 +52,7 @@ } } - .resend-verify-label { + .didnt-get-link { text-align: center; } @@ -59,7 +60,7 @@ justify-content: center; } - .building-page-footer { + .login-footer { display: flex; flex-direction: column; justify-content: center; diff --git a/client/src/styles/Password.scss b/client/src/styles/Password.scss index 0c4d2b65..9e10139a 100644 --- a/client/src/styles/Password.scss +++ b/client/src/styles/Password.scss @@ -8,14 +8,7 @@ justify-content: space-between; label { - @include body-standard; - // vars marked !important bc typography mixin is not available for inconsolata - // font-weight: 600 !important; - font-family: $wow-font !important; - font-size: 0.8125rem !important; - line-height: 120%; - color: $justfix-black; - + @include wow-body-standard; text-align: left; } diff --git a/client/src/styles/StandalonePage.scss b/client/src/styles/StandalonePage.scss new file mode 100644 index 00000000..dd4e1cff --- /dev/null +++ b/client/src/styles/StandalonePage.scss @@ -0,0 +1,130 @@ +@import "_vars.scss"; + +.StandalonePage { + div.page-container { + display: flex; + flex-direction: column; + align-items: center; + flex-grow: 1; + + margin-top: 0; + margin-bottom: 0; + padding: 4rem 0 1.5rem 0; + @include for-phone-only() { + margin: 0; + width: 100%; + padding: 1.5rem 1rem; + } + + .jf-logo { + display: flex; + @include for-phone-only { + margin-top: 1rem; + } + svg { + color: $justfix-black; + height: 1rem; + } + } + + .standalone-container { + display: flex; + flex-direction: column; + background-color: $justfix-white-200; + margin: 2rem 0; + + width: 464px; + padding: 3rem 4rem; + @include for-phone-only() { + width: 100%; + padding: 1.5rem; + } + + .Loader { + background-color: inherit; + } + + h1, + h2, + p { + margin: 0; + text-align: left; + &, + a { + font-weight: 500; + } + } + + h1 { + font-size: 1.5rem; + } + + h2, + p { + &, + a { + font-size: 0.875rem; + } + } + + h1 + h2 { + margin-top: 1rem; + } + + .input-group, + .email-input-field + .password-input-field, + .jfcl-button.jfcl-variant-primary { + margin-top: 1.5rem; + } + + .send-new-link, + .jfcl-button { + width: 100%; + } + + // reset password link to forgot pw, should redesign.. + a.jfcl-button { + text-decoration: none; + } + + .didnt-get-link { + margin-top: 1rem; + } + + .send-new-link-container { + margin-top: 1.5rem; + } + + .standalone-footer, + .login-footer, + .unsubscribe-footer { + display: flex; + flex-direction: column; + gap: 1rem; + text-align: center; + margin-top: 1.5rem; + + &, + a, + .button.is-text { + font-size: 0.875rem; + font-weight: 500; + color: #464d5d; // TODO: this isn't in design system, confirm it's the right one to use and add + } + + .button.is-text { + display: inline; + } + } + } + + .wow-footer { + margin-top: auto; + text-align: center; + &, + a { + font-size: 0.875rem; + } + } + } +} diff --git a/client/src/styles/UnsubscribePage.scss b/client/src/styles/UnsubscribePage.scss index e4034ec2..7796b05f 100644 --- a/client/src/styles/UnsubscribePage.scss +++ b/client/src/styles/UnsubscribePage.scss @@ -1,10 +1,63 @@ +@import "_vars.scss"; + .UnsubscribePage { - .unsubscribe-container { + div.page-container { display: flex; flex-direction: column; align-items: center; + flex-grow: 1; + margin-top: 4rem; + margin-bottom: 1.5rem; + + @include for-phone-only() { + margin: 1.5rem 1rem; + } } + + h1, + h2 { + margin: 0; + align-self: start; + &, + a { + font-weight: 500; + } + } + + h1 { + font-size: 1.5rem; + } + + h2 { + &, + a { + font-size: 0.875rem; + } + } + + h1 + h2 { + margin-top: 1rem; + } + + .jfcl-button.jfcl-variant-primary { + align-self: start; + margin-top: 1.5rem; + } + + .subscriptions-container { + width: 100%; + margin-top: 3rem; + } + p.settings-section { margin-bottom: 2rem; } + + .wow-footer { + margin-top: auto; + &, + a { + font-size: 0.875rem; + } + } } diff --git a/jfauthprovider/views.py b/jfauthprovider/views.py index a0c1a4f7..212bf050 100644 --- a/jfauthprovider/views.py +++ b/jfauthprovider/views.py @@ -166,7 +166,6 @@ def reset_password_request(request): @api def reset_password_request_with_token(request): try: - print(request.GET.get("token")) post_data = { "token": request.GET.get("token"), "origin": request.headers["Origin"], @@ -176,6 +175,7 @@ def reset_password_request_with_token(request): "POST", "user/email/password_reset/request/", post_data, + {"Cookie": request.headers.get("Cookie")}, ) except KeyError: return HttpResponse(content_type="application/json", status=401)