fix(onboarding): Lots of changes to account management, dead code cleanup

Summary:
Better error handling in the account settings page and a loading spinner

Add Account... replaces "Link External Account", and it works

Clean dead code from onboarding pages, remove base class component

Always show the account switcher

rm dead EdgehillAPI code, AccountStore now manages accounts and credentials in config, not in database

Fix specs

Test Plan: Run tests

Reviewers: dillon, evan

Reviewed By: evan

Projects: #edgehill

Differential Revision: https://phab.nylas.com/D2059
This commit is contained in:
Ben Gotow 2015-09-24 14:51:15 -07:00
parent f85b5d60ac
commit 297320df94
25 changed files with 249 additions and 329 deletions

View file

@ -20,8 +20,7 @@ class AccountSwitcher extends React.Component
unsubscribe() for unsubscribe in @unsubscribers
render: =>
return undefined if @state.accounts.length < 1
return false unless @state.account
<div id="account-switcher" tabIndex={-1} onBlur={@_onBlur} ref="button">
{@_renderAccount(@state.account, true)}
{@_renderDropdown()}
@ -125,8 +124,9 @@ class AccountSwitcher extends React.Component
@setState(showing: false)
_onAddAccount: =>
require('remote').getGlobal('application').windowManager.newOnboardingWindow()
@setState showing: false
ipc = require('ipc')
ipc.send('command', 'application:add-account')
@setState(showing: false)
_getStateFromStores: =>
accounts: AccountStore.items()

View file

@ -79,10 +79,13 @@
}
#account-switcher {
padding-top: @padding-large-vertical;
padding-bottom: @padding-base-vertical;
border-bottom: 1px solid @border-color-divider;
.primary-item {
padding-top: @padding-large-vertical;
padding-bottom: @padding-base-vertical;
}
.account {
position: relative;
margin-bottom: 0;

View file

@ -3,13 +3,12 @@ _ = require 'underscore'
{RetinaImg} = require 'nylas-component-kit'
{EdgehillAPI, Utils} = require 'nylas-exports'
Page = require './page'
OnboardingActions = require './onboarding-actions'
NylasApiEnvironmentStore = require './nylas-api-environment-store'
Providers = require './account-types'
url = require 'url'
class AccountChoosePage extends Page
class AccountChoosePage extends React.Component
@displayName: "AccountChoosePage"
constructor: (@props) ->
@ -27,7 +26,9 @@ class AccountChoosePage extends Page
render: =>
<div className="page account-choose">
{@_renderClose("quit")}
<div className="quit" onClick={ => atom.close() }>
<RetinaImg name="onboarding-close.png" mode={RetinaImg.Mode.ContentPreserve}/>
</div>
<div className="logo-container">
<RetinaImg name="onboarding-logo.png" mode={RetinaImg.Mode.ContentPreserve} className="logo"/>
@ -88,14 +89,6 @@ class AccountChoosePage extends Page
})
shell.openExternal(googleUrl)
_onSubmit: (e) =>
valid = React.findDOMNode(@refs.form).reportValidity()
if valid
url = EdgehillAPI.urlForConnecting("inbox", @state.email)
OnboardingActions.moveToPage("add-account-auth", {url})
else
e.preventDefault()
_environmentComponent: =>
return <div></div> unless atom.inDevMode()
<div className="environment-selector">

View file

