diff --git a/internal_packages/onboarding/lib/onboarding-helpers.es6 b/internal_packages/onboarding/lib/onboarding-helpers.es6
index 24ffe0846..fabcbc904 100644
--- a/internal_packages/onboarding/lib/onboarding-helpers.es6
+++ b/internal_packages/onboarding/lib/onboarding-helpers.es6
@@ -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() {
diff --git a/internal_packages/onboarding/lib/page-account-settings-gmail.jsx b/internal_packages/onboarding/lib/page-account-settings-gmail.jsx
index 2577ca908..da4a7180f 100644
--- a/internal_packages/onboarding/lib/page-account-settings-gmail.jsx
+++ b/internal_packages/onboarding/lib/page-account-settings-gmail.jsx
@@ -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 (
);
diff --git a/internal_packages/onboarding/stylesheets/onboarding.less b/internal_packages/onboarding/stylesheets/onboarding.less
index 9353a7a7c..e9b90de8b 100644
--- a/internal_packages/onboarding/stylesheets/onboarding.less
+++ b/internal_packages/onboarding/stylesheets/onboarding.less
@@ -342,7 +342,7 @@
padding-bottom: 10px;
}
}
-.page.account-setup.gmail {
+.page.account-setup.google {
.logo-container {
padding-top: 160px;
}
diff --git a/src/components/oauth-signin-page.jsx b/src/components/oauth-signin-page.jsx
index 8d92e5479..ee0dd0c76 100644
--- a/src/components/oauth-signin-page.jsx
+++ b/src/components/oauth-signin-page.jsx
@@ -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 (
+ Sign in with {this.props.serviceName} in
your browser.
+
)
+ } else if (authStage === "fetchingAccount") {
+ return Connecting to {this.props.serviceName}…
+ } else if (this.authStage === "accountSuccess") {
+ return Connected to {this.props.serviceName}…
+ }
+ // Error
+ return (
+
Sorry, we had trouble logging you in
+
+
{this.state.errorMessage}
+
Please try again. If you continue to see this error contact support@nylas.com
+
+
)
+ }
_renderAlternative() {
let classnames = "input hidden"
- if (this.state.showAlternative) {
+ if (this.state.authStage === "polling" && this.state.showAlternative) {
classnames += " fadein"
}
return (
-
-
- Page didn't open? Paste this URL into your browser:
-
-
-
clipboard.writeText(this.props.authUrl)}
- onMouseDown={() => this.setState({pressed: true})}
- onMouseUp={() => this.setState({pressed: false})}
- >
-
+
+
+ Page didn't open? Paste this URL into your browser:
+
+
+
clipboard.writeText(this.props.providerAuthPageUrl)}
+ onMouseDown={() => this.setState({pressed: true})}
+ onMouseUp={() => this.setState({pressed: false})}
+ >
+
+
)
}
-
render() {
return (
@@ -120,12 +194,8 @@ export default class OAuthSignInPage extends React.Component {
className="logo"
/>
-
- Sign in to {this.props.serviceName} in
your browser.
-
-
- {this._renderAlternative()}
-
+ {this._renderHeader()}
+ {this._renderAlternative()}
);
}