During onboarding, test Gmail accounts for the All Mail folder

This commit is contained in:
Ben Gotow 2017-10-06 12:45:21 -07:00
parent d6c336f070
commit 71f537ede0
6 changed files with 217 additions and 8 deletions

View file

@ -0,0 +1,209 @@
import { ipcRenderer, shell, clipboard } from 'electron';
import { React, PropTypes, Actions } from 'mailspring-exports';
import { RetinaImg } from 'mailspring-component-kit';
import FormErrorMessage from './form-error-message';
export default class OAuthSignInPage extends React.Component {
static displayName = 'OAuthSignInPage';
static propTypes = {
/**
* Step 1: Open a webpage in the user's browser letting them login on
* the native provider's website. We pass along a key and a redirect
* url to a Nylas-owned server
*/
providerAuthPageUrl: PropTypes.string,
/**
* Step 2: Poll a Nylas server with this function looking for the key.
* Once users complete the auth successfully, Nylas servers will get
* the token and vend it back to us via this url. We need to poll
* since we don't know how long it'll take users to log in on their
* provider's website.
*/
tokenRequestPollFn: PropTypes.func,
/**
* Once we have the token, we can use that to retrieve the full
* account credentials or establish a direct connection ourselves.
* Some Nylas backends vend all account credentials along with the
* token making this function unnecessary and a no-op. Mailspring
* local sync needs to use the returned OAuth token to establish an
* IMAP connection directly that may have its own set of failure
* cases.
*/
accountFromTokenFn: PropTypes.func,
/**
* Called once we have successfully received the account data from
* `accountFromTokenFn`
*/
onSuccess: PropTypes.func,
onTryAgain: PropTypes.func,
iconName: PropTypes.string,
sessionKey: PropTypes.string,
serviceName: PropTypes.string,
};
constructor() {
super();
this.state = {
authStage: 'initial',
showAlternative: false,
};
}
componentDidMount() {
// Show the "Sign in to ..." prompt for a moment before bouncing
// to URL. (400msec animation + 200msec to read)
this._pollTimer = null;
this._startTimer = setTimeout(() => {
shell.openExternal(this.props.providerAuthPageUrl);
this.startPollingForResponse();
}, 600);
setTimeout(() => {
this.setState({ showAlternative: true });
}, 1500);
}
componentWillUnmount() {
if (this._startTimer) clearTimeout(this._startTimer);
if (this._pollTimer) clearTimeout(this._pollTimer);
}
_handleError(err) {
this.setState({ authStage: 'error', errorMessage: err.message });
AppEnv.reportError(err);
Actions.recordUserEvent('Email Account Auth Failed', {
errorMessage: err.message,
provider: 'gmail',
});
}
startPollingForResponse() {
let delay = 1000;
let onWindowFocused = null;
let poll = null;
this.setState({ authStage: 'polling' });
onWindowFocused = () => {
delay = 1000;
if (this._pollTimer) {
clearTimeout(this._pollTimer);
this._pollTimer = setTimeout(poll, delay);
}
};
poll = async () => {
clearTimeout(this._pollTimer);
try {
const tokenData = await this.props.tokenRequestPollFn(this.props.sessionKey);
ipcRenderer.removeListener('browser-window-focus', onWindowFocused);
this.fetchAccountDataWithToken(tokenData);
} catch (err) {
if (err.statusCode === 404) {
delay = Math.min(delay * 1.1, 3000);
this._pollTimer = setTimeout(poll, delay);
} else {
ipcRenderer.removeListener('browser-window-focus', onWindowFocused);
this._handleError(err);
}
}
};
ipcRenderer.on('browser-window-focus', onWindowFocused);
this._pollTimer = setTimeout(poll, 3000);
}
async fetchAccountDataWithToken(tokenData) {
try {
this.setState({ authStage: 'fetchingAccount' });
const accountData = await this.props.accountFromTokenFn(tokenData);
this.props.onSuccess(accountData);
this.setState({ authStage: 'accountSuccess' });
} catch (err) {
this._handleError(err);
}
}
_renderHeader() {
const authStage = this.state.authStage;
if (authStage === 'initial' || authStage === 'polling') {
return (
<h2>
Sign in with {this.props.serviceName} in<br />your browser.
</h2>
);
} else if (authStage === 'fetchingAccount') {
return <h2>Connecting to {this.props.serviceName}</h2>;
} else if (authStage === 'accountSuccess') {
return (
<div>
<h2>Successfully connected to {this.props.serviceName}!</h2>
<h3>Adding your account to Mailspring</h3>
</div>
);
}
// Error
return (
<div>
<h2>Sorry, we had trouble logging you in</h2>
<div className="error-region">
<FormErrorMessage message={this.state.errorMessage} />
<div className="btn" style={{ marginTop: 20 }} onClick={this.props.onTryAgain}>
Try Again
</div>
</div>
</div>
);
}
_renderAlternative() {
let classnames = 'input hidden';
if (this.state.authStage === 'polling' && this.state.showAlternative) {
classnames += ' fadein';
}
return (
<div className="alternative-auth">
<div className={classnames}>
<div style={{ marginTop: 40 }}>
Page didn&#39;t open? Paste this URL into your browser:
</div>
<input
type="url"
className="url-copy-target"
value={this.props.providerAuthPageUrl}
readOnly
/>
<div
className="copy-to-clipboard"
onClick={() => clipboard.writeText(this.props.providerAuthPageUrl)}
onMouseDown={() => this.setState({ pressed: true })}
onMouseUp={() => this.setState({ pressed: false })}
>
<RetinaImg name="icon-copytoclipboard.png" mode={RetinaImg.Mode.ContentIsMask} />
</div>
</div>
</div>
);
}
render() {
return (
<div className={`page account-setup ${this.props.serviceName.toLowerCase()}`}>
<div className="logo-container">
<RetinaImg
name={this.props.iconName}
mode={RetinaImg.Mode.ContentPreserve}
className="logo"
/>
</div>
{this._renderHeader()}
{this._renderAlternative()}
</div>
);
}
}

