From 54131cd94c1c98dcc78011a482052ff178c3769d Mon Sep 17 00:00:00 2001 From: Rajat Talesra Date: Wed, 20 Sep 2023 17:55:29 +0530 Subject: [PATCH] Move the digest email opt-in to the follow tags onboarding step (#20133) * Removed intro slide and updated sequence * Revert db change * Initial save * Design changes * Mobile designs * Added functionality * Added tests * Gradient effect added * Added API call test --- .../stylesheets/preact/onboarding-modal.scss | 45 ++++++++ .../components/EmailPreferencesForm.jsx | 16 +-- .../onboarding/components/FollowTags.jsx | 100 ++++++++++++++++-- .../__tests__/EmailPreferencesForm.test.jsx | 3 +- .../components/__tests__/FollowTags.test.jsx | 69 ++++++++++++ 5 files changed, 210 insertions(+), 23 deletions(-) diff --git a/app/assets/stylesheets/preact/onboarding-modal.scss b/app/assets/stylesheets/preact/onboarding-modal.scss index 7e6cebb4b..1bd0a8d10 100644 --- a/app/assets/stylesheets/preact/onboarding-modal.scss +++ b/app/assets/stylesheets/preact/onboarding-modal.scss @@ -92,6 +92,46 @@ } } +.onboarding-content-separator { + position: absolute; + bottom: 160px; + left: var(--su-4); + width: calc(100% - 2 * var(--su-4)); + height: 96px; + background-image: linear-gradient( + rgba(255, 255, 255, 0) 25%, + rgba(255, 255, 255, 1) 75% + ); + pointer-events: none; + + @media screen and (min-width: $breakpoint-s) { + left: var(--su-8); + width: calc(100% - 2 * var(--su-8)); + bottom: 168px; + } +} + +.onboarding-email-digest { + z-index: 1; + display: flex; + margin: 0px var(--su-4) var(--su-4) var(--su-4); + align-items: center; + border-radius: 0px 6px 6px 0px; + border: 1px solid rgba(var(--tag-onboarding-border), 1); + background-color: var(--card-secondary-bg); + + @media screen and (min-width: $breakpoint-s) { + margin: 0px var(--su-8) 40px var(--su-8); + } + + &__rectangle { + width: 4px; + height: 100%; + flex-shrink: 0; + background: rgba(var(--accent-brand-rgb), 1); + } +} + // modal navigation .onboarding-navigation { align-self: flex-end; @@ -217,6 +257,11 @@ grid-template-columns: repeat(2, 1fr); grid-gap: var(--su-2); width: 100%; + padding-bottom: 48px; + + @media screen and (min-width: $breakpoint-s) { + padding-bottom: 24px; + } @media screen and (min-width: 800px) { grid-template-columns: repeat(3, 1fr); diff --git a/app/javascript/onboarding/components/EmailPreferencesForm.jsx b/app/javascript/onboarding/components/EmailPreferencesForm.jsx index 6dbb06351..8371a2977 100644 --- a/app/javascript/onboarding/components/EmailPreferencesForm.jsx +++ b/app/javascript/onboarding/components/EmailPreferencesForm.jsx @@ -13,7 +13,6 @@ export class EmailPreferencesForm extends Component { this.state = { email_newsletter: false, - email_digest_periodic: false, }; } @@ -49,7 +48,7 @@ export class EmailPreferencesForm extends Component { } render() { - const { email_newsletter, email_digest_periodic } = this.state; + const { email_newsletter } = this.state; const { prev, slidesCount, currentSlideIndex } = this.props; return (
-
  • - -
  • diff --git a/app/javascript/onboarding/components/FollowTags.jsx b/app/javascript/onboarding/components/FollowTags.jsx index fc7fb389a..a360df468 100644 --- a/app/javascript/onboarding/components/FollowTags.jsx +++ b/app/javascript/onboarding/components/FollowTags.jsx @@ -8,12 +8,14 @@ export class FollowTags extends Component { constructor(props) { super(props); + this.handleChange = this.handleChange.bind(this); this.handleClick = this.handleClick.bind(this); this.handleComplete = this.handleComplete.bind(this); this.state = { allTags: [], selectedTags: [], + email_digest_periodic: false, }; } @@ -38,6 +40,31 @@ export class FollowTags extends Component { }); } + handleContainerClick = () => { + const checkbox = document.getElementById('email_digest_periodic'); + checkbox.checked = !checkbox.checked; + + const event = new Event('change', { bubbles: true }); + checkbox.dispatchEvent(event); + + this.setState({ email_digest_periodic: checkbox.checked }); + }; + + handleContainerKeyDown = (event) => { + if (event.key === 'Enter' || event.key === ' ') { + this.handleContainerClick(); + } + }; + + handleCheckboxClick = (event) => { + event.stopPropagation(); + }; + + handleChange = (event) => { + const { name, checked } = event.target; + this.setState({ [name]: checked }); + }; + handleClick(tag) { let { selectedTags } = this.state; if (!selectedTags.includes(tag)) { @@ -56,7 +83,7 @@ export class FollowTags extends Component { handleComplete() { const csrfToken = getContentOfToken('csrf-token'); - const { selectedTags } = this.state; + const { selectedTags, email_digest_periodic } = this.state; Promise.all( selectedTags.map((tag) => @@ -74,10 +101,31 @@ export class FollowTags extends Component { credentials: 'same-origin', }), ), - ).then((_) => { - const { next } = this.props; - next(); - }); + ) + .then(() => { + if (email_digest_periodic) { + return fetch('/onboarding/notifications', { + method: 'PATCH', + headers: { + 'X-CSRF-Token': csrfToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + notifications: { + email_digest_periodic: this.state.email_digest_periodic, + }, + }), + credentials: 'same-origin', + }); + } + return Promise.resolve(); + }) + .then((response) => { + if (!email_digest_periodic || response.ok) { + const { next } = this.props; + next(); + } + }); } renderFollowCount() { @@ -94,7 +142,7 @@ export class FollowTags extends Component { render() { const { prev, currentSlideIndex, slidesCount } = this.props; - const { selectedTags, allTags } = this.state; + const { selectedTags, allTags, email_digest_periodic } = this.state; const canSkip = selectedTags.length === 0; return ( @@ -162,6 +210,46 @@ export class FollowTags extends Component { })}
    + +
    + +
    +
    +
    +
      +
    • + +
    • +
    +
    +
    +
    +

    + Get a Periodic Digest of Top Posts +

    +

    + We'll email you with a curated selection of top posts based on + the tags you follow. +

    +
    +
    +
    { expect(queryByText('Email preferences')).toExist(); }); - it('should show the two checkboxes unchecked', () => { + it('should show the checkbox unchecked', () => { const { queryByLabelText } = renderEmailPreferencesForm(); expect(queryByLabelText(/receive weekly newsletter/i).checked).toBe(false); - expect(queryByLabelText(/receive a periodic digest/i).checked).toBe(false); }); it('should render a stepper', () => { diff --git a/app/javascript/onboarding/components/__tests__/FollowTags.test.jsx b/app/javascript/onboarding/components/__tests__/FollowTags.test.jsx index 776423784..4568f1c0f 100644 --- a/app/javascript/onboarding/components/__tests__/FollowTags.test.jsx +++ b/app/javascript/onboarding/components/__tests__/FollowTags.test.jsx @@ -162,4 +162,73 @@ describe('FollowTags', () => { expect(getByText('0 tags selected')).toBeInTheDocument(), ); }); + + it('should toggle the checkbox when container is clicked', async () => { + const { container } = renderFollowTags(); + const checkbox = container.querySelector('#email_digest_periodic'); + + expect(checkbox.checked).toBeFalsy(); + + fireEvent.click(container.querySelector('.onboarding-email-digest')); + + expect(checkbox.checked).toBeTruthy(); + + fireEvent.click(container.querySelector('.onboarding-email-digest')); + + expect(checkbox.checked).toBeFalsy(); + }); + + it('should toggle the checkbox when Enter or Space key is pressed', async () => { + const { container } = renderFollowTags(); + const checkbox = container.querySelector('#email_digest_periodic'); + + expect(checkbox.checked).toBeFalsy(); + + fireEvent.keyDown(container.querySelector('.onboarding-email-digest'), { + key: 'Enter', + code: 'Enter', + keyCode: 13, + charCode: 13, + }); + + expect(checkbox.checked).toBeTruthy(); + + fireEvent.keyDown(container.querySelector('.onboarding-email-digest'), { + key: ' ', + code: 'Space', + keyCode: 32, + charCode: 32, + }); + + expect(checkbox.checked).toBeFalsy(); + }); + + it('should prevent checkbox click event from propagating', async () => { + const { container } = renderFollowTags(); + const checkbox = container.querySelector('#email_digest_periodic'); + + let clicked = false; + + checkbox.addEventListener('click', () => { + clicked = true; + }); + + fireEvent.click(container.querySelector('.onboarding-email-digest')); + + expect(clicked).toBeFalsy(); + }); + + it('should call /onboarding/notifications API when email_digest_periodic is true', async () => { + const { getByText, container } = renderFollowTags(); + + fireEvent.click(container.querySelector('.onboarding-email-digest')); + + const skipButton = getByText(/Skip for now/i); + fireEvent.click(skipButton); + + await waitFor(() => { + const [lastFetchUri] = fetch.mock.calls[fetch.mock.calls.length - 1]; + expect(lastFetchUri).toEqual('/onboarding/notifications'); + }); + }); });