+
@@ -94,29 +89,15 @@ class PreferencesAccounts extends React.Component
tokens
_onAddAccount: =>
- require('remote').getGlobal('application').windowManager.newOnboardingWindow()
+ ipc = require('ipc')
+ ipc.send('command', 'application:add-account')
_onAccountChange: =>
@setState(@getStateFromStores())
_onUnlinkAccount: (account) =>
- return [] unless @props.config
-
- tokens = @props.config.get('tokens') || []
- token = _.find tokens, (t) ->
- t.provider is 'nylas' and t.identifier is account.emailAddress
- tokens = _.without(tokens, token)
-
- if not token
- console.warn("Could not find nylas token for email address #{account.emailAddress}")
- return
-
- DatabaseStore.unpersistModel(account).then =>
- # TODO: Delete other mail data
- EdgehillAPI.unlinkToken(token)
+ AccountStore.removeAccountId(account.id)
_onUnlinkToken: (token) =>
- EdgehillAPI.unlinkToken(token)
- return
module.exports = PreferencesAccounts
diff --git a/menus/darwin.cson b/menus/darwin.cson
index 47d9923f3..a113f5d0f 100644
--- a/menus/darwin.cson
+++ b/menus/darwin.cson
@@ -6,7 +6,7 @@
{ type: 'separator' }
{ label: 'Preferences', command: 'application:open-preferences' }
{ type: 'separator' }
- { label: 'Add Account...', command: 'atom-workspace:add-account' }
+ { label: 'Add Account...', command: 'application:add-account' }
{ label: 'VERSION', enabled: false }
{ label: 'Restart and Install Update', command: 'application:install-update', visible: false}
{ label: 'Check for Update', command: 'application:check-for-update', visible: false}
diff --git a/menus/linux.cson b/menus/linux.cson
index c66355f1b..3df7dc11d 100644
--- a/menus/linux.cson
+++ b/menus/linux.cson
@@ -5,7 +5,7 @@
{ label: '&New Message', command: 'application:new-message' }
{ type: 'separator' }
{ label: 'Preferences', command: 'application:open-preferences' }
- { label: 'Add Account...', command: 'atom-workspace:add-account' }
+ { label: 'Add Account...', command: 'application:add-account' }
{ label: 'Clos&e Window', command: 'window:close' }
{ label: 'Quit', command: 'application:quit' }
]
diff --git a/menus/win32.cson b/menus/win32.cson
index 1506c7138..76b5362aa 100644
--- a/menus/win32.cson
+++ b/menus/win32.cson
@@ -1,5 +1,5 @@
'menu': [
- { label: 'Add Account...', command: 'atom-workspace:add-account' }
+ { label: 'Add Account...', command: 'application:add-account' }
{
label: '&Edit'
submenu: [
diff --git a/spec-nylas/stores/account-store-spec.coffee b/spec-nylas/stores/account-store-spec.coffee
index 32e31ce21..8fbfb9780 100644
--- a/spec-nylas/stores/account-store-spec.coffee
+++ b/spec-nylas/stores/account-store-spec.coffee
@@ -1,5 +1,6 @@
_ = require 'underscore'
AccountStore = require '../../src/flux/stores/account-store'
+Account = require '../../src/flux/models/account'
describe "AccountStore", ->
beforeEach ->
@@ -9,17 +10,38 @@ describe "AccountStore", ->
afterEach ->
@instance.stopListeningToAll()
- it "should initialize current() using data saved in config", ->
- state =
- "id": "123",
- "email_address":"bengotow@gmail.com",
- "object":"account"
- "organization_unit": "label"
+ it "should initialize using data saved in config", ->
+ accounts =
+ [{
+ "id": "123",
+ "client_id" : 'local-4f9d476a-c173',
+ "server_id" : '123',
+ "email_address":"bengotow@gmail.com",
+ "object":"account"
+ "organization_unit": "label"
+ },{
+ "id": "1234",
+ "client_id" : 'local-4f9d476a-c175',
+ "server_id" : '1234',
+ "email_address":"ben@nylas.com",
+ "object":"account"
+ "organization_unit": "label"
+ }]
- spyOn(atom.config, 'get').andCallFake -> state
+ spyOn(atom.config, 'get').andCallFake (key) ->
+ if key is 'nylas.accounts'
+ return accounts
+ else if key is 'nylas.currentAccountIndex'
+ return 1
@instance = new @constructor
- expect(@instance.current().id).toEqual(state['id'])
- expect(@instance.current().emailAddress).toEqual(state['email_address'])
+
+ expect(@instance.items()).toEqual([
+ (new Account).fromJSON(accounts[0]),
+ (new Account).fromJSON(accounts[1])
+ ])
+ expect(@instance.current() instanceof Account).toBe(true)
+ expect(@instance.current().id).toEqual(accounts[1]['id'])
+ expect(@instance.current().emailAddress).toEqual(accounts[1]['email_address'])
it "should initialize current() to null if data is not present", ->
spyOn(atom.config, 'get').andCallFake -> null
diff --git a/src/atom.coffee b/src/atom.coffee
index a13419a96..6ac872048 100644
--- a/src/atom.coffee
+++ b/src/atom.coffee
@@ -631,18 +631,7 @@ class Atom extends Model
CommandInstaller.installApmCommand resourcePath, false, (error) ->
console.warn error.message if error?
@commands.add 'atom-workspace',
- 'atom-workspace:add-account': @onAddAccount
-
- onAddAccount: =>
- @newWindow
- title: 'Add an Account'
- width: 340
- height: 550
- toolbar: false
- resizable: false
- windowType: 'onboarding'
- windowProps:
- page: 'add-account'
+ 'atom-workspace:add-account': @addAccount
# Call this method when establishing a secondary application window
# displaying a specific set of packages.
@@ -777,6 +766,17 @@ class Atom extends Model
executeJavaScriptInDevTools: (code) ->
ipc.send('call-window-method', 'executeJavaScriptInDevTools', code)
+ addAccount: =>
+ @newWindow
+ title: 'Add an Account'
+ width: 340
+ height: 550
+ toolbar: false
+ resizable: false
+ windowType: 'onboarding'
+ windowProps:
+ page: 'add-account'
+
###
Section: Private
###
diff --git a/src/browser/application.coffee b/src/browser/application.coffee
index dd9fa5dc2..b1244fa0f 100644
--- a/src/browser/application.coffee
+++ b/src/browser/application.coffee
@@ -154,31 +154,31 @@ class Application
app.commandLine.appendSwitch 'js-flags', '--harmony'
openWindowsForTokenState: (loadingMessage) =>
- hasToken = @config.get('tokens')?.length > 0
- if hasToken
+ hasAccount = @config.get('nylas.accounts')?.length > 0
+ if hasAccount
@windowManager.showMainWindow(loadingMessage)
@windowManager.ensureWorkWindow()
else
- @windowManager.newOnboardingWindow()
+ @windowManager.newOnboardingWindow({welcome: true})
# The onboarding window automatically shows when it's ready
_resetConfigAndRelaunch: =>
@setDatabasePhase('close')
@windowManager.closeAllWindows()
@_deleteDatabase =>
- @config.set('tokens', null)
@config.set('nylas', null)
@config.set('edgehill', null)
@setDatabasePhase('setup')
- @openWindowsForTokenState()
+ @windowManager.newOnboardingWindow({welcome: true})
_deleteDatabase: (callback) ->
@deleteFileWithRetry path.join(configDirPath,'edgehill.db'), callback
@deleteFileWithRetry path.join(configDirPath,'edgehill.db-wal')
@deleteFileWithRetry path.join(configDirPath,'edgehill.db-shm')
- _loginSuccessful: =>
- @openWindowsForTokenState()
+ _accountSetupSuccessful: =>
+ @windowManager.showMainWindow()
+ @windowManager.ensureWorkWindow()
@windowManager.mainWindow().waitForLoad =>
@windowManager.onboardingWindow()?.close()
@@ -247,6 +247,7 @@ class Application
atomWindow ?= @windowManager.focusedWindow()
atomWindow?.browserWindow.inspectElement(x, y)
+ @on 'application:add-account', => @windowManager.newOnboardingWindow()
@on 'application:new-message', => @windowManager.sendToMainWindow('new-message')
@on 'application:send-feedback', => @windowManager.sendToMainWindow('send-feedback')
@on 'application:open-preferences', => @windowManager.sendToMainWindow('open-preferences')
@@ -257,6 +258,7 @@ class Application
@quitting = true
@windowManager.unregisterAllHotWindows()
@autoUpdateManager.install()
+
@on 'application:open-dev', =>
@devMode = true
@windowManager.closeAllWindows()
@@ -375,8 +377,8 @@ class Application
clipboard ?= require 'clipboard'
clipboard.writeText(selectedText, 'selection')
- ipc.on 'login-successful', (event) =>
- @_loginSuccessful()
+ ipc.on 'account-setup-successful', (event) =>
+ @_accountSetupSuccessful()
ipc.on 'run-in-window', (event, params) =>
@_sourceWindows ?= {}
diff --git a/src/browser/window-manager.coffee b/src/browser/window-manager.coffee
index b3e18420f..097b6e585 100644
--- a/src/browser/window-manager.coffee
+++ b/src/browser/window-manager.coffee
@@ -132,17 +132,23 @@ class WindowManager
# Returns a new onboarding window
#
- newOnboardingWindow: ->
- @newWindow
- title: 'Welcome to Nylas'
+ newOnboardingWindow: ({welcome} = {}) ->
+ options =
toolbar: false
resizable: false
hidden: true
+ title: 'Add an Account'
windowType: 'onboarding'
windowProps:
- page: "welcome"
+ page: 'account-choose'
uniqueId: 'onboarding'
+ if welcome
+ options.title = "Welcome to N1"
+ options.windowProps.page = "welcome"
+
+ @newWindow(options)
+
# Makes a new window appear of a certain `windowType`.
#
# In almost all cases, instead of booting up a new window from scratch,
diff --git a/src/flux/edgehill-api.coffee b/src/flux/edgehill-api.coffee
index fb40ce5f6..f767e9436 100644
--- a/src/flux/edgehill-api.coffee
+++ b/src/flux/edgehill-api.coffee
@@ -13,14 +13,6 @@ class EdgehillAPI
constructor: ->
atom.config.onDidChange('env', @_onConfigChanged)
@_onConfigChanged()
-
- # Always ask Edgehill Server for our tokens at launch. This way accounts
- # added elsewhere will appear, and we'll also handle the 0.2.5=>0.3.0 upgrade.
- if atom.isWorkWindow()
- existing = @_getCredentials()
- if existing and existing.username
- @setUserIdentifierAndRetrieveTokens(existing.username)
-
@
_onConfigChanged: =>
@@ -72,48 +64,6 @@ class EdgehillAPI
else
options.success(body) if options.success
- urlForConnecting: (provider, email_address = '') ->
- auth = @_getCredentials()
- root = @APIRoot
- token = auth?.username
- "#{root}/connect/#{provider}?login_hint=#{email_address}&token=#{token}"
-
- setUserIdentifierAndRetrieveTokens: (user_identifier) ->
- @_setCredentials(username: user_identifier, password: '')
- @request
- path: "/users/me"
- success: (userData={}) =>
- @setTokens(userData.tokens)
- if atom.getWindowType() is 'onboarding'
- ipc = require 'ipc'
- ipc.send('login-successful')
- error: (apiError) =>
- console.error apiError
-
- setTokens: (incoming) ->
- # todo: remove once the edgehill-server inbox service is called `nylas`
- for token in incoming
- if token.provider is 'inbox'
- token.provider = 'nylas'
- atom.config.set('tokens', incoming)
- atom.config.save()
-
- unlinkToken: (token) ->
- @request
- path: "/users/token/#{token.id}"
- method: 'DELETE'
- success: =>
- tokens = atom.config.get('tokens') || []
- tokens = _.reject tokens, (t) -> t.id is token.id
- atom.config.set('tokens', tokens)
-
- accessTokenForProvider: (provider) ->
- tokens = atom.config.get('tokens') || []
- for token in tokens
- if token.provider is provider
- return token.access_token
- return null
-
_getCredentials: ->
atom.config.get('edgehill.credentials')
diff --git a/src/flux/nylas-api.coffee b/src/flux/nylas-api.coffee
index 4cb568806..ff03807d5 100644
--- a/src/flux/nylas-api.coffee
+++ b/src/flux/nylas-api.coffee
@@ -123,16 +123,11 @@ class NylasAPI
@_optimisticChangeTracker = new NylasAPIOptimisticChangeTracker()
atom.config.onDidChange('env', @_onConfigChanged)
- atom.config.onDidChange('tokens', @_onConfigChanged)
@_onConfigChanged()
_onConfigChanged: =>
prev = {@AppID, @APIRoot, @APITokens}
- tokens = atom.config.get('tokens') || []
- tokens = tokens.filter (t) -> t.provider is 'nylas'
- @APITokens = tokens.map (t) -> t.access_token
-
env = atom.config.get('env')
if not env
env = 'production'
@@ -154,13 +149,6 @@ class NylasAPI
current = {@AppID, @APIRoot, @APITokens}
- if atom.isWorkWindow() and not _.isEqual(prev, current)
- @APITokens.forEach (token) =>
- @makeRequest
- path: "/account"
- auth: {'user': token, 'pass': '', sendImmediately: true}
- returnsModel: true
-
# Delegates to node's request object.
# On success, it will call the passed in success callback with options.
# On error it will create a new APIError object that wraps the error,
@@ -356,17 +344,8 @@ class NylasAPI
decrementOptimisticChangeCount: (klass, id) ->
@_optimisticChangeTracker.decrement(klass, id)
- tokenObjectForAccountId: (aid) ->
- AccountStore = require './stores/account-store'
- accounts = AccountStore.items() || []
- account = _.find accounts, (acct) -> acct.id is aid
- return null unless account
-
- tokens = atom.config.get('tokens') || []
- token = _.find tokens, (t) -> t.provider is 'nylas' and t.identifier is account.emailAddress
- return token
-
accessTokenForAccountId: (aid) ->
- @tokenObjectForAccountId(aid)?.access_token
+ AccountStore = require './stores/account-store'
+ AccountStore.tokenForAccountId(aid)
module.exports = new NylasAPI()
diff --git a/src/flux/stores/account-store.coffee b/src/flux/stores/account-store.coffee
index 94b66c3d5..b266eff2c 100644
--- a/src/flux/stores/account-store.coffee
+++ b/src/flux/stores/account-store.coffee
@@ -6,7 +6,9 @@ _ = require 'underscore'
{Listener, Publisher} = require '../modules/reflux-coffee'
CoffeeHelpers = require '../coffee-helpers'
-saveStateKey = "nylas.current_account"
+saveObjectsKey = "nylas.accounts"
+saveTokensKey = "nylas.accountTokens"
+saveIndexKey = "nylas.currentAccountIndex"
###
Public: The AccountStore listens to changes to the available accounts in
@@ -21,49 +23,63 @@ class AccountStore
@include Listener
constructor: ->
- @_items = []
- @_current = null
- @_accounts = []
-
- saveState = atom.config.get(saveStateKey)
- if saveState and _.isObject(saveState)
- savedAccount = (new Account).fromJSON(saveState)
- if savedAccount.usesLabels() or savedAccount.usesFolders()
- @_setCurrent(savedAccount)
- @_accounts = [@_current]
-
+ @_load()
@listenTo Actions.selectAccountId, @onSelectAccountId
- @listenTo DatabaseStore, @onDataChanged
+ atom.config.observe saveTokensKey, (updatedTokens) =>
+ return if _.isEqual(updatedTokens, @_tokens)
+ newAccountIds = _.keys(_.omit(updatedTokens, _.keys(@_tokens)))
+ if newAccountIds.length > 0
+ Actions.selectAccountId(newAccountIds[0])
+ @_load()
- @populateItems()
+ _load: =>
+ @_accounts = []
+ for json in atom.config.get(saveObjectsKey) || []
+ @_accounts.push((new Account).fromJSON(json))
- populateItems: =>
- DatabaseStore.findAll(Account).order(Account.attributes.emailAddress.descending()).then (accounts) =>
- current = _.find accounts, (a) -> a.id is @_current?.id
- current = accounts?[0] unless current
+ index = atom.config.get(saveIndexKey) || 0
+ @_index = Math.min(@_accounts.length - 1, Math.max(0, index))
- if not _.isEqual(current, @_current) or not _.isEqual(accounts, @_accounts)
- @_setCurrent(current)
- @_accounts = accounts
- @trigger()
+ @_tokens = atom.config.get(saveTokensKey) || {}
+ @trigger()
- .catch (err) =>
- console.warn("Request for accounts failed. #{err}", err.stack)
-
- _setCurrent: (current) =>
- atom.config.set(saveStateKey, current)
- @_current = current
+ _save: =>
+ atom.config.set(saveObjectsKey, @_accounts)
+ atom.config.set(saveIndexKey, @_index)
+ atom.config.set(saveTokensKey, @_tokens)
+ atom.config.save()
# Inbound Events
- onDataChanged: (change) =>
- return unless change && change.objectClass is Account.name
- @populateItems()
-
onSelectAccountId: (id) =>
- return if @_current?.id is id
- @_current = _.find @_accounts, (a) -> a.id is id
- @trigger(@)
+ idx = _.findIndex @_accounts, (a) -> a.id is id
+ return if idx is -1
+ atom.config.set(saveIndexKey, idx)
+ @_index = idx
+ @trigger()
+
+ removeAccountId: (id) =>
+ idx = _.findIndex @_accounts, (a) -> a.id is id
+ return if idx is -1
+
+ delete @_tokens[id]
+ @_accounts.splice(idx, 1)
+ @_save()
+
+ if @_accounts.length is 0
+ ipc = require('ipc')
+ ipc.send('command', 'application:reset-config-and-relaunch')
+ else
+ if @_index is idx
+ Actions.selectAccountId(@_accounts[0].id)
+ @trigger()
+
+ addAccountFromJSON: (json) =>
+ return if @_tokens[json.id]
+ @_tokens[json.id] = json.auth_token
+ @_accounts.push((new Account).fromJSON(json))
+ @_save()
+ @trigger()
# Exposed Data
@@ -73,6 +89,11 @@ class AccountStore
# Public: Returns the currently active {Account}.
current: =>
- @_current
+ @_accounts[@_index] || null
+
+ # Private: This method is going away soon, do not rely on it.
+ #
+ tokenForAccountId: (id) =>
+ @_tokens[id]
module.exports = new AccountStore()
diff --git a/static/images/onboarding/sending-spinner.gif b/static/images/onboarding/sending-spinner.gif
new file mode 100644
index 000000000..1c72ebb55
Binary files /dev/null and b/static/images/onboarding/sending-spinner.gif differ