View file

@ -99,10 +99,6 @@ export function makeGmailOAuthRequest(sessionKey) {
}
export async function buildGmailAccountFromToken(serverTokenResponse) {
// At this point, the Mailspring server has retrieved the Gmail token,
// created an account object in the database and tested it. All we
// need to do is save it locally, since we're confident Gmail will be
// accessible from the local sync worker.
const { name, emailAddress, refreshToken } = serverTokenResponse;
const account = expandAccountWithCommonSettings(
@ -118,6 +114,10 @@ export async function buildGmailAccountFromToken(serverTokenResponse) {
account.id = idForAccount(emailAddress, account.settings);
// test the account locally to ensure the All Mail folder is enabled
// and the refresh token can be exchanged for an account token.
await finalizeAndValidateAccount(account);
return account;
}

View file

@ -1,6 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { OAuthSignInPage } from 'mailspring-component-kit';
import {
makeGmailOAuthRequest,
@ -9,6 +8,7 @@ import {
buildGmailAuthURL,
} from './onboarding-helpers';
import OAuthSignInPage from './oauth-signin-page';
import OnboardingActions from './onboarding-actions';
import AccountProviders from './account-providers';

View file

@ -95,7 +95,6 @@ lazyLoad('UndoToast', 'undo-toast');
lazyLoad('LazyRenderedList', 'lazy-rendered-list');
lazyLoad('OverlaidComponents', 'overlaid-components/overlaid-components');
lazyLoad('OverlaidComposerExtension', 'overlaid-components/overlaid-composer-extension');
lazyLoad('OAuthSignInPage', 'oauth-signin-page');
lazyLoadFrom('AttachmentItem', 'attachment-items');
lazyLoadFrom('ImageAttachmentItem', 'attachment-items');
lazyLoad('CodeSnippet', 'code-snippet');

View file

@ -13,7 +13,8 @@ let Utils = null;
export const LocalizedErrorStrings = {
ErrorConnection: 'Connection Error - Unable to connect to the server / port you provided.',
ErrorInvalidAccount: 'This account is invalid, or does not have an inbox or all folder.',
ErrorInvalidAccount:
'This account is invalid or Mailspring could not find the Inbox or All Mail folder. http://support.getmailspring.com/hc/en-us/articles/115001881912',
ErrorTLSNotAvailable: 'TLS Not Available',
ErrorParse: 'Parsing Error',
ErrorCertificate: 'Certificate Error',

@ -1 +1 @@
Subproject commit 6024cf3744aca7448e623e1ec3f9820e1fe5a3d9
Subproject commit bbf196e106cbf858015748b5b04b2ca537253d5f