@ -4,12 +4,11 @@ ipc = require 'ipc'
{RetinaImg} = require 'nylas-component-kit'
{EdgehillAPI, NylasAPI, APIError} = require 'nylas-exports'
Page = require './page'
OnboardingActions = require './onboarding-actions'
NylasApiEnvironmentStore = require './nylas-api-environment-store'
Providers = require './account-types'
class AccountSettingsPage extends Page
class AccountSettingsPage extends React.Component
@displayName: "AccountSettingsPage"
constructor: (@props) ->
@ -18,6 +17,8 @@ class AccountSettingsPage extends Page
settings: {}
fields: {}
pageNumber: 0
errorFieldNames: []
errorMessage: null
show_advanced: false
@props.pageData.provider.settings.forEach (field) =>
@ -36,7 +37,7 @@ class AccountSettingsPage extends Page
@_pollForGmailAccount((account) ->
if account?
done = true
OnboardingActions.nylasAccountReceived(account)
OnboardingActions.accountJSONReceived(account)
else if tries < 10 and id is poll_attempt_id
setTimeout(_retry, delay)
delay *= 1.5 # exponential backoff
@ -106,8 +107,8 @@ class AccountSettingsPage extends Page
</h2>
_renderErrorMessage: =>
if @state.error
<div className="errormsg">{@state.error.message ? ""}</div>
if @state.errorMessage
<div className="errormsg">{@state.errorMessage ? ""}</div>
_fieldOnCurrentPage: (field) =>
!@state.provider.pages || field.page is @state.pageNumber
@ -115,7 +116,7 @@ class AccountSettingsPage extends Page
_renderFields: =>
@state.provider.fields?.filter(@_fieldOnCurrentPage)
.map (field, idx) =>
errclass = if field.name in (@state.error?.invalid_fields ? []) then "error " else ""
errclass = if field.name in @state.errorFieldNames then "error " else ""
<label className={(field.className || "")} key={field.name}>
{field.label}
<input type={field.type}
@ -141,7 +142,7 @@ class AccountSettingsPage extends Page
{field.label}
</label>
else
errclass = if field.name in (@state.error?.invalid_settings ? []) then "error " else ""
errclass = if field.name in @state.errorFieldNames then "error " else ""
<label className={field.className ? ""}
style={if field.advanced and not @state.show_advanced then {display:'none'} else {}}
key={field.name}>
@ -157,16 +158,21 @@ class AccountSettingsPage extends Page
_renderButton: =>
pages = @state.provider.pages || []
if pages.length > @state.pageNumber+1
if pages.length > @state.pageNumber + 1
<button className="btn btn-large btn-gradient" type="button" onClick={@_onNextButton}>Next</button>
else if @state.provider.name isnt 'gmail'
<button className="btn btn-large btn-gradient" type="button" onClick={@_submit}>Set up account</button>
if @state.tryingToAuthenticate
<button className="btn btn-large btn-gradient btn-setup-spinning" type="button">
<RetinaImg name="sending-spinner.gif" width={15} height={15} mode={RetinaImg.Mode.ContentPreserve} /> Setting up&hellip;
</button>
else
<button className="btn btn-large btn-gradient" type="button" onClick={@_onSubmit}>Set up account</button>
_onNextButton: (event) =>
@setState(pageNumber: @state.pageNumber+1)
@setState(pageNumber: @state.pageNumber + 1)
@_resize()
_submit: (event) =>
_onSubmit: (event) =>
data = settings: {}
for own k,v of @state.fields when v isnt ''
data[k] = v
@ -178,6 +184,8 @@ class AccountSettingsPage extends Page
if data.provider in ['exchange','outlook'] and not data.settings.username?.trim().length
data.settings.username = data.email
@setState(tryingToAuthenticate: true)
# Send the form data directly to Nylas to get code
# If this succeeds, send the received code to Edgehill server to register the account
# Otherwise process the error message from the server and highlight UI as needed
@ -186,6 +194,7 @@ class AccountSettingsPage extends Page
method: 'POST'
body: data
returnsModel: false
timeout: 30000
auth:
user: ''
pass: ''
@ -196,51 +205,50 @@ class AccountSettingsPage extends Page
method: "POST"
body: json
success: (json) =>
OnboardingActions.nylasAccountReceived(json)
error: (err) =>
throw err
.catch APIError, (err) =>
err_page_numbers = [@state.pageNumber]
OnboardingActions.accountJSONReceived(json)
error: @_onNetworkError
.catch(@_onNetworkError)
if err.body.missing_fields?
err.body.invalid_fields = err.body.missing_fields
_onNetworkError: (err) =>
errorMessage = err.message
pageNumber = @state.pageNumber
errorFieldNames = err.body?.missing_fields || err.body?.missing_settings
missing_fields = []
for missing in err.body.missing_fields
for f in @state.provider.fields when f.name is missing
missing_fields.push(f.label.toLowerCase())
if f.page isnt undefined
err_page_numbers.push(f.page)
if errorFieldNames
{pageNumber, errorMessage} = @_stateForMissingFieldNames(errorFieldNames)
if err.statusCode is -123 # timeout
errorMessage = "Request timed out. Please try again."
err.body.message = @_missing_fields_message(missing_fields)
@setState
pageNumber: pageNumber
errorMessage: errorMessage
errorFieldNames: errorFieldNames || []
tryingToAuthenticate: false
@_resize()
else if err.body.missing_settings?
err.body.invalid_settings = err.body.missing_settings
_stateForMissingFieldNames: (fieldNames) ->
fieldLabels = []
fields = [].concat(@state.provider.settings, @state.provider.fields)
pageNumbers = [@state.pageNumber]
missing_settings = []
for missing in err.body.missing_settings
for s in @state.provider.settings when s.name is missing
missing_settings.push(s.label.toLowerCase())
if s.page isnt undefined
err_page_numbers.push(s.page)
for fieldName in fieldNames
for s in fields when s.name is fieldName
fieldLabels.push(s.label.toLowerCase())
if s.page isnt undefined
pageNumbers.push(s.page)
err.body.message = @_missing_fields_message(missing_settings)
pageNumber = Math.min.apply(null, pageNumbers)
errorMessage = @_messageForFieldLabels(fieldLabels)
console.log(Math.min.apply(err_page_numbers), err_page_numbers)
@setState(error: err.body, pageNumber: Math.min.apply(null,err_page_numbers))
@_resize()
console.log(err)
{pageNumber, errorMessage}
_missing_fields_message: (missing_settings) ->
if missing_settings.length > 2
_messageForFieldLabels: (labels) ->
if labels.length > 2
return "Please fix the highlighted fields."
else if missing_settings.length is 2
first = missing_settings[0]
last = missing_settings[1]
return "Please provide your #{first} and #{last}."
else if labels.length is 2
return "Please provide your #{labels[0]} and #{labels[1]}."
else
setting = missing_settings[0]
return "Please provide your #{setting}."
return "Please provide your #{labels[0]}."
_pollForGmailAccount: (callback) =>
EdgehillAPI.request

View file

@ -1,31 +0,0 @@
React = require 'react'
Page = require './page'
{RetinaImg} = require 'nylas-component-kit'
{EdgehillAPI} = require 'nylas-exports'
OnboardingActions = require './onboarding-actions'
class ConnectAccountPage extends Page
@displayName: "ConnectAccountPage"
render: =>
<div className="page">
{@_renderClose("close")}
<RetinaImg name="onboarding-logo.png" mode={RetinaImg.Mode.ContentPreserve} className="logo"/>
<h2>Connect an Account</h2>
<RetinaImg name="onboarding-divider.png" mode={RetinaImg.Mode.ContentPreserve} />
<div className="thin-container">
<div className="prompt">Link accounts from other services to supercharge your email.</div>
<p>No more external accounts to link. Add additional features with plugins to Nylas Mail.</p>
</div>
</div>
_fireAuthAccount: (service) =>
url = EdgehillAPI.urlForConnecting(service)
OnboardingActions.moveToPage "add-account-auth", {url}
module.exports = ConnectAccountPage

View file

@ -1,5 +1,4 @@
React = require 'react'
Page = require './page'
{RetinaImg, ConfigPropContainer} = require 'nylas-component-kit'
{EdgehillAPI} = require 'nylas-exports'
OnboardingActions = require './onboarding-actions'
@ -58,7 +57,7 @@ class InitialPackagesList extends React.Component
else
atom.packages.disablePackage(packageName)
class InitialPackagesPage extends Page
class InitialPackagesPage extends React.Component
@displayName: "InitialPackagesPage"
render: =>
@ -84,6 +83,6 @@ class InitialPackagesPage extends Page
_onGetStarted: =>
ipc = require 'ipc'
ipc.send('login-successful')
ipc.send('account-setup-successful')
module.exports = InitialPackagesPage

View file

@ -1,5 +1,4 @@
React = require 'react'
Page = require './page'
path = require 'path'
fs = require 'fs'
{RetinaImg, Flexbox, ConfigPropContainer} = require 'nylas-component-kit'
@ -77,7 +76,7 @@ class InitialPreferencesOptions extends React.Component
</div>
class InitialPreferencesPage extends Page
class InitialPreferencesPage extends React.Component
@displayName: "InitialPreferencesPage"
render: =>

View file

@ -6,7 +6,7 @@ OnboardingActions = Reflux.createActions [
"moveToPreviousPage"
"moveToPage"
"nylasAccountReceived"
"accountJSONReceived"
]
for key, action of OnboardingActions

View file

@ -1,5 +1,6 @@
Reflux = require 'reflux'
OnboardingActions = require './onboarding-actions'
{AccountStore} = require 'nylas-exports'
NylasStore = require 'nylas-store'
ipc = require 'ipc'
url = require 'url'
@ -8,7 +9,7 @@ return unless atom.getWindowType() is "onboarding"
class PageRouterStore extends NylasStore
constructor: ->
atom.onWindowPropsReceived @_onWindowPropsChagned
atom.onWindowPropsReceived @_onWindowPropsChanged
@_page = atom.getWindowProps().page ? ''
@_pageData = atom.getWindowProps().pageData ? {}
@ -17,20 +18,18 @@ class PageRouterStore extends NylasStore
@listenTo OnboardingActions.moveToPreviousPage, @_onMoveToPreviousPage
@listenTo OnboardingActions.moveToPage, @_onMoveToPage
@listenTo OnboardingActions.nylasAccountReceived, @_onNylasAccountReceived
@listenTo OnboardingActions.accountJSONReceived, @_onAccountJSONReceived
_onNylasAccountReceived: (account) =>
tokens = atom.config.get('tokens') || []
tokens.push({
provider: 'nylas'
identifier: account.email_address
access_token: account.auth_token
})
atom.config.set('tokens', tokens)
atom.config.save()
@_onMoveToPage('initial-preferences', {account})
_onAccountJSONReceived: (json) =>
isFirstAccount = AccountStore.items().length is 0
AccountStore.addAccountFromJSON(json)
atom.displayWindow()
if isFirstAccount
@_onMoveToPage('initial-preferences', {account: json})
else
ipc.send('account-setup-successful')
_onWindowPropsChagned: ({page, pageData}={}) =>
_onWindowPropsChanged: ({page, pageData}={}) =>
@_onMoveToPage(page, pageData)
page: -> @_page

View file

@ -23,21 +23,29 @@ class PageRouter extends React.Component
componentDidMount: =>
@unsubscribe = PageRouterStore.listen(@_onStateChanged, @)
setTimeout(@_initializeWindowSize, 10)
componentDidUpdate: =>
setTimeout(@_updateWindowSize, 10)
_initializeWindowSize: =>
return if @_unmounted
{width, height} = React.findDOMNode(@refs.activePage).getBoundingClientRect()
atom.center()
atom.setSizeAnimated(width, height, 0)
atom.show()
componentDidUpdate: =>
setTimeout(@_resizePage, 10)
_resizePage: =>
_updateWindowSize: =>
return if @_unmounted
{width, height} = React.findDOMNode(@refs.activePage).getBoundingClientRect()
atom.setSizeAnimated(width, height)
_onStateChanged: => @setState(@_getStateFromStore())
_onStateChanged: =>
@setState(@_getStateFromStore())
componentWillUnmount: => @unsubscribe?()
componentWillUnmount: =>
@_unmounted = true
@unsubscribe?()
render: =>
<div className="page-frame">
@ -72,7 +80,7 @@ class PageRouter extends React.Component
}[@state.page]
<div key={@state.page} className="page-container">
<Component pageData={@state.pageData} ref="activePage" onResize={@_resizePage}/>
<Component pageData={@state.pageData} ref="activePage" onResize={@_updateWindowSize}/>
</div>
_renderDragRegion: ->

