fix(auth): Gmail auth screen now shows error states

Summary:
- Refactor Gmail auth functions
- Add Analytics in
- Show error states

Depends on D3735

Test Plan: manual

Reviewers: khamidou, juan

Reviewed By: khamidou, juan

Differential Revision: https://phab.nylas.com/D3732
This commit is contained in:
Evan Morikawa 2017-01-18 17:44:22 -08:00
parent 23bd05a514
commit 735c7faa73
4 changed files with 141 additions and 76 deletions

View file

@ -35,43 +35,34 @@ function base64url(inBuffer) {
.replace(/\//g, '_'); // Convert '/' to '_'
}
export async function makeGmailOAuthRequest(sessionKey, callback) {
const noauth = {
user: '',
pass: '',
sendImmediately: true,
};
const NO_AUTH = { user: '', pass: '', sendImmediately: true };
export async function tokenRequestPollForGmail(sessionKey) {
const remoteRequest = new NylasAPIRequest({
api: N1CloudAPI,
options: {
path: `/auth/gmail/token?key=${sessionKey}`,
method: 'GET',
error: callback,
auth: noauth,
auth: NO_AUTH,
},
});
let remoteJSON = {}
try {
remoteJSON = await remoteRequest.run()
} catch (err) {
if (err.statusCode === 404) {
return
}
throw err
}
return remoteRequest.run()
}
export async function authIMAPForGmail(tokenData) {
const localRequest = new NylasAPIRequest({
api: NylasAPI,
options: {
path: `/auth`,
method: 'POST',
auth: noauth,
auth: NO_AUTH,
body: {
email: remoteJSON.email_address,
name: remoteJSON.name,
email: tokenData.email_address,
name: tokenData.name,
provider: 'gmail',
settings: {
xoauth2: remoteJSON.resolved_settings.xoauth2,
expiry_date: remoteJSON.resolved_settings.expiry_date,
xoauth2: tokenData.resolved_settings.xoauth2,
expiry_date: tokenData.resolved_settings.expiry_date,
},
},
},
@ -79,8 +70,8 @@ export async function makeGmailOAuthRequest(sessionKey, callback) {
const localJSON = await localRequest.run()
const account = Object.assign({}, localJSON);
account.localToken = localJSON.account_token;
account.cloudToken = remoteJSON.account_token;
callback(null, account);
account.cloudToken = tokenData.account_token;
return account
}
export function buildGmailSessionKey() {

View file

@ -2,7 +2,8 @@ import React from 'react';
import {OAuthSignInPage} from 'nylas-component-kit';
import {
makeGmailOAuthRequest,
tokenRequestPollForGmail,
authIMAPForGmail,
buildGmailSessionKey,
buildGmailAuthURL,
} from './onboarding-helpers';
@ -31,14 +32,17 @@ export default class AccountSettingsPageGmail extends React.Component {
render() {
const {accountInfo} = this.props;
const iconName = AccountTypes.find(a => a.type === accountInfo.type).headerIcon;
const goBack = () => OnboardingActions.moveToPreviousPage()
return (
<OAuthSignInPage
authUrl={this._gmailAuthUrl}
providerAuthPageUrl={this._gmailAuthUrl}
iconName={iconName}
makeRequest={makeGmailOAuthRequest}
tokenRequestPollFn={tokenRequestPollForGmail}
accountFromTokenFn={authIMAPForGmail}
onSuccess={this.onSuccess}
serviceName="Gmail"
onTryAgain={goBack}
serviceName="Google"
sessionKey={this._sessionKey}
/>
);

View file

@ -342,7 +342,7 @@
padding-bottom: 10px;
}
}
.page.account-setup.gmail {
.page.account-setup.google {
.logo-container {
padding-top: 160px;
}

View file

@ -1,25 +1,57 @@
import React from 'react';
import {ipcRenderer, shell} from 'electron';
import {Actions} from 'nylas-exports'
import {RetinaImg} from 'nylas-component-kit';
const clipboard = require('electron').clipboard
export default class OAuthSignInPage extends React.Component {
static displayName = "OAuthSignInPage";
static propTypes = {
authUrl: React.PropTypes.string,
iconName: React.PropTypes.string,
makeRequest: React.PropTypes.func,
/**
* 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: React.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: React.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. Nylas Mail
* 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: React.PropTypes.func,
/**
* Called once we have successfully received the account data from
* `accountFromTokenFn`
*/
onSuccess: React.PropTypes.func,
serviceName: React.PropTypes.string,
onTryAgain: React.PropTypes.func,
iconName: React.PropTypes.string,
sessionKey: React.PropTypes.string,
serviceName: React.PropTypes.string,
};
constructor() {
super()
this.state = {
authStage: "initial",
showAlternative: false,
}
}
@ -29,7 +61,7 @@ export default class OAuthSignInPage extends React.Component {
// to URL. (400msec animation + 200msec to read)
this._pollTimer = null;
this._startTimer = setTimeout(() => {
shell.openExternal(this.props.authUrl);
shell.openExternal(this.props.providerAuthPageUrl);
this.startPollingForResponse();
}, 600);
setTimeout(() => {
@ -42,10 +74,20 @@ export default class OAuthSignInPage extends React.Component {
if (this._pollTimer) clearTimeout(this._pollTimer);
}
_handleError(err) {
this.setState({authStage: "error", errorMessage: err.message})
NylasEnv.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;
@ -55,61 +97,93 @@ export default class OAuthSignInPage extends React.Component {
}
};
poll = () => {
this.props.makeRequest(this.props.sessionKey, (err, json) => {
clearTimeout(this._pollTimer);
if (json) {
ipcRenderer.removeListener('browser-window-focus', onWindowFocused);
let body = json
if (json.body) {
body = json.body
}
this.props.onSuccess(body);
} else {
delay = Math.min(delay * 1.2, 10000);
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, 5000);
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 (this.authStage === "accountSuccess") {
return <h2>Connected to {this.props.serviceName}</h2>
}
// Error
return (<div>
<h2>Sorry, we had trouble logging you in</h2>
<div className="error-region">
<p className="message error error-message">{this.state.errorMessage}</p>
<p className="extra">Please <a onClick={this.props.onTryAgain}>try again</a>. If you continue to see this error contact support@nylas.com</p>
</div>
</div>)
}
_renderAlternative() {
let classnames = "input hidden"
if (this.state.showAlternative) {
if (this.state.authStage === "polling" && this.state.showAlternative) {
classnames += " fadein"
}
return (
<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.authUrl}
readOnly
/>
<div
className="copy-to-clipboard"
onClick={() => clipboard.writeText(this.props.authUrl)}
onMouseDown={() => this.setState({pressed: true})}
onMouseUp={() => this.setState({pressed: false})}
>
<RetinaImg
name="icon-copytoclipboard.png"
mode={RetinaImg.Mode.ContentIsMask}
<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()}`}>
@ -120,12 +194,8 @@ export default class OAuthSignInPage extends React.Component {
className="logo"
/>
</div>
<h2>
Sign in to {this.props.serviceName} in<br />your browser.
</h2>
<div className="alternative-auth">
{this._renderAlternative()}
</div>
{this._renderHeader()}
{this._renderAlternative()}
</div>
);
}