diff --git a/app/internal_packages/onboarding/lib/oauth-signin-page.jsx b/app/internal_packages/onboarding/lib/oauth-signin-page.jsx new file mode 100644 index 000000000..b4775bd35 --- /dev/null +++ b/app/internal_packages/onboarding/lib/oauth-signin-page.jsx @@ -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 ( +

+ Sign in with {this.props.serviceName} in
your browser. +

+ ); + } else if (authStage === 'fetchingAccount') { + return

Connecting to {this.props.serviceName}…

; + } else if (authStage === 'accountSuccess') { + return ( +
+

Successfully connected to {this.props.serviceName}!

+

Adding your account to Mailspring…

+
+ ); + } + + // Error + return ( +
+

Sorry, we had trouble logging you in

+
+ +
+ Try Again +
+
+
+ ); + } + + _renderAlternative() { + let classnames = 'input hidden'; + 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.providerAuthPageUrl)} + onMouseDown={() => this.setState({ pressed: true })} + onMouseUp={() => this.setState({ pressed: false })} + > + +
+
+
+ ); + } + + render() { + return ( +
+
+ +
+ {this._renderHeader()} + {this._renderAlternative()} +
+ ); + } +} diff --git a/app/internal_packages/onboarding/lib/onboarding-helpers.es6 b/app/internal_packages/onboarding/lib/onboarding-helpers.es6 index 0f4981aaf..3f89fe62a 100644 --- a/app/internal_packages/onboarding/lib/onboarding-helpers.es6 +++ b/app/internal_packages/onboarding/lib/onboarding-helpers.es6 @@ -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; } diff --git a/app/internal_packages/onboarding/lib/page-account-settings-gmail.jsx b/app/internal_packages/onboarding/lib/page-account-settings-gmail.jsx index a4be2e545..4e604dcd7 100644 --- a/app/internal_packages/onboarding/lib/page-account-settings-gmail.jsx +++ b/app/internal_packages/onboarding/lib/page-account-settings-gmail.jsx @@ -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'; diff --git a/app/src/global/mailspring-component-kit.es6 b/app/src/global/mailspring-component-kit.es6 index 87db7fd51..4943d828c 100644 --- a/app/src/global/mailspring-component-kit.es6 +++ b/app/src/global/mailspring-component-kit.es6 @@ -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'); diff --git a/app/src/mailsync-process.es6 b/app/src/mailsync-process.es6 index fec26b627..6082368ec 100644 --- a/app/src/mailsync-process.es6 +++ b/app/src/mailsync-process.es6 @@ -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', diff --git a/mailsync b/mailsync index 6024cf374..bbf196e10 160000 --- a/mailsync +++ b/mailsync @@ -1 +1 @@ -Subproject commit 6024cf3744aca7448e623e1ec3f9820e1fe5a3d9 +Subproject commit bbf196e106cbf858015748b5b04b2ca537253d5f