diff --git a/internal_packages/onboarding/lib/onboarding-store.es6 b/internal_packages/onboarding/lib/onboarding-store.es6 index e285d0901..b0d29f12a 100644 --- a/internal_packages/onboarding/lib/onboarding-store.es6 +++ b/internal_packages/onboarding/lib/onboarding-store.es6 @@ -1,8 +1,8 @@ import OnboardingActions from './onboarding-actions'; -import {AccountStore, Actions, NylasAPI} from 'nylas-exports'; +import {AccountStore, Actions, IdentityStore} from 'nylas-exports'; import {shell, ipcRenderer} from 'electron'; import NylasStore from 'nylas-store'; -import AccountTypes from './account-types'; +import {accountTypeForProvider} from './account-types'; import {buildWelcomeURL} from './onboarding-helpers'; class OnboardingStore extends NylasStore { @@ -16,23 +16,22 @@ class OnboardingStore extends NylasStore { this.listenTo(OnboardingActions.setAccountInfo, this._onSetAccountInfo); this.listenTo(OnboardingActions.setAccountType, this._onSetAccountType); - const {page, existingAccount} = NylasEnv.getWindowProps(); + const {existingAccount} = NylasEnv.getWindowProps(); if (existingAccount) { + const accountType = accountTypeForProvider(existingAccount.provider); this._pageStack = ['account-choose'] this._accountInfo = { name: existingAccount.name, - email: existingAccount.email, + email: existingAccount.emailAddress, }; - - const accountType = AccountTypes.accountTypeForProvider(existingAccount.provider); this._onSetAccountType(accountType); } else { - this._pageStack = [page || 'welcome']; - const N1Account = NylasAPI.N1UserAccount(); - if (N1Account) { + const identity = IdentityStore.identity(); + this._pageStack = ['welcome']; + if (identity) { this._accountInfo = { - name: `${N1Account.firstname || ""} ${N1Account.lastname || ""}`, + name: `${identity.firstname || ""} ${identity.lastname || ""}`, }; } else { this._accountInfo = {}; @@ -81,7 +80,7 @@ class OnboardingStore extends NylasStore { _onAuthenticationJSONReceived = (json) => { const isFirstAccount = AccountStore.accounts().length === 0; - NylasAPI.setN1UserAccount(json); + Actions.setNylasIdentity(json); setTimeout(() => { if (isFirstAccount) { diff --git a/internal_packages/onboarding/lib/page-authenticate.jsx b/internal_packages/onboarding/lib/page-authenticate.jsx index 04b0beeeb..3783ceebe 100644 --- a/internal_packages/onboarding/lib/page-authenticate.jsx +++ b/internal_packages/onboarding/lib/page-authenticate.jsx @@ -5,7 +5,7 @@ import {RetinaImg} from 'nylas-component-kit'; import OnboardingActions from './onboarding-actions'; import networkErrors from 'chromium-net-errors'; -class AuthenticateLoadingCover extends React.Component { +class InitialLoadingCover extends React.Component { static propTypes = { ready: React.PropTypes.bool, error: React.PropTypes.string, @@ -40,7 +40,7 @@ class AuthenticateLoadingCover extends React.Component { if (this.props.error) { message = this.props.error; } else if (this.state.slow) { - message = "Still trying to reach Nylas.com..."; + message = "Still trying to reach Nylas…"; } else { message = ' ' } @@ -79,7 +79,7 @@ export default class AuthenticatePage extends React.Component { componentDidMount() { const webview = ReactDOM.findDOMNode(this.refs.webview); - webview.src = "https://billing-staging.nylas.com/onboarding"; + webview.src = `${IdentityStore.URLRoot}/onboarding`; webview.addEventListener('did-start-loading', this.webviewDidStartLoading); webview.addEventListener('did-fail-load', this.webviewDidFailLoad); webview.addEventListener('did-finish-load', this.webviewDidFinishLoad); @@ -94,7 +94,7 @@ export default class AuthenticatePage extends React.Component { } webviewDidStartLoading = () => { - this.setState({error: null}); + this.setState({error: null, webviewLoading: true}); } webviewDidFailLoad = ({errorCode, errorDescription, validatedURL}) => { @@ -108,7 +108,7 @@ export default class AuthenticatePage extends React.Component { const e = networkErrors.createByCode(errorCode); error = `Could not reach ${validatedURL}. ${e ? e.message : errorCode}`; } - this.setState({ready: false, error: error}); + this.setState({ready: false, error: error, webviewLoading: false}); } webviewDidFinishLoad = () => { @@ -122,7 +122,7 @@ export default class AuthenticatePage extends React.Component { const webview = ReactDOM.findDOMNode(this.refs.webview); webview.executeJavaScript(js, false, (result) => { - this.setState({ready: true}); + this.setState({ready: true, webviewLoading: false}); if (result !== null) { OnboardingActions.authenticationJSONReceived(JSON.parse(result)); } @@ -133,7 +133,14 @@ export default class AuthenticatePage extends React.Component { return (
- + +
+ { + const identity = IdentityStore.identity(); + if (!identity) { + return; + } + + if (!this.props.destination.startsWith('/')) { + throw new Error("destination must start with a leading slash."); + } + + this.setState({loading: true}); + + request({ + method: 'POST', + url: `${IdentityStore.URLRoot}/n1/login-link`, + json: true, + body: { + destination: this.props.destination, + account_token: identity.token, + }, + }, (error, response = {}, body) => { + this.setState({loading: false}); + if (error || !body.startsWith('http')) { + // Single-sign on attempt failed. Rather than churn the user right here, + // at least try to open the page directly in the browser. + shell.openExternal(`${IdentityStore.URLRoot}${this.props.destination}`); + } else { + shell.openExternal(body); + } + }); + } + + render() { + if (this.state.loading) { + return ( +
+ +  {this.props.label}… +
+ ); + } + return ( +
{this.props.label}
+ ); + } +} + +class PreferencesIdentity extends React.Component { + + static displayName = 'PreferencesIdentity'; + + constructor() { + super(); + this.state = this.getStateFromStores(); + } + + componentDidMount() { + this.unsubscribe = IdentityStore.listen(() => { + this.setState(this.getStateFromStores()); + }); + } + + componentWillUnmount() { + this.unsubscribe(); + } + + getStateFromStores() { + return { + identity: IdentityStore.identity(), + }; + } + + render() { + return ( +
+
+ {JSON.stringify(this.state.identity)} + +
+
+ ); + } + +} + +export default PreferencesIdentity; diff --git a/src/browser/application.es6 b/src/browser/application.es6 index 4f31d1426..3cf57278f 100644 --- a/src/browser/application.es6 +++ b/src/browser/application.es6 @@ -198,26 +198,31 @@ export default class Application extends EventEmitter { } else { this.windowManager.ensureWindow(WindowManager.ONBOARDING_WINDOW, { title: "Welcome to N1", - windowProps: { - page: "welcome", - }, }); this.windowManager.ensureWindow(WindowManager.WORK_WINDOW); } } - _resetConfigAndRelaunch = () => { + _relaunchToInitialWindows = ({resetConfig, resetDatabase} = {}) => { this.setDatabasePhase('close'); this.windowManager.destroyAllWindows(); - this._deleteDatabase(() => { - this.config.set('nylas', null); - this.config.set('edgehill', null); + + let fn = (callback) => callback() + if (resetDatabase) { + fn = this._deleteDatabase; + } + + fn(() => { + if (resetConfig) { + this.config.set('nylas', null); + this.config.set('edgehill', null); + } this.setDatabasePhase('setup'); this.openWindowsForTokenState(); }); } - _deleteDatabase(callback) { + _deleteDatabase = (callback) => { this.deleteFileWithRetry(path.join(this.configDirPath, 'edgehill.db'), callback); this.deleteFileWithRetry(path.join(this.configDirPath, 'edgehill.db-wal')); this.deleteFileWithRetry(path.join(this.configDirPath, 'edgehill.db-shm')); @@ -293,7 +298,7 @@ export default class Application extends EventEmitter { }); }); - this.on('application:reset-config-and-relaunch', this._resetConfigAndRelaunch); + this.on('application:relaunch-to-initial-windows', this._relaunchToInitialWindows); this.on('application:quit', () => { app.quit() @@ -310,12 +315,10 @@ export default class Application extends EventEmitter { this.on('application:add-account', ({existingAccount} = {}) => { this.windowManager.ensureWindow(WindowManager.ONBOARDING_WINDOW, { title: "Add an Account", - windowProps: { - page: "account-choose", - pageData: {existingAccount}, - }, + windowProps: { existingAccount }, }) }); + this.on('application:new-message', () => { this.windowManager.sendToWindow(WindowManager.MAIN_WINDOW, 'new-message'); }); diff --git a/src/flux/actions.coffee b/src/flux/actions.coffee index 71b81b22a..77235f399 100644 --- a/src/flux/actions.coffee +++ b/src/flux/actions.coffee @@ -152,6 +152,12 @@ class Actions ### @clearDeveloperConsole: ActionScopeWindow + ### + Public: Manage the Nylas identity + ### + @setNylasIdentity: ActionScopeWindow + @logoutNylasIdentity: ActionScopeWindow + ### Public: Remove the selected account diff --git a/src/flux/nylas-api.coffee b/src/flux/nylas-api.coffee index 540a1c4cd..beddf2e35 100644 --- a/src/flux/nylas-api.coffee +++ b/src/flux/nylas-api.coffee @@ -59,12 +59,6 @@ class NylasAPI NylasEnv.config.onDidChange('env', @_onConfigChanged) @_onConfigChanged() - N1UserAccount: => - NylasEnv.config.get('nylas.identity') - - setN1UserAccount: (n1Account) => - NylasEnv.config.set('nylas.identity', n1Account) - _onConfigChanged: => prev = {@AppID, @APIRoot, @APITokens} diff --git a/src/flux/stores/account-store.coffee b/src/flux/stores/account-store.coffee index 049f8c844..f93a4f601 100644 --- a/src/flux/stores/account-store.coffee +++ b/src/flux/stores/account-store.coffee @@ -147,7 +147,9 @@ class AccountStore extends NylasStore if remainingAccounts.length is 0 ipc = require('electron').ipcRenderer - ipc.send('command', 'application:reset-config-and-relaunch') + ipc.send('command', 'application:relaunch-to-initial-windows', { + resetDatabase: true, + }) _onReorderAccount: (id, newIdx) => existingIdx = _.findIndex @_accounts, (a) -> a.id is id diff --git a/src/flux/stores/identity-store.es6 b/src/flux/stores/identity-store.es6 new file mode 100644 index 000000000..6c381255f --- /dev/null +++ b/src/flux/stores/identity-store.es6 @@ -0,0 +1,51 @@ +import NylasStore from 'nylas-store'; +import Actions from '../actions'; +import keytar from 'keytar'; +import {ipcRenderer} from 'electron'; + +const configIdentityKey = "nylas.identity"; +const keytarServiceName = 'Nylas'; +const keytarIdentityKey = 'Nylas Account'; + +class IdentityStore extends NylasStore { + + constructor() { + super(); + + this.URLRoot = "https://billing-staging.nylas.com"; + + this.listenTo(Actions.setNylasIdentity, this._onSetNylasIdentity); + this.listenTo(Actions.logoutNylasIdentity, this._onLogoutNylasIdentity); + + NylasEnv.config.onDidChange(configIdentityKey, () => { + this._loadIdentity(); + this.trigger(); + }); + this._loadIdentity(); + } + + _loadIdentity() { + this._identity = NylasEnv.config.get(configIdentityKey); + if (this._identity) { + this._identity.token = keytar.getPassword(keytarServiceName, keytarIdentityKey); + } + } + + identity() { + return this._identity; + } + + _onLogoutNylasIdentity = () => { + keytar.deletePassword(keytarServiceName, keytarIdentityKey); + NylasEnv.config.unset(configIdentityKey); + ipcRenderer.send('command', 'application:relaunch-to-initial-windows'); + } + + _onSetNylasIdentity = (identity) => { + keytar.replacePassword(keytarServiceName, keytarIdentityKey, identity.token); + delete identity.token; + NylasEnv.config.set(configIdentityKey, identity); + } +} + +export default new IdentityStore() diff --git a/src/global/nylas-exports.coffee b/src/global/nylas-exports.coffee index c349a459b..7289d3cdd 100644 --- a/src/global/nylas-exports.coffee +++ b/src/global/nylas-exports.coffee @@ -122,6 +122,7 @@ class NylasExports @lazyLoadAndRegisterStore "AccountStore", 'account-store' @lazyLoadAndRegisterStore "MessageStore", 'message-store' @lazyLoadAndRegisterStore "ContactStore", 'contact-store' + @lazyLoadAndRegisterStore "IdentityStore", 'identity-store' @lazyLoadAndRegisterStore "MetadataStore", 'metadata-store' @lazyLoadAndRegisterStore "CategoryStore", 'category-store' @lazyLoadAndRegisterStore "UndoRedoStore", 'undo-redo-store'