Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add/email verification #2587

Merged
merged 9 commits into from
Jan 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import NavigationBar from './navigation-bar';
import AppLayout from './app-layout';
import DevBadge from './components/dev-badge';
import DialogRenderer from './dialog-renderer';
import EmailVerification from './email-verification';
import { isElectron, isMac } from './utils/platform';
import classNames from 'classnames';
import {
Expand Down Expand Up @@ -33,6 +34,7 @@ type StateProps = {
isSmallScreen: boolean;
lineLength: T.LineLength;
isSearchActive: boolean;
showEmailVerification: boolean;
showNavigation: boolean;
showNoteInfo: boolean;
theme: 'light' | 'dark';
Expand Down Expand Up @@ -160,6 +162,7 @@ class AppComponent extends Component<Props> {
const {
isDevConfig,
lineLength,
showEmailVerification,
showNavigation,
showNoteInfo,
theme,
Expand All @@ -179,6 +182,7 @@ class AppComponent extends Component<Props> {

return (
<div className={appClasses}>
{showEmailVerification && <EmailVerification />}
{isDevConfig && <DevBadge />}
<div className={mainClasses}>
{showNavigation && <NavigationBar />}
Expand All @@ -197,6 +201,7 @@ const mapStateToProps: S.MapState<StateProps> = (state) => ({
isSearchActive: !!state.ui.searchQuery.length,
isSmallScreen: selectors.isSmallScreen(state),
lineLength: state.settings.lineLength,
showEmailVerification: selectors.shouldShowEmailVerification(state),
showNavigation: state.ui.showNavigation,
showNoteInfo: state.ui.showNoteInfo,
theme: selectors.getTheme(state),
Expand Down
147 changes: 147 additions & 0 deletions lib/email-verification/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React, { Fragment, FunctionComponent, useState, useEffect } from 'react';
import classNames from 'classnames';
import Modal from 'react-modal';
import { connect } from 'react-redux';

import CrossIcon from '../icons/cross';
import MailIcon from '../icons/mail';
import WarningIcon from '../icons/warning';

import actions from '../state/actions';
import * as selectors from '../state/selectors';
import { recordEvent } from '../state/analytics/middleware';

import * as S from '../state';
import * as T from '../types';

type StateProps = {
accountVerification: T.VerificationState;
email: string | null;
theme: string;
};

type DispatchProps = {
dismiss: () => any;
};

type Props = StateProps & DispatchProps;

const EmailVerification: FunctionComponent<Props> = ({
accountVerification,
dismiss,
email,
theme,
}) => {
const base64EncodedEmail = btoa(email || '');
const sendVerifyUrl: string = `https://app.simplenote.com/account/verify-email/${base64EncodedEmail}`;

if (accountVerification === 'pending') {
recordEvent('verification_verify_screen_viewed');
} else {
recordEvent('verification_review_screen_viewed');
}

const displayClose = (
<div className="email-verification__dismiss">
<button
className="icon-button"
aria-label="Close dialog"
onClick={dismiss}
>
<CrossIcon />
</button>
</div>
);

const displayEmailConfirm = (
<Fragment>
{displayClose}
<span className="theme-color-fg-dim">
<WarningIcon />
</span>
<h2>Review Your Account</h2>
<p>
You are registered with Simplenote using the email{' '}
<strong>{email}</strong>.
</p>
<p>
Improvements to account security may result in account loss if you no
longer have access to this email address.
</p>

<div className="email-verification__button-row button-borderless">
<a
target="_blank"
rel="noreferrer"
href="https://app.simplenote.com/settings"
onClick={() => recordEvent('verification_change_email_button_tapped')}
>
<button className="button button-borderless">Change Email</button>
</a>
<a
target="_blank"
rel="noreferrer"
href={sendVerifyUrl}
onClick={() => recordEvent('verification_confirm_button_tapped')}
>
<button className="button button-primary">Confirm</button>
</a>
</div>
</Fragment>
);

const displayEmailRequested = (
<Fragment>
{displayClose}
<span className="theme-color-fg-dim">
<MailIcon />
</span>
<h2>Verify Your Email</h2>
<p>
We’ve sent a verification email to <strong>{email}</strong>.
</p>
<p>Please check your inbox and follow the instructions.</p>

<div className="email-verification__button-row">
<a
target="_blank"
rel="noreferrer"
href={sendVerifyUrl}
onClick={() => recordEvent('verification_resend_email_button_tapped')}
>
<button className="button button-borderless">Resend Email</button>
</a>
</div>
</Fragment>
);
return (
<Modal
key="email-verification"
className="email-verification__modal theme-color-fg theme-color-bg"
isOpen
onRequestClose={dismiss}
contentLabel="Confirm your email"
overlayClassName="email-verification__overlay"
portalClassName={classNames(
'email-verification__portal',
'theme-' + theme
)}
>
{accountVerification === 'pending'
? displayEmailRequested
: displayEmailConfirm}
</Modal>
);
};

const mapDispatchToProps: S.MapDispatch<DispatchProps> = {
dismiss: () => actions.ui.dismissEmailVerifyDialog(),
};

const mapStateToProps: S.MapState<StateProps> = (state) => ({
accountVerification: state.data.accountVerification,
email: state.settings.accountName,
theme: selectors.getTheme(state),
});

export default connect(mapStateToProps, mapDispatchToProps)(EmailVerification);
83 changes: 83 additions & 0 deletions lib/email-verification/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
.email-verification__overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
background: rgba($studio-gray-20, 0.75);
}

.email-verification__dismiss {
display: flex;
height: 60px;
justify-content: flex-end;
line-height: 60px;
margin: 0 16px;
width: 100%;

svg {
height: 16px;
width: 16px;
margin: auto 0;
}
}

.email-verification__modal {
align-items: center;
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
margin: 20%;
max-width: 420px;
padding: 0 24px 16px;
font-size: 16px;

.icon-mail,
.icon-warning {
height: 48px;
width: 48px;
}

p {
margin: 0 26px;
margin-block-start: 0;
}

p:not(:last-of-type) {
padding-bottom: 20px;
}

.email-verification__button-row {
padding-top: 10px;
padding-bottom: 10px;
display: flex;
flex-direction: row;
justify-content: flex-end;
flex-wrap: wrap;
width: 100%;

a {
padding-right: 12px;
}
.button-borderless {
color: $studio-simplenote-blue-50;
}
}

&:focus {
outline: 0;
}
}

.theme-dark {
.email-verification__overlay {
background: rgba($studio-gray-100, 0.75);
}
.email-verification__button-row .button-borderless {
color: $studio-simplenote-blue-30;
}
}
14 changes: 14 additions & 0 deletions lib/icons/mail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';

export default function MailIcon() {
return (
<svg
className="icon-mail"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<rect width="24" height="24" fill="none" />
<path d="M20 4H4C2.9 4 2 4.9 2 6v12C2 19.1 2.9 20 4 20h16C21.1 20 22 19.1 22 18V6C22 4.9 21.1 4 20 4zM18.3 6L12 10.8 5.7 6H18.3zM4 6L4 6V6H4zM4 18L4 7.2 12 13.3l8-6L20 18 4 18z" />
</svg>
);
}
18 changes: 18 additions & 0 deletions lib/icons/warning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';

export default function WarningIcon() {
return (
<svg
className="icon-warning"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<rect x="0" fill="none" width="24" height="24" />
<g>
<path d="M12,22C6.5,22,2,17.5,2,12S6.5,2,12,2s10,4.5,10,10S17.5,22,12,22z M12,4c-4.4,0-8,3.6-8,8s3.6,8,8,8s8-3.6,8-8 S16.4,4,12,4z" />
<path d="M12,15c0.6,0,1,0.4,1,1c0,0.6-0.4,1-1,1s-1-0.4-1-1C11,15.4,11.4,15,12,15z" />
<path d="M11,7h2v6h-2V7z" />
</g>
</svg>
);
}
5 changes: 5 additions & 0 deletions lib/state/action-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,10 @@ export type TagRefresh = Action<
'TAG_REFRESH',
{ noteTags: Map<T.TagHash, Set<T.EntityId>> }
>;
export type UpdateAccountVerification = Action<
'UPDATE_ACCOUNT_VERIFICATION',
{ state: T.VerificationState }
>;

export type ActionType =
| AcknowledgePendingChange
Expand Down Expand Up @@ -409,6 +413,7 @@ export type ActionType =
| TrashNote
| TrashTag
| TrashOpenNote
| UpdateAccountVerification
| WindowResize;

export type ActionCreator<A extends ActionType> = (...args: any[]) => A;
Expand Down
5 changes: 5 additions & 0 deletions lib/state/analytics/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ export const middleware: S.Middleware = (store) => {
case 'CREATE_NOTE':
record('list_note_created');
break;
case 'UPDATE_ACCOUNT_VERIFICATION':
if (action.state === 'dismissed') {
record('verification_dismissed');
}
break;
case 'EDIT_NOTE':
recordNoteEdit();
break;
Expand Down
9 changes: 9 additions & 0 deletions lib/state/data/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ export const analyticsAllowed: A.Reducer<boolean | null> = (
}
};

const accountVerification: A.Reducer<T.VerificationState> = (
state = 'unknown',
action
) =>
action.type === 'UPDATE_ACCOUNT_VERIFICATION' && 'dismissed' !== state
? action.state
: state;

const modified = <Entity extends { modificationDate: number }>(
entity: Entity
): Entity => ({
Expand Down Expand Up @@ -531,6 +539,7 @@ export const noteTags: A.Reducer<Map<T.TagHash, Set<T.EntityId>>> = (
};

export default combineReducers({
accountVerification,
analyticsAllowed,
notes,
noteRevisions,
Expand Down
4 changes: 4 additions & 0 deletions lib/state/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ export const noteHasPendingChanges: S.Selector<boolean> = (
state.data.notes.get(noteId),
state.simperium.ghosts[1].get('note')?.get(noteId)?.data
);

export const shouldShowEmailVerification: S.Selector<boolean> = ({
data: { accountVerification: status },
}) => status === 'unverified' || status === 'pending';
Loading