mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-21 15:56:10 +08:00
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:
parent
23bd05a514
commit
735c7faa73
|
@ -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() {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -342,7 +342,7 @@
|
|||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
.page.account-setup.gmail {
|
||||
.page.account-setup.google {
|
||||
.logo-container {
|
||||
padding-top: 160px;
|
||||
}
|
||||
|
|
|
@ -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'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'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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue