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
This commit is contained in:
Rajat Talesra 2023-09-20 17:55:29 +05:30 committed by GitHub
parent 35c6c4bca7
commit 54131cd94c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 210 additions and 23 deletions

View file

@ -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);

View file

@ -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 (
<div
@ -88,19 +87,6 @@ export class EmailPreferencesForm extends Component {
I want to receive weekly newsletter emails.
</label>
</li>
<li className="checkbox-item">
<label htmlFor="email_digest_periodic">
<input
type="checkbox"
id="email_digest_periodic"
name="email_digest_periodic"
checked={email_digest_periodic}
onChange={this.handleChange}
/>
I want to receive a periodic digest of top posts from my
tags.
</label>
</li>
</ul>
</fieldset>
</form>

View file

@ -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 {
})}
</div>
</div>
<span class="onboarding-content-separator" />
<div
class="onboarding-email-digest"
onClick={this.handleContainerClick}
onKeyDown={this.handleContainerKeyDown}
role="button"
tabIndex="0"
>
<span class="onboarding-email-digest__rectangle" />
<div class="flex items-start my-4 ml-1 mr-4">
<form>
<fieldset>
<ul>
<li className="checkbox-item">
<label htmlFor="email_digest_periodic">
<input
type="checkbox"
id="email_digest_periodic"
name="email_digest_periodic"
checked={email_digest_periodic}
onChange={this.handleChange}
onClick={this.handleCheckboxClick}
tabIndex="-1"
/>
</label>
</li>
</ul>
</fieldset>
</form>
<div class="flex flex-col items-start">
<p class="crayons-subtitle-3 fw-medium">
Get a Periodic Digest of Top Posts
</p>
<p class="fs-s fw-normal lh-base color-secondary">
We'll email you with a curated selection of top posts based on
the tags you follow.
</p>
</div>
</div>
</div>
<Navigation
prev={prev}
next={this.handleComplete}

View file

@ -56,11 +56,10 @@ describe('EmailPreferencesForm', () => {
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', () => {

View file

@ -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');
});
});
});