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'