View file

@ -1,34 +0,0 @@
React = require 'react'
{RetinaImg} = require 'nylas-component-kit'
class Page extends React.Component
@displayName: "Page"
constructor: (@props) ->
_renderClose: (action="close") ->
if action is "close"
onClick = -> atom.close()
else if action is "quit"
onClick = ->
require('ipc').send('command', 'application:quit')
else onClick = ->
<div className="quit" onClick={onClick}>
<RetinaImg name="onboarding-close.png" mode={RetinaImg.Mode.ContentPreserve}/>
</div>
_renderSpinner: ->
styles =
position: "absolute"
zIndex: 10
top: "50%"
left: "50%"
transform: 'translate(-50%, -50%)'
<RetinaImg ref="spinner"
style={styles}
name="Setup-Spinner.gif"
mode={RetinaImg.Mode.ContentPreserve}/>
module.exports = Page

View file

@ -1,9 +1,8 @@
React = require 'react'
Page = require './page'
{RetinaImg, TimeoutTransitionGroup} = require 'nylas-component-kit'
OnboardingActions = require './onboarding-actions'
class WelcomePage extends Page
class WelcomePage extends React.Component
@displayName: "WelcomePage"
constructor: (@props) ->
@ -17,7 +16,10 @@ class WelcomePage extends Page
buttons.push <button key="next" className="btn btn-large" onClick={@_onContinue}>Continue</button>
<div className="page no-top opaque" style={width: 667, display: "inline-block"}>
{@_renderClose("close")}
<div className="quit" onClick={ => atom.close() }>
<RetinaImg name="onboarding-close.png" mode={RetinaImg.Mode.ContentPreserve}/>
</div>
<TimeoutTransitionGroup leaveTimeout={300}
enterTimeout={300}
className="welcome-image-container"

