diff --git a/cypress/e2e/alerts.cy.ts b/cypress/e2e/alerts.cy.ts index b4e666af14f..b9881ded5c4 100644 --- a/cypress/e2e/alerts.cy.ts +++ b/cypress/e2e/alerts.cy.ts @@ -2,7 +2,7 @@ describe("user/alerts", () => { describe("unauthenticated", () => { it("redirects to the login page", () => { cy.visit("user/alerts") - cy.contains("Log in") + cy.contains("Sign up or log in") }) }) }) diff --git a/cypress/e2e/notifications.cy.ts b/cypress/e2e/notifications.cy.ts index 66d4117d929..e10b11e966d 100644 --- a/cypress/e2e/notifications.cy.ts +++ b/cypress/e2e/notifications.cy.ts @@ -4,7 +4,7 @@ describe("/notifications", () => { describe("unauthenticated", () => { it("redirects to the login page", () => { cy.visit("/notifications") - cy.contains("Log in") + cy.contains("Sign up or log in") }) }) }) diff --git a/cypress/e2e/userPayments.cy.js b/cypress/e2e/userPayments.cy.js index eb70bd91025..73f853e665a 100644 --- a/cypress/e2e/userPayments.cy.js +++ b/cypress/e2e/userPayments.cy.js @@ -2,7 +2,7 @@ describe("user/payments", () => { describe("unauthenticated", () => { it("redirects to the login page", () => { cy.visit("user/payments") - cy.contains("Log in") + cy.contains("Sign up or log in") }) }) }) diff --git a/cypress/e2e/userShipping.cy.js b/cypress/e2e/userShipping.cy.js index 5448657bbe9..4a1d893a42a 100644 --- a/cypress/e2e/userShipping.cy.js +++ b/cypress/e2e/userShipping.cy.js @@ -2,7 +2,7 @@ describe("user/shipping", () => { describe("unauthenticated", () => { it("redirects to the login page", () => { cy.visit("user/shipping") - cy.contains("Log in") + cy.contains("Sign up or log in") }) }) }) diff --git a/docs/authentication.md b/docs/authentication.md index f5d9894b85a..b1cb00c0ff0 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -41,7 +41,6 @@ const YourComponent = () => { ) } else if (shouldPromptIdVerification) { + // eslint-disable-next-line react/no-unstable-nested-components PreviewAction = () => ( ) } else { + // eslint-disable-next-line react/no-unstable-nested-components PreviewAction = () => ( - + ) } diff --git a/src/Components/AuthDialog/Hooks/useCountryCode.ts b/src/Components/AuthDialog/Hooks/useCountryCode.ts index 46014144369..5fb6d0c83fe 100644 --- a/src/Components/AuthDialog/Hooks/useCountryCode.ts +++ b/src/Components/AuthDialog/Hooks/useCountryCode.ts @@ -27,9 +27,47 @@ export const useCountryCode = () => { skip: isLoggedIn, }) + const countryCode = data?.requestLocation?.countryCode + + const isAutomaticallySubscribed = !!( + countryCode && !GDPR_COUNTRY_CODES.includes(countryCode) + ) + return { - countryCode: data?.requestLocation?.countryCode, - loading, + countryCode, error, + isAutomaticallySubscribed, + loading, } } + +export const GDPR_COUNTRY_CODES = [ + "AT", + "BE", + "BG", + "CY", + "CZ", + "DE", + "DK", + "EE", + "ES", + "FI", + "FR", + "GB", + "GR", + "HR", + "HU", + "IE", + "IT", + "LT", + "LU", + "LV", + "MT", + "NL", + "PL", + "PT", + "RO", + "SE", + "SI", + "SK", +] diff --git a/src/Components/AuthDialog/README.md b/src/Components/AuthDialog/README.md index 22a134760c3..2c2457f2b8c 100644 --- a/src/Components/AuthDialog/README.md +++ b/src/Components/AuthDialog/README.md @@ -14,9 +14,8 @@ const YourComponent = () => { @@ -101,7 +101,7 @@ export const AuthDialogForgotPassword: FC = () => { textDecoration="underline" data-test="login" onClick={() => { - dispatch({ type: "MODE", payload: { mode: "Login" } }) + dispatch({ type: "MODE", payload: { mode: "Welcome" } }) }} > Log in @@ -111,7 +111,7 @@ export const AuthDialogForgotPassword: FC = () => { textDecoration="underline" data-test="signup" onClick={() => { - dispatch({ type: "MODE", payload: { mode: "SignUp" } }) + dispatch({ type: "MODE", payload: { mode: "Welcome" } }) }} > sign up. diff --git a/src/Components/AuthDialog/Views/AuthDialogLogin.tsx b/src/Components/AuthDialog/Views/AuthDialogLogin.tsx index 9c08e104d1d..04230711595 100644 --- a/src/Components/AuthDialog/Views/AuthDialogLogin.tsx +++ b/src/Components/AuthDialog/Views/AuthDialogLogin.tsx @@ -9,10 +9,10 @@ import { Message, PasswordInput, Spacer, + Stack, Text, } from "@artsy/palette" import { useAuthDialogContext } from "Components/AuthDialog/AuthDialogContext" -import { AuthDialogSocial } from "Components/AuthDialog/Components/AuthDialogSocial" import { Form, Formik } from "formik" import { login } from "Utils/auth" import { useAfterAuthentication } from "Components/AuthDialog/Hooks/useAfterAuthentication" @@ -20,10 +20,7 @@ import { formatErrorMessage } from "Components/AuthDialog/Utils/formatErrorMessa import { useAuthDialogTracking } from "Components/AuthDialog/Hooks/useAuthDialogTracking" export const AuthDialogLogin: FC = () => { - const { - dispatch, - state: { options }, - } = useAuthDialogContext() + const { dispatch, state } = useAuthDialogContext() const { runAfterAuthentication } = useAfterAuthentication() @@ -34,7 +31,7 @@ export const AuthDialogLogin: FC = () => { validateOnBlur={false} validationSchema={VALIDATION_SCHEMA} initialValues={{ - email: "", + email: state.values.email || "", password: "", authenticationCode: "", mode: "Pending", @@ -55,7 +52,7 @@ export const AuthDialogLogin: FC = () => { track.loggedIn({ service: "email", userId: user.id }) - options.onSuccess?.() + state.options.onSuccess?.() } catch (err) { console.error(err) @@ -97,20 +94,7 @@ export const AuthDialogLogin: FC = () => { }) => { return (
- }> - - + }> { onChange={handleChange} onBlur={handleBlur} autoComplete="current-password" + autoFocus error={touched.password && errors.password} /> @@ -168,33 +153,33 @@ export const AuthDialogLogin: FC = () => { {status.error} )} - - - - or - - - - - - Don’t have an account?{" "} - { - dispatch({ type: "MODE", payload: { mode: "SignUp" } }) - }} + + + + {state.isFallback && ( + + Don’t have an account?{" "} + { + dispatch({ type: "MODE", payload: { mode: "SignUp" } }) + }} + > + Sign up. + + + )} + ) diff --git a/src/Components/AuthDialog/Views/AuthDialogSignUp.tsx b/src/Components/AuthDialog/Views/AuthDialogSignUp.tsx index 5ae3b1c6eba..be2b3bf28d3 100644 --- a/src/Components/AuthDialog/Views/AuthDialogSignUp.tsx +++ b/src/Components/AuthDialog/Views/AuthDialogSignUp.tsx @@ -5,43 +5,34 @@ import { Checkbox, Clickable, Input, - Join, Message, PasswordInput, Spacer, + Stack, Text, } from "@artsy/palette" import { useAuthDialogContext } from "Components/AuthDialog/AuthDialogContext" -import { AuthDialogSocial } from "Components/AuthDialog/Components/AuthDialogSocial" import { Form, Formik } from "formik" import { FC } from "react" import { signUp } from "Utils/auth" import { useAfterAuthentication } from "Components/AuthDialog/Hooks/useAfterAuthentication" import { formatErrorMessage } from "Components/AuthDialog/Utils/formatErrorMessage" -import { isTouch } from "Utils/device" import { useAuthDialogTracking } from "Components/AuthDialog/Hooks/useAuthDialogTracking" import { AuthDialogSignUpPlaceholder } from "Components/AuthDialog/Components/AuthDialogSignUpPlaceholder" import { useCountryCode } from "Components/AuthDialog/Hooks/useCountryCode" -import { RouterLink } from "System/Components/RouterLink" -import { useFeatureFlag } from "System/Hooks/useFeatureFlag" +import { AuthDialogDisclaimer } from "Components/AuthDialog/Views/AuthDialogDisclaimer" export const AuthDialogSignUp: FC = () => { const { dispatch, - state: { options }, + state: { options, values, isFallback }, } = useAuthDialogContext() const { runAfterAuthentication } = useAfterAuthentication() const track = useAuthDialogTracking() - const { loading, countryCode } = useCountryCode() - - const showNewDisclaimer = useFeatureFlag("diamond_new-terms-and-conditions") - - const isAutomaticallySubscribed = !!( - countryCode && !GDPR_COUNTRY_CODES.includes(countryCode) - ) + const { loading, isAutomaticallySubscribed } = useCountryCode() if (loading) { return @@ -52,6 +43,7 @@ export const AuthDialogSignUp: FC = () => { validateOnBlur={false} initialValues={{ ...INITIAL_VALUES, + email: values.email || "", agreedToReceiveEmails: isAutomaticallySubscribed, }} validationSchema={VALIDATION_SCHEMA} @@ -80,8 +72,12 @@ export const AuthDialogSignUp: FC = () => { } catch (err) { console.error(err) + const message = + formatErrorMessage(err) || + "Something went wrong. Please try again or contact support@artsy.net." + setFieldValue("mode", "Error") - setStatus({ error: formatErrorMessage(err) }) + setStatus({ error: message }) } }} > @@ -98,50 +94,40 @@ export const AuthDialogSignUp: FC = () => { }) => { return (
- }> - - - - - - + + - - - - Password must be at least 8 characters and include a lowercase - letter, uppercase letter, and digit. - - + + + + + + + Password must be at least 8 characters and include a + lowercase letter, uppercase letter, and digit. + + + {!isAutomaticallySubscribed && ( { }} > - Dive deeper into the art market with Artsy emails. Subscribe - to hear about our products, services, editorials, and other - promotional content. Unsubscribe at any time. + Subscribe to email to hear about our products, services, + editorials, and other promotional content. Unsubscribe at + any time. )} @@ -162,78 +148,36 @@ export const AuthDialogSignUp: FC = () => { {status.error} )} - - - - or - - - - - - Already have an account?{" "} - { - dispatch({ type: "MODE", payload: { mode: "Login" } }) - }} + + + + {isFallback && ( + + Already have an account?{" "} + { + dispatch({ type: "MODE", payload: { mode: "Login" } }) + }} + > + Log in. + + )} - . - + - - This site is protected by reCAPTCHA and the{" "} - - Google Privacy Policy - {" "} - and{" "} - - Terms of Service - {" "} - apply. - - + + ) }} @@ -264,34 +208,3 @@ const VALIDATION_SCHEMA = Yup.object().shape({ .required("Please enter a valid email."), password: passwordValidator, }) - -const GDPR_COUNTRY_CODES = [ - "AT", - "BE", - "BG", - "CY", - "CZ", - "DE", - "DK", - "EE", - "ES", - "FI", - "FR", - "GB", - "GR", - "HR", - "HU", - "IE", - "IT", - "LT", - "LU", - "LV", - "MT", - "NL", - "PL", - "PT", - "RO", - "SE", - "SI", - "SK", -] diff --git a/src/Components/AuthDialog/Views/AuthDialogWelcome.tsx b/src/Components/AuthDialog/Views/AuthDialogWelcome.tsx new file mode 100644 index 00000000000..575a101ca4a --- /dev/null +++ b/src/Components/AuthDialog/Views/AuthDialogWelcome.tsx @@ -0,0 +1,139 @@ +import * as Yup from "yup" +import { Button, Input, Stack, Text } from "@artsy/palette" +import { FC } from "react" +import { Form, Formik } from "formik" +import { AuthDialogSocial } from "Components/AuthDialog/Components/AuthDialogSocial" +import { AuthDialogDisclaimer } from "Components/AuthDialog/Views/AuthDialogDisclaimer" +import { fetchQuery, graphql } from "react-relay" +import { AuthDialogWelcomeQuery } from "__generated__/AuthDialogWelcomeQuery.graphql" +import { useSystemContext } from "System/Hooks/useSystemContext" +import { + DEFAULT_AUTH_MODAL_INTENTS, + useAuthDialogContext, +} from "Components/AuthDialog/AuthDialogContext" +import { recaptcha } from "Utils/recaptcha" + +interface AuthDialogWelcomeProps {} + +export const AuthDialogWelcome: FC = () => { + const { relayEnvironment } = useSystemContext() + + const { dispatch, state } = useAuthDialogContext() + + return ( + { + try { + const recaptchaToken = await recaptcha("verify_user") + + const res = await fetchQuery( + relayEnvironment, + QUERY, + { email, recaptchaToken: recaptchaToken ?? "" } + ).toPromise() + + const exists = !!res?.verifyUser?.exists + const mode = exists ? "Login" : "SignUp" + + dispatch({ + type: "SET", + payload: { + values: { email }, + analytics: { + ...state.analytics, + intent: DEFAULT_AUTH_MODAL_INTENTS[mode], + }, + }, + }) + + dispatch({ + type: "MODE", + payload: { mode }, + }) + } catch (error) { + console.error(error) + + dispatch({ + type: "SET", + payload: { + values: { email }, + analytics: { + ...state.analytics, + intent: DEFAULT_AUTH_MODAL_INTENTS.SignUp, + }, + }, + }) + + dispatch({ type: "FALLBACK" }) + } + }} + > + {({ + dirty, + errors, + handleBlur, + handleChange, + isSubmitting, + isValid, + touched, + values, + }) => { + return ( +
+ + + + + + + + Or continue with + + + + + + + +
+ ) + }} +
+ ) +} + +const VALIDATION_SCHEMA = Yup.object().shape({ + email: Yup.string() + .email("Please enter a valid email.") + .required("Email required."), +}) + +const QUERY = graphql` + query AuthDialogWelcomeQuery($email: String!, $recaptchaToken: String!) { + verifyUser(email: $email, recaptchaToken: $recaptchaToken) { + exists + } + } +` diff --git a/src/Components/AuthDialog/Views/__tests__/AuthDialogForgotPassword.jest.tsx b/src/Components/AuthDialog/Views/__tests__/AuthDialogForgotPassword.jest.tsx index e091f3971a4..d130a55d5d0 100644 --- a/src/Components/AuthDialog/Views/__tests__/AuthDialogForgotPassword.jest.tsx +++ b/src/Components/AuthDialog/Views/__tests__/AuthDialogForgotPassword.jest.tsx @@ -36,8 +36,6 @@ describe("AuthDialogForgotPassword", () => { // eslint-disable-next-line testing-library/no-node-access const button = submit.parentElement! - expect(button).toBeDisabled() - fireEvent.change(input, { target: { value: "example@example.com" } }) expect(button).toBeEnabled() diff --git a/src/Components/AuthDialog/Views/__tests__/AuthDialogLogin.jest.tsx b/src/Components/AuthDialog/Views/__tests__/AuthDialogLogin.jest.tsx index 1285e2d5d34..119d3e55575 100644 --- a/src/Components/AuthDialog/Views/__tests__/AuthDialogLogin.jest.tsx +++ b/src/Components/AuthDialog/Views/__tests__/AuthDialogLogin.jest.tsx @@ -19,6 +19,18 @@ jest.mock("Utils/auth", () => ({ login: jest.fn(), })) +jest.mock("Components/AuthDialog/AuthDialogContext", () => ({ + useAuthDialogContext: jest.fn().mockReturnValue({ + state: { + analytics: {}, + options: {}, + values: { + email: "example@example.com", + }, + }, + }), +})) + describe("AuthDialogLogin", () => { it("renders correctly", () => { render() @@ -26,21 +38,12 @@ describe("AuthDialogLogin", () => { expect(screen.getByText("Log in")).toBeInTheDocument() }) - it("renders the social auth buttons", () => { - render() - - expect(screen.getByText("Continue with Facebook")).toBeInTheDocument() - expect(screen.getByText("Continue with Google")).toBeInTheDocument() - expect(screen.getByText("Continue with Apple")).toBeInTheDocument() - }) - - it("submits the email and password", async () => { + it("submits the password", async () => { render() const loginMock = jest.fn().mockReturnValue(Promise.resolve()) ;(login as jest.Mock).mockImplementationOnce(loginMock) - const email = screen.getByPlaceholderText("Enter your email address") const password = screen.getByPlaceholderText("Enter your password") const submit = screen.getByText("Log in") @@ -49,7 +52,6 @@ describe("AuthDialogLogin", () => { expect(button).toBeDisabled() - fireEvent.change(email, { target: { value: "example@example.com" } }) fireEvent.change(password, { target: { value: "secret" } }) expect(button).toBeEnabled() @@ -75,7 +77,6 @@ describe("AuthDialogLogin", () => { ) ;(login as jest.Mock).mockImplementationOnce(loginMock) - const email = screen.getByPlaceholderText("Enter your email address") const password = screen.getByPlaceholderText("Enter your password") const submit = screen.getByText("Log in") @@ -84,7 +85,6 @@ describe("AuthDialogLogin", () => { expect(button).toBeDisabled() - fireEvent.change(email, { target: { value: "example@example.com" } }) fireEvent.change(password, { target: { value: "secret" } }) fireEvent.click(button) @@ -125,7 +125,6 @@ describe("AuthDialogLogin", () => { ) ;(login as jest.Mock).mockImplementationOnce(loginMock) - const email = screen.getByPlaceholderText("Enter your email address") const password = screen.getByPlaceholderText("Enter your password") const submit = screen.getByText("Log in") @@ -134,7 +133,6 @@ describe("AuthDialogLogin", () => { expect(button).toBeDisabled() - fireEvent.change(email, { target: { value: "example@example.com" } }) fireEvent.change(password, { target: { value: "secret" } }) fireEvent.click(button) diff --git a/src/Components/AuthDialog/Views/__tests__/AuthDialogSignUp.jest.tsx b/src/Components/AuthDialog/Views/__tests__/AuthDialogSignUp.jest.tsx index c6a18256b30..a6d036d27f0 100644 --- a/src/Components/AuthDialog/Views/__tests__/AuthDialogSignUp.jest.tsx +++ b/src/Components/AuthDialog/Views/__tests__/AuthDialogSignUp.jest.tsx @@ -1,7 +1,6 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react" import { useCountryCode } from "Components/AuthDialog/Hooks/useCountryCode" import { AuthDialogSignUp } from "Components/AuthDialog/Views/AuthDialogSignUp" -import { useFeatureFlag } from "System/Hooks/useFeatureFlag" import { signUp } from "Utils/auth" jest.mock("Utils/getENV", () => ({ @@ -21,6 +20,18 @@ jest.mock("Utils/auth", () => ({ signUp: jest.fn(), })) +jest.mock("Components/AuthDialog/AuthDialogContext", () => ({ + useAuthDialogContext: jest.fn().mockReturnValue({ + state: { + analytics: {}, + options: {}, + values: { + email: "example@example.com", + }, + }, + }), +})) + // mocks the module that tells us if the user is on a touch device let mockIsTouch = false jest.mock("Utils/device", () => ({ @@ -30,13 +41,14 @@ jest.mock("Utils/device", () => ({ })) jest.mock("Components/AuthDialog/Hooks/useCountryCode") + jest.mock("System/Hooks/useFeatureFlag") describe("AuthDialogSignUp", () => { beforeAll(() => { ;(useCountryCode as jest.Mock).mockImplementation(() => ({ loading: false, - countryCode: "US", + isAutomaticallySubscribed: true, })) }) @@ -46,24 +58,15 @@ describe("AuthDialogSignUp", () => { expect(screen.getByText("Sign up")).toBeInTheDocument() }) - it("renders the social auth buttons", () => { - render() - - expect(screen.getByText("Continue with Facebook")).toBeInTheDocument() - expect(screen.getByText("Continue with Google")).toBeInTheDocument() - expect(screen.getByText("Continue with Apple")).toBeInTheDocument() - }) - it("renders a disclaimer", () => { render() expect(screen.getByTestId("disclaimer")).toHaveTextContent( - "By clicking Sign Up or Continue with Apple, Google, or Facebook, you agree to Artsy’s Terms of Use and Privacy Policy and to receiving emails from Artsy." - ) - expect(screen.getByRole("link", { name: "Terms of Use" })).toHaveAttribute( - "href", - "/terms" + "By clicking Sign Up or Continue with Email, Apple, Google, or Facebook, you agree to Artsy’s Terms and Conditions and Privacy Policy and to receiving emails from Artsy." ) + expect( + screen.getByRole("link", { name: "Terms and Conditions" }) + ).toHaveAttribute("href", "/terms") expect( screen.getByRole("link", { name: "Privacy Policy" }) ).toHaveAttribute("href", "/privacy") @@ -82,7 +85,7 @@ describe("AuthDialogSignUp", () => { render() expect(screen.getByTestId("disclaimer")).toHaveTextContent( - "By tapping Sign Up or Continue with Apple, Google, or Facebook, you agree to Artsy’s Terms of Use and Privacy Policy and to receiving emails from Artsy." + "By tapping Sign Up or Continue with Email, Apple, Google, or Facebook, you agree to Artsy’s Terms and Conditions and Privacy Policy and to receiving emails from Artsy." ) }) }) @@ -91,14 +94,14 @@ describe("AuthDialogSignUp", () => { beforeAll(() => { ;(useCountryCode as jest.Mock).mockImplementation(() => ({ loading: false, - countryCode: "DE", + isAutomaticallySubscribed: false, })) }) afterAll(() => { ;(useCountryCode as jest.Mock).mockImplementation(() => ({ loading: false, - countryCode: "US", + isAutomaticallySubscribed: true, })) }) @@ -106,7 +109,7 @@ describe("AuthDialogSignUp", () => { render() expect(screen.getByTestId("disclaimer")).toHaveTextContent( - "By clicking Sign Up or Continue with Apple, Google, or Facebook, you agree to Artsy’s Terms of Use and Privacy Policy." + "By clicking Sign Up or Continue with Email, Apple, Google, or Facebook, you agree to Artsy’s Terms and Conditions and Privacy Policy." ) }) }) @@ -115,14 +118,14 @@ describe("AuthDialogSignUp", () => { beforeAll(() => { ;(useCountryCode as jest.Mock).mockImplementation(() => ({ loading: true, - countryCode: "US", + isAutomaticallySubscribed: true, })) }) afterAll(() => { ;(useCountryCode as jest.Mock).mockImplementation(() => ({ loading: false, - countryCode: "US", + isAutomaticallySubscribed: true, })) }) @@ -130,54 +133,8 @@ describe("AuthDialogSignUp", () => { render() expect(screen.getByTestId("skeleton-disclaimer")).toHaveTextContent( - "By clicking Sign Up or Continue with Apple, Google, or Facebook, you agree to Artsy’s Terms of Use and Privacy Policy and to receiving emails from Artsy." - ) - }) - - describe("when the new disclaimer is enabled", () => { - beforeAll(() => { - ;(useFeatureFlag as jest.Mock).mockImplementation( - (f: string) => f === "diamond_new-terms-and-conditions" - ) - }) - - afterAll(() => { - ;(useFeatureFlag as jest.Mock).mockReset() - }) - - it("renders a disclaimer with the new text", () => { - render() - - expect(screen.getByTestId("skeleton-disclaimer")).toHaveTextContent( - "By clicking Sign Up or Continue with Email, Apple, Google, or Facebook, you agree to Artsy’s Terms and Conditions and Privacy Policy and to receiving emails from Artsy." - ) - }) - }) - }) - - describe("when the new disclaimer is enabled", () => { - beforeAll(() => { - ;(useFeatureFlag as jest.Mock).mockImplementation( - (f: string) => f === "diamond_new-terms-and-conditions" - ) - }) - - afterAll(() => { - ;(useFeatureFlag as jest.Mock).mockReset() - }) - - it("renders a disclaimer with the new text", () => { - render() - - expect(screen.getByTestId("disclaimer")).toHaveTextContent( "By clicking Sign Up or Continue with Email, Apple, Google, or Facebook, you agree to Artsy’s Terms and Conditions and Privacy Policy and to receiving emails from Artsy." ) - expect( - screen.getByRole("link", { name: "Terms and Conditions" }) - ).toHaveAttribute("href", "/terms") - expect( - screen.getByRole("link", { name: "Privacy Policy" }) - ).toHaveAttribute("href", "/privacy") }) }) @@ -188,7 +145,6 @@ describe("AuthDialogSignUp", () => { ;(signUp as jest.Mock).mockImplementationOnce(signUpMock) const name = screen.getByPlaceholderText("Enter your full name") - const email = screen.getByPlaceholderText("Enter your email address") const password = screen.getByPlaceholderText("Enter your password") const submit = screen.getByText("Sign up") @@ -199,7 +155,6 @@ describe("AuthDialogSignUp", () => { expect(button).toBeDisabled() fireEvent.change(name, { target: { value: "Test User" } }) - fireEvent.change(email, { target: { value: "example@example.com" } }) fireEvent.change(password, { target: { value: "Secret000" } }) // pragma: allowlist secret expect(button).toBeEnabled() diff --git a/src/Components/FollowButton/FollowArtistButton.tsx b/src/Components/FollowButton/FollowArtistButton.tsx index 2a281090d70..cca17feb0e1 100644 --- a/src/Components/FollowButton/FollowArtistButton.tsx +++ b/src/Components/FollowButton/FollowArtistButton.tsx @@ -126,12 +126,8 @@ const FollowArtistButton: React.FC = ({ if (!isLoggedIn) { showAuthDialog({ - mode: "SignUp", options: { - title: mode => { - const action = mode === "SignUp" ? "Sign up" : "Log in" - return `${action} to follow ${artist.name}` - }, + title: `Sign up or log in to follow ${artist.name}`, afterAuthAction: { action: "follow", kind: "artist", diff --git a/src/Components/FollowButton/FollowGeneButton.tsx b/src/Components/FollowButton/FollowGeneButton.tsx index f22c90750d7..f00fe8cfdd4 100644 --- a/src/Components/FollowButton/FollowGeneButton.tsx +++ b/src/Components/FollowButton/FollowGeneButton.tsx @@ -68,12 +68,8 @@ const FollowGeneButton: React.FC = ({ if (!isLoggedIn) { showAuthDialog({ - mode: "SignUp", options: { - title: mode => { - const action = mode === "SignUp" ? "Sign up" : "Log in" - return `${action} to follow ${gene.name}` - }, + title: `Sign up or log in to follow ${gene.name}`, afterAuthAction: { action: "follow", kind: "gene", diff --git a/src/Components/FollowButton/FollowProfileButton.tsx b/src/Components/FollowButton/FollowProfileButton.tsx index c563a0a6f8a..fda8f59b522 100644 --- a/src/Components/FollowButton/FollowProfileButton.tsx +++ b/src/Components/FollowButton/FollowProfileButton.tsx @@ -92,12 +92,8 @@ const FollowProfileButton: React.FC = ({ if (!isLoggedIn) { showAuthDialog({ - mode: "SignUp", options: { - title: mode => { - const action = mode === "SignUp" ? "Sign up" : "Log in" - return `${action} to follow ${profile.name}` - }, + title: `Sign up or log in to follow ${profile.name}`, afterAuthAction: { action: "follow", kind: "profile", diff --git a/src/Components/FollowButton/__tests__/FollowArtistButton.jest.tsx b/src/Components/FollowButton/__tests__/FollowArtistButton.jest.tsx index 60f87855fc2..c5f5b4c6c6b 100644 --- a/src/Components/FollowButton/__tests__/FollowArtistButton.jest.tsx +++ b/src/Components/FollowButton/__tests__/FollowArtistButton.jest.tsx @@ -88,14 +88,13 @@ describe("FollowArtistButton", () => { expect(showAuthDialog).toBeCalledWith({ analytics: { contextModule: "artistHeader", intent: "followArtist" }, - mode: "SignUp", options: { afterAuthAction: { action: "follow", kind: "artist", objectId: "example", }, - title: expect.any(Function), + title: expect.any(String), }, }) }) diff --git a/src/Components/FollowButton/__tests__/FollowGeneButton.jest.tsx b/src/Components/FollowButton/__tests__/FollowGeneButton.jest.tsx index 272cd84a654..d4626dabd69 100644 --- a/src/Components/FollowButton/__tests__/FollowGeneButton.jest.tsx +++ b/src/Components/FollowButton/__tests__/FollowGeneButton.jest.tsx @@ -84,14 +84,13 @@ describe("FollowGeneButton", () => { expect(showAuthDialog).toBeCalledWith({ analytics: { contextModule: "geneHeader", intent: "followGene" }, - mode: "SignUp", options: { afterAuthAction: { action: "follow", kind: "gene", objectId: "example", }, - title: expect.any(Function), + title: expect.any(String), }, }) }) diff --git a/src/Components/FollowButton/__tests__/FollowProfileButton.jest.tsx b/src/Components/FollowButton/__tests__/FollowProfileButton.jest.tsx index 59f03f54578..a9d76498d5a 100644 --- a/src/Components/FollowButton/__tests__/FollowProfileButton.jest.tsx +++ b/src/Components/FollowButton/__tests__/FollowProfileButton.jest.tsx @@ -93,14 +93,13 @@ describe("FollowProfileButton", () => { contextModule: "partnerHeader", intent: "followPartner", }, - mode: "SignUp", options: { afterAuthAction: { action: "follow", kind: "profile", objectId: "example", }, - title: expect.any(Function), + title: expect.any(String), }, }) }) diff --git a/src/Components/NavBar/NavBarLoggedOutActions.tsx b/src/Components/NavBar/NavBarLoggedOutActions.tsx index a9c064801dc..2eec1814e90 100644 --- a/src/Components/NavBar/NavBarLoggedOutActions.tsx +++ b/src/Components/NavBar/NavBarLoggedOutActions.tsx @@ -15,7 +15,6 @@ export const NavBarLoggedOutActions = () => { size="small" onClick={() => { showAuthDialog({ - mode: "Login", analytics: { contextModule: ContextModule.header, intent: Intent.login, @@ -32,7 +31,6 @@ export const NavBarLoggedOutActions = () => { size="small" onClick={() => { showAuthDialog({ - mode: "SignUp", analytics: { contextModule: ContextModule.header, intent: Intent.signup, diff --git a/src/Components/NavBar/__tests__/NavBar.jest.tsx b/src/Components/NavBar/__tests__/NavBar.jest.tsx index cfc72c56b76..df4bf59b09b 100644 --- a/src/Components/NavBar/__tests__/NavBar.jest.tsx +++ b/src/Components/NavBar/__tests__/NavBar.jest.tsx @@ -119,7 +119,6 @@ describe("NavBar", () => { wrapper.find("button").at(0).simulate("click") expect(showAuthDialog).toBeCalledWith({ - mode: "Login", analytics: { contextModule: "header", intent: "login", @@ -136,7 +135,6 @@ describe("NavBar", () => { wrapper.find("button").at(1).simulate("click") expect(showAuthDialog).toBeCalledWith({ - mode: "SignUp", analytics: { contextModule: "header", intent: "signup", diff --git a/src/Utils/Hooks/useScrollToOpenArtistAuthModal.ts b/src/Utils/Hooks/useScrollToOpenArtistAuthModal.ts index cdec91f197c..b009ebcff6b 100644 --- a/src/Utils/Hooks/useScrollToOpenArtistAuthModal.ts +++ b/src/Utils/Hooks/useScrollToOpenArtistAuthModal.ts @@ -31,17 +31,13 @@ export const useScrollToOpenArtistAuthModal = ({ const handleScroll = () => { timeout = setTimeout(() => { showAuthDialog({ - mode: "SignUp", options: { image: true, onClose: dismiss, onSuccess: dismiss, - title: mode => { - const action = mode === "SignUp" ? "Sign up" : "Log in" - return `${action} to discover new works by ${ - name || "this artist" - } and more artists you love` - }, + title: `Sign up or log in to discover new works by ${ + name || "this artist" + } and more artists you love`, }, analytics: { contextModule: ContextModule.popUpModal, diff --git a/src/Utils/Hooks/useScrollToOpenEditorialAuthModal.ts b/src/Utils/Hooks/useScrollToOpenEditorialAuthModal.ts index 6fd2c4b27f2..2e8a1fbf22d 100644 --- a/src/Utils/Hooks/useScrollToOpenEditorialAuthModal.ts +++ b/src/Utils/Hooks/useScrollToOpenEditorialAuthModal.ts @@ -5,12 +5,8 @@ export const useScrollToOpenEditorialAuthModal = () => { useScrollToOpenAuthModal({ key: "editorial-signup-dismissed", options: { - mode: "SignUp", options: { - title: mode => { - const action = mode === "SignUp" ? "Sign up" : "Log in" - return `${action} for the latest in art market news` - }, + title: `Sign up or log in for the latest in art market news`, }, analytics: { contextModule: ContextModule.popUpModal, diff --git a/src/Utils/__tests__/auth.jest.ts b/src/Utils/__tests__/auth.jest.ts index 938c56cf381..bdfb374d9ce 100644 --- a/src/Utils/__tests__/auth.jest.ts +++ b/src/Utils/__tests__/auth.jest.ts @@ -20,6 +20,12 @@ jest.mock("sharify", () => ({ }, })) +jest.mock("Utils/recaptcha", () => ({ + recaptcha: jest + .fn() + .mockReturnValue(Promise.resolve("EXAMPLE_RECAPTCHA_KEY")), +})) + describe("login", () => { it("makes the correct request", async () => { const mockFetch = jest.fn(() => @@ -162,7 +168,7 @@ describe("signUp", () => { expect(mockFetch).toBeCalledWith("https://www.artsy.net/signup", { body: - '{"agreed_to_receive_emails":true,"accepted_terms_of_service":true,"email":"example@example.com","name":"Example Example","password":"secret","session_id":"session_id"}', + '{"agreed_to_receive_emails":true,"accepted_terms_of_service":true,"email":"example@example.com","name":"Example Example","password":"secret","recaptcha_token":"EXAMPLE_RECAPTCHA_KEY","session_id":"session_id"}', credentials: "same-origin", headers: { Accept: "application/json", diff --git a/src/Utils/__tests__/recaptcha.jest.ts b/src/Utils/__tests__/recaptcha.jest.ts index a3e0824d269..96f01bd0da7 100644 --- a/src/Utils/__tests__/recaptcha.jest.ts +++ b/src/Utils/__tests__/recaptcha.jest.ts @@ -1,12 +1,14 @@ -import { recaptcha } from "../recaptcha" -jest.mock("sharify", () => ({ data: jest.fn() })) -const sd = require("sharify").data +import { getENV } from "Utils/getENV" +import { recaptcha } from "Utils/recaptcha" + +jest.mock("Utils/getENV", () => ({ + getENV: jest.fn(() => "recaptcha-api-key"), +})) describe("repcaptcha", () => { beforeEach(() => { window.grecaptcha.execute.mockClear() window.grecaptcha.ready.mockClear() - sd.RECAPTCHA_KEY = "recaptcha-api-key" }) it("fires an action", () => { @@ -16,45 +18,30 @@ describe("repcaptcha", () => { }) }) - it("fires an action with callback", done => { + it("fires an action with callback", async () => { const action = jest.fn() - const callback = jest.fn(token => { - action(token) - expect(action).toBeCalledWith("recaptcha-token") - done() - }) - recaptcha("signup_submit", callback) - expect(window.grecaptcha.execute).toBeCalledWith("recaptcha-api-key", { - action: "signup_submit", - }) - }) + const token = await recaptcha("signup_submit") - it("Still calls the callback if firing action fails", done => { - window.grecaptcha.execute.mockRejectedValueOnce(new Error("google failed")) - const action = jest.fn() - const callback = jest.fn(() => { - action() - expect(action).toBeCalled() - done() - }) - - recaptcha("signup_submit", callback) + action(token) + expect(action).toBeCalledWith("recaptcha-token") expect(window.grecaptcha.execute).toBeCalledWith("recaptcha-api-key", { action: "signup_submit", }) }) - it("Fires the callback but no action if sd.RECAPTCHA_KEY is missing", done => { - sd.RECAPTCHA_KEY = "" + it("rejects if sd.RECAPTCHA_KEY is missing", async () => { + ;(getENV as jest.Mock).mockImplementation(() => undefined) + const action = jest.fn() - const callback = jest.fn(() => { + + try { + await recaptcha("signup_submit") + } catch (err) { action() - expect(action).toBeCalled() - done() - }) + } - recaptcha("signup_submit", callback) + expect(action).toBeCalled() expect(window.grecaptcha.ready).not.toBeCalled() expect(window.grecaptcha.execute).not.toBeCalled() }) diff --git a/src/Utils/auth.ts b/src/Utils/auth.ts index ee200b365e4..83bcbdc9d0d 100644 --- a/src/Utils/auth.ts +++ b/src/Utils/auth.ts @@ -4,7 +4,7 @@ import Cookies from "cookies-js" import { getENV } from "Utils/getENV" -import { recaptcha as _recaptcha, RecaptchaAction } from "Utils/recaptcha" +import { recaptcha } from "Utils/recaptcha" const headers = { Accept: "application/json", @@ -120,10 +120,6 @@ export const resetPassword = async (args: { return await Promise.reject(new Error(JSON.stringify(err))) } -const recaptcha = (action: RecaptchaAction) => { - return new Promise(resolve => _recaptcha(action, resolve)) -} - /** * To use ensure that `EnableRecaptcha` is included somewhere on your page */ diff --git a/src/Utils/recaptcha.ts b/src/Utils/recaptcha.ts index 8698fa89074..81afa1253a9 100644 --- a/src/Utils/recaptcha.ts +++ b/src/Utils/recaptcha.ts @@ -3,38 +3,31 @@ import createLogger from "Utils/logger" const logger = createLogger("recaptcha.ts") -// TODO: Should return a Promise instead of accepting a callback -export const recaptcha = ( - action: RecaptchaAction, - callback?: RecaptchaCallback -) => { - if (getENV("RECAPTCHA_KEY")) { - window.grecaptcha?.ready(async () => { - try { - const token = await window.grecaptcha.execute(getENV("RECAPTCHA_KEY"), { - action, - }) - - callback?.(token) - } catch (err) { - logger.error(err) - - if (action === "signup_submit") { - logger.warn("Signup submitted without Recaptcha Token") +export const recaptcha = async (action: RecaptchaAction): Promise => { + return new Promise((resolve, reject) => { + if (getENV("RECAPTCHA_KEY")) { + window.grecaptcha?.ready(async () => { + try { + const token = await window.grecaptcha.execute( + getENV("RECAPTCHA_KEY"), + { action } + ) + + return resolve(token) + } catch (err) { + logger.error(err) + + if (action === "signup_submit") { + logger.warn("Signup submitted without Recaptcha Token") + } + + return reject(err) } - - callback?.() - } - }) - - return - } - - if (action === "signup_submit") { - logger.warn("Signup submitted without Recaptcha Key") - } - - callback?.() + }) + } else { + reject("`RECAPTCHA_KEY` not found") + } + }) } export type RecaptchaAction = @@ -48,5 +41,4 @@ export type RecaptchaAction = | "login_submit" | "signup_submit" | "submission_submit" - -type RecaptchaCallback = (token?: string) => void + | "verify_user" diff --git a/src/__generated__/AuthDialogWelcomeQuery.graphql.ts b/src/__generated__/AuthDialogWelcomeQuery.graphql.ts new file mode 100644 index 00000000000..f88f496b22e --- /dev/null +++ b/src/__generated__/AuthDialogWelcomeQuery.graphql.ts @@ -0,0 +1,100 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest, Query } from 'relay-runtime'; +export type AuthDialogWelcomeQuery$variables = { + email: string; + recaptchaToken: string; +}; +export type AuthDialogWelcomeQuery$data = { + readonly verifyUser: { + readonly exists: boolean; + } | null | undefined; +}; +export type AuthDialogWelcomeQuery = { + response: AuthDialogWelcomeQuery$data; + variables: AuthDialogWelcomeQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "email" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "recaptchaToken" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "email", + "variableName": "email" + }, + { + "kind": "Variable", + "name": "recaptchaToken", + "variableName": "recaptchaToken" + } + ], + "concreteType": "VerifyUser", + "kind": "LinkedField", + "name": "verifyUser", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "exists", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "AuthDialogWelcomeQuery", + "selections": (v1/*: any*/), + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "AuthDialogWelcomeQuery", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "1a726d22d475625907540026eb1a39ff", + "id": null, + "metadata": {}, + "name": "AuthDialogWelcomeQuery", + "operationKind": "query", + "text": "query AuthDialogWelcomeQuery(\n $email: String!\n $recaptchaToken: String!\n) {\n verifyUser(email: $email, recaptchaToken: $recaptchaToken) {\n exists\n }\n}\n" + } +}; +})(); + +(node as any).hash = "187a4934af565bf023955eca066dce73"; + +export default node;