View file

@ -30,6 +30,10 @@
font-size: 40pt;
}
h2 {
line-height: 1.3em;
}
h4 {
font-weight: 400;
font-size: 20pt;
@ -52,6 +56,9 @@
box-sizing: content-box;
}
.btn-setup-spinning {
img { vertical-align: top; margin-right: 5px; }
}
.prompt {
color:#5D5D5D;
font-size:1.07em;
@ -100,10 +107,11 @@
}
&.account-setup {
padding: 0 15px;
width: 300px
width: 330px
}
&.account-choose {
width: 300px;
width: 330px;
padding-bottom:10px;
}
&.opaque {
background-color: @gray-lighter;
@ -254,7 +262,8 @@
}
.errormsg {
color: #A33
color: #A33;
margin-bottom:5px;
}
form.settings {
@ -297,6 +306,10 @@
label.checkbox {
text-align: center;
}
.btn {
margin-top:8px;
}
}
.appearance-mode {

View file

@ -6,9 +6,6 @@ _ = require 'underscore'
class PreferencesAccounts extends React.Component
@displayName: 'PreferencesAccounts'
@propTypes:
config: React.PropTypes.object.isRequired
constructor: (@props) ->
@state = @getStateFromStores()
@ -31,8 +28,6 @@ class PreferencesAccounts extends React.Component
_renderAccounts: =>
return false unless @state.accounts
allowUnlinking = @state.accounts.length > 1
<div>
<div className="section-header">
Accounts:
@ -49,7 +44,7 @@ class PreferencesAccounts extends React.Component
<div className="account-name">{account.emailAddress}</div>
<div className="account-subtext">{account.name || "No name provided."} ({account.displayProvider()})</div>
</div>
<div style={textAlign:"right", marginTop:10, display: if allowUnlinking then 'inline-block' else 'none'}>
<div style={textAlign:"right", marginTop:10, display:'inline-block'}>
<button className="btn btn-large" onClick={ => @_onUnlinkAccount(account) }>Unlink</button>
</div>
</Flexbox>
@ -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

View file

@ -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}

View file

@ -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' }
]

View file

@ -1,5 +1,5 @@
'menu': [
{ label: 'Add Account...', command: 'atom-workspace:add-account' }
{ label: 'Add Account...', command: 'application:add-account' }
{
label: '&Edit'
submenu: [

View file

@ -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

View file

@ -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
###

View file

@ -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 ?= {}

View file

@ -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,

View file

@ -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')

View file

@ -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()

View file

@ -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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB