2015-09-23 11:11:51 +08:00
React = require 'react'
2016-03-29 16:41:24 +08:00
ReactDOM = require 'react-dom'
2016-01-16 03:27:14 +08:00
_ = require 'underscore'
2015-11-24 14:09:17 +08:00
{ipcRenderer, dialog, remote} = require 'electron'
2015-09-23 11:11:51 +08:00
{RetinaImg} = require 'nylas-component-kit'
2016-04-09 03:00:03 +08:00
{RegExpUtils, EdgehillAPI, NylasAPI, APIError, Actions, AccountStore} = require 'nylas-exports'
2015-09-23 11:11:51 +08:00
OnboardingActions = require './onboarding-actions'
NylasApiEnvironmentStore = require './nylas-api-environment-store'
Providers = require './account-types'
2015-09-25 05:51:15 +08:00
class AccountSettingsPage extends React.Component
2015-09-23 11:11:51 +08:00
@displayName: "AccountSettingsPage"
constructor: (@props) ->
@state =
provider: @props.pageData.provider
settings: {}
fields: {}
pageNumber: 0
2015-09-25 05:51:15 +08:00
errorFieldNames: []
errorMessage: null
2015-09-23 11:11:51 +08:00
show_advanced: false
@props.pageData.provider.settings.forEach (field) =>
if field.default?
@state.settings[field.name] = field.default
2015-10-05 18:27:28 +08:00
# Special case for gmail. Rather than showing a form, we poll in the
# background for completion of the gmail auth on the server.
2015-09-23 11:11:51 +08:00
if @state.provider.name is 'gmail'
2015-10-05 18:27:28 +08:00
pollAttemptId = 0
2015-09-23 11:11:51 +08:00
done = false
# polling with capped exponential backoff
delay = 1000
tries = 0
poll = (id,initial_delay) =>
_retry = =>
tries++
2015-10-05 18:27:28 +08:00
@_pollForGmailAccount((account_data) =>
if account_data?
2015-09-23 11:11:51 +08:00
done = true
2015-10-05 18:27:28 +08:00
{data} = account_data
account = JSON.parse(data)
2015-10-06 08:31:29 +08:00
@_onAccountReceived(account)
2015-10-05 18:27:28 +08:00
else if tries < 20 and id is pollAttemptId
2015-09-23 11:11:51 +08:00
setTimeout(_retry, delay)
2015-10-05 18:27:28 +08:00
delay *= 1.2 # exponential backoff
2015-09-23 11:11:51 +08:00
)
setTimeout(_retry,initial_delay)
2015-11-24 14:09:17 +08:00
ipcRenderer.on('browser-window-focus', ->
2015-09-23 11:11:51 +08:00
if not done # hack to deactivate this listener when done
2015-10-05 18:27:28 +08:00
pollAttemptId++
poll(pollAttemptId,0)
2015-09-23 11:11:51 +08:00
)
2015-10-05 18:27:28 +08:00
poll(pollAttemptId,5000)
2015-09-23 11:11:51 +08:00
render: ->
<div className="page account-setup">
<div className="logo-container">
2016-01-21 09:09:07 +08:00
<RetinaImg
name={@state.provider.header_icon}
mode={RetinaImg.Mode.ContentPreserve}
className="logo"/>
2015-09-23 11:11:51 +08:00
</div>
{@_renderTitle()}
<div className="back" onClick={@_fireMoveToPrevPage}>
2016-01-21 09:09:07 +08:00
<RetinaImg
name="onboarding-back.png"
mode={RetinaImg.Mode.ContentPreserve}/>
2015-09-23 11:11:51 +08:00
</div>
2016-01-21 09:09:07 +08:00
2015-09-23 11:11:51 +08:00
{@_renderErrorMessage()}
2016-01-21 09:09:07 +08:00
2015-09-23 11:11:51 +08:00
<form className="settings">
{@_renderFields()}
{@_renderSettings()}
{@_renderButton()}
</form>
</div>
2016-03-19 05:03:37 +08:00
componentDidMount: =>
@_applyFocus()
componentDidUpdate: =>
@_applyFocus()
_applyFocus: =>
2016-03-29 16:41:24 +08:00
firstInput = ReactDOM.findDOMNode(@).querySelector('input')
2016-03-19 05:03:37 +08:00
anyInputFocused = document.activeElement and document.activeElement.nodeName is 'INPUT'
if firstInput and not anyInputFocused
firstInput.focus()
2015-09-23 11:11:51 +08:00
_onSettingsChanged: (event) =>
2016-01-21 09:09:07 +08:00
# NOTE: This code is largely duplicated in _onValueChanged. TODO Fix!
{field, format} = event.target.dataset
intFormatter = (a) ->
2015-10-03 06:32:59 +08:00
i = parseInt(a)
if isNaN(i) then "" else i
2016-01-21 09:09:07 +08:00
formatter = if format is 'integer' then intFormatter else (a) -> a
2015-09-23 11:11:51 +08:00
settings = @state.settings
if event.target.type is 'checkbox'
settings[field] = event.target.checked
else
2015-10-03 06:32:59 +08:00
settings[field] = formatter(event.target.value)
2016-01-16 03:27:14 +08:00
2016-01-21 09:09:07 +08:00
settingField = _.findWhere(@state.provider.settings, {name: field})
2016-01-16 03:27:14 +08:00
# If the field defines an isValid method, try to validate
# the input.
2016-01-21 09:09:07 +08:00
if settingField
valueIsValid = not settingField.isValid? or settingField.isValid(event.target.value)
valueIsPresent = event.target.value and event.target.value.length > 0
valueIsRequired = settingField.required is true
if (not valueIsPresent and valueIsRequired) or (valueIsPresent and not valueIsValid)
errorFields = _.uniq(@state.errorFieldNames.concat([field]))
2016-01-16 03:27:14 +08:00
else
2016-01-21 09:09:07 +08:00
errorFields = _.uniq(_.without(@state.errorFieldNames, field))
@setState({errorFieldNames: errorFields})
2016-01-16 03:27:14 +08:00
2015-09-23 11:11:51 +08:00
@setState({settings})
2016-01-16 03:27:14 +08:00
_noFormErrors: =>
allFields = @state.provider.fields.concat(@state.provider.settings || [])
fieldsOnThisPage = allFields.filter(@_fieldOnCurrentPage)
fieldNames = _.pluck(fieldsOnThisPage, 'name')
return _.intersection(fieldNames, @state.errorFieldNames).length == 0
_fieldRequired: (f) =>
return f?.required == true
_allRequiredFieldsFilled: =>
allFields = @state.provider.fields.concat(@state.provider.settings || [])
requiredFields = allFields.filter(@_fieldOnCurrentPage).filter(@_fieldRequired)
2016-01-21 09:09:07 +08:00
fields = _.extend({}, @state.fields, @state.settings)
2016-01-16 03:27:14 +08:00
for field in requiredFields
fieldName = field['name']
if not (fieldName of fields) or fields[fieldName] == ''
return false
return true
2015-09-23 11:11:51 +08:00
_onValueChanged: (event) =>
2016-01-21 09:09:07 +08:00
# NOTE: This code is largely duplicated in _onSettingsChanged. TODO Fix!
2015-09-23 11:11:51 +08:00
field = event.target.dataset.field
fields = @state.fields
fields[field] = event.target.value
2016-01-16 03:27:14 +08:00
2016-01-21 09:09:07 +08:00
providerField = _.find(@state.provider.fields, ((e) -> return e['name'] == field))
2016-01-16 03:27:14 +08:00
# If the field defines an isValid method, try to validate
# the input.
2016-01-21 09:09:07 +08:00
if providerField
valueIsValid = not providerField.isValid? or providerField.isValid(event.target.value)
valueIsPresent = event.target.value and event.target.value.length > 0
valueIsRequired = providerField.required is true
if (not valueIsPresent and valueIsRequired) or (valueIsPresent and not valueIsValid)
errorFields = _.uniq(@state.errorFieldNames.concat([field]))
2016-01-16 03:27:14 +08:00
else
2016-01-21 09:09:07 +08:00
errorFields = _.uniq(_.without(@state.errorFieldNames, field))
@setState({errorFieldNames: errorFields})
2016-01-16 03:27:14 +08:00
2016-02-04 06:27:34 +08:00
if providerField.type == "email" and event.target.value
if event.target.value.endsWith('@gmail.com')
# set a state that contains a "this is a gmail account" message
2016-02-04 07:37:23 +08:00
errorFields = _.uniq(@state.errorFieldNames.concat([field]))
@setState
errorMessage: "This looks like a Gmail account. You should go back and sign in to Gmail instead."
errorFieldNames: errorFields
2016-02-04 06:27:34 +08:00
@_resize()
else
@setState({errorMessage: null})
@_resize()
2015-09-23 11:11:51 +08:00
@setState({fields})
2015-10-01 02:58:40 +08:00
_onFieldKeyPress: (event) =>
if event.key in ['Enter', 'Return']
2015-10-03 06:32:59 +08:00
pages = @state.provider.pages || []
2016-01-21 09:09:07 +08:00
if pages.length > @state.pageNumber + 1
2015-10-03 06:32:59 +08:00
@_onNextButton()
else
@_onSubmit()
2015-10-01 02:58:40 +08:00
2015-09-23 11:11:51 +08:00
_renderTitle: =>
if @state.provider.name is 'gmail'
<h2>
2016-01-21 09:09:07 +08:00
Sign in to Google in<br/>your browser.
2015-09-23 11:11:51 +08:00
</h2>
else if @state.provider.pages?.length > 0
<h2>
{@state.provider.pages[@state.pageNumber]}
</h2>
else
<h2>
Sign in to {@state.provider.displayName}
</h2>
_renderErrorMessage: =>
2016-03-11 04:00:05 +08:00
return unless @state.errorMessage
text = @state.errorMessage
result = RegExpUtils.urlRegex(matchEntireString: false).exec(text)
if result
link = result[0]
beforeText = text.substr(0, result.index)
afterText = text.substr(result.index + link.length)
return (
<div className="errormsg">
{beforeText}<a href={link}>{link}</a>{afterText}
</div>
)
else
return (
<div className="errormsg">
{text}
</div>
)
2015-09-23 11:11:51 +08:00
_fieldOnCurrentPage: (field) =>
!@state.provider.pages || field.page is @state.pageNumber
_renderFields: =>
@state.provider.fields?.filter(@_fieldOnCurrentPage)
.map (field, idx) =>
2015-09-25 05:51:15 +08:00
errclass = if field.name in @state.errorFieldNames then "error " else ""
2015-09-23 11:11:51 +08:00
<label className={(field.className || "")} key={field.name}>
{field.label}
<input type={field.type}
tabIndex={idx + 1}
value={@state.fields[field.name]}
onChange={@_onValueChanged}
2015-10-01 02:58:40 +08:00
onKeyPress={@_onFieldKeyPress}
2015-09-23 11:11:51 +08:00
data-field={field.name}
2015-10-03 08:10:49 +08:00
data-format={field.format ? ""}
disabled={@state.tryingToAuthenticate}
2015-09-23 11:11:51 +08:00
className={errclass}
placeholder={field.placeholder} />
</label>
_renderSettings: =>
@state.provider.settings?.filter(@_fieldOnCurrentPage)
.map (field, idx) =>
if field.type is 'checkbox'
<label className={"checkbox #{field.className ? ""}"} key={field.name}>
<input type={field.type}
tabIndex={idx + 5}
checked={@state.settings[field.name]}
onChange={@_onSettingsChanged}
2015-10-01 02:58:40 +08:00
onKeyPress={@_onFieldKeyPress}
2015-09-23 11:11:51 +08:00
data-field={field.name}
2015-10-03 08:10:49 +08:00
disabled={@state.tryingToAuthenticate}
data-format={field.format ? ""}
2015-09-23 11:11:51 +08:00
className={field.className ? ""} />
{field.label}
</label>
else
2015-09-25 05:51:15 +08:00
errclass = if field.name in @state.errorFieldNames then "error " else ""
2015-09-23 11:11:51 +08:00
<label className={field.className ? ""}
style={if field.advanced and not @state.show_advanced then {display:'none'} else {}}
key={field.name}>
{field.label}
<input type={field.type}
tabIndex={idx + 5}
value={@state.settings[field.name]}
onChange={@_onSettingsChanged}
2015-10-01 02:58:40 +08:00
onKeyPress={@_onFieldKeyPress}
2015-09-23 11:11:51 +08:00
data-field={field.name}
2015-10-03 08:10:49 +08:00
data-format={field.format ? ""}
disabled={@state.tryingToAuthenticate}
2015-09-23 11:11:51 +08:00
className={errclass+(field.className ? "")}
placeholder={field.placeholder} />
</label>
_renderButton: =>
pages = @state.provider.pages || []
2016-01-21 09:09:07 +08:00
if pages.length > @state.pageNumber + 1
2016-01-16 03:27:14 +08:00
# We're not on the last page.
if @_noFormErrors() and @_allRequiredFieldsFilled()
2016-01-21 09:09:07 +08:00
<button className="btn btn-large btn-gradient" onClick={@_onNextButton}>Continue</button>
2016-01-16 03:27:14 +08:00
else
# Disable the "Continue" button if the fields haven't been filled correctly.
2016-01-21 09:09:07 +08:00
<button className="btn btn-large btn-gradient btn-disabled">Continue</button>
2015-09-23 11:11:51 +08:00
else if @state.provider.name isnt 'gmail'
2015-09-25 05:51:15 +08:00
if @state.tryingToAuthenticate
2016-01-21 09:09:07 +08:00
<button className="btn btn-large btn-disabled btn-add-account-spinning">
2015-09-26 08:43:36 +08:00
<RetinaImg name="sending-spinner.gif" width={15} height={15} mode={RetinaImg.Mode.ContentPreserve} /> Adding account…
2015-09-25 05:51:15 +08:00
</button>
else
2016-01-16 03:27:14 +08:00
if @_noFormErrors() and @_allRequiredFieldsFilled()
2016-01-21 09:09:07 +08:00
<button className="btn btn-large btn-gradient btn-add-account" onClick={@_onSubmit}>Add account</button>
2016-01-16 03:27:14 +08:00
else
# Disable the "Add Account" button if the fields haven't been filled correctly.
2016-01-21 09:09:07 +08:00
<button className="btn btn-large btn-gradient btn-add-account btn-disabled">Add account</button>
2015-09-23 11:11:51 +08:00
_onNextButton: (event) =>
2016-01-21 09:09:07 +08:00
return unless @_noFormErrors() and @_allRequiredFieldsFilled()
2015-09-25 05:51:15 +08:00
@setState(pageNumber: @state.pageNumber + 1)
2015-09-23 11:11:51 +08:00
@_resize()
2015-09-25 05:51:15 +08:00
_onSubmit: (event) =>
2016-01-21 09:09:07 +08:00
return unless @_noFormErrors() and @_allRequiredFieldsFilled()
2015-10-01 02:58:40 +08:00
return if @state.tryingToAuthenticate
2015-09-23 11:11:51 +08:00
data = settings: {}
for own k,v of @state.fields when v isnt ''
data[k] = v
for own k,v of @state.settings when v isnt ''
data.settings[k] = v
data.provider = @state.provider.name
2016-04-09 03:00:03 +08:00
# if there's an account with this email, get the ID for it to notify the backend of re-auth
2016-04-12 01:48:13 +08:00
account = AccountStore.accountForEmail(data.email)
2016-04-09 03:00:03 +08:00
reauthParam = if account then "&reauth=#{account.id}" else ""
2015-10-03 07:12:08 +08:00
# handle special case for exchange/outlook/hotmail username field
if data.provider in ['exchange','outlook','hotmail'] and not data.settings.username?.trim().length
2015-09-23 11:11:51 +08:00
data.settings.username = data.email
2015-09-25 05:51:15 +08:00
@setState(tryingToAuthenticate: true)
2015-09-23 11:11:51 +08:00
# Send the form data directly to Nylas to get code
2015-09-30 00:45:02 +08:00
# If this succeeds, send the received code to N1 server to register the account
2015-09-23 11:11:51 +08:00
# Otherwise process the error message from the server and highlight UI as needed
NylasAPI.makeRequest
2016-04-09 03:00:03 +08:00
path: "/auth?client_id=#{NylasAPI.AppID}&n1_id=#{NylasEnv.config.get('updateIdentity')}#{reauthParam}"
2015-09-23 11:11:51 +08:00
method: 'POST'
body: data
returnsModel: false
2016-01-21 02:49:27 +08:00
timeout: 60000
2015-09-23 11:11:51 +08:00
auth:
user: ''
pass: ''
sendImmediately: true
.then (json) =>
2015-11-12 02:25:11 +08:00
invite_code = NylasEnv.config.get('invitationCode')
2015-10-05 18:27:28 +08:00
json.invite_code = invite_code
2015-10-05 07:49:41 +08:00
json.email = data.email
2015-10-05 18:27:28 +08:00
2015-09-23 11:11:51 +08:00
EdgehillAPI.request
path: "/connect/nylas"
method: "POST"
2016-01-21 02:49:27 +08:00
timeout: 60000
2015-09-23 11:11:51 +08:00
body: json
2015-10-06 08:31:29 +08:00
success: @_onAccountReceived
2015-09-25 05:51:15 +08:00
error: @_onNetworkError
.catch(@_onNetworkError)
2015-10-06 08:31:29 +08:00
_onAccountReceived: (json) =>
2015-12-09 08:39:38 +08:00
Actions.recordUserEvent('Auth Successful', {
provider: @state.provider.name
})
2015-10-06 08:31:29 +08:00
try
OnboardingActions.accountJSONReceived(json)
catch e
2016-02-04 07:06:52 +08:00
NylasEnv.reportError(e)
2015-10-06 08:31:29 +08:00
@setState
tryingToAuthenticate: false
errorMessage: "Sorry, something went wrong on the Nylas server. Please try again. If you're still having issues, contact us at support@nylas.com."
@_resize()
2015-09-25 05:51:15 +08:00
_onNetworkError: (err) =>
errorMessage = err.message
2015-12-09 08:39:38 +08:00
Actions.recordUserEvent('Auth Failed', {
errorMessage: errorMessage
provider: @state.provider.name
})
2016-01-21 09:09:07 +08:00
if errorMessage is "Invite code required"
2015-10-05 07:53:39 +08:00
choice = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
buttons: ['Okay'],
title: 'Confirm',
message: 'Due to a large number of sign-ups this week, you’ ll need an invitation code to add another account! Visit http://invite.nylas.com/ to grab one, or hold tight!'
})
2015-10-05 07:49:41 +08:00
OnboardingActions.moveToPage("token-auth")
2016-01-21 09:09:07 +08:00
if errorMessage is "Invalid invite code"
2015-10-05 07:49:41 +08:00
OnboardingActions.moveToPage("token-auth")
2016-01-21 09:09:07 +08:00
2015-09-25 05:51:15 +08:00
pageNumber = @state.pageNumber
errorFieldNames = err.body?.missing_fields || err.body?.missing_settings
2016-04-09 03:00:03 +08:00
if err.errorTitle is "setting_update_error"
2016-04-14 07:13:25 +08:00
@setState
tryingToAuthenticate: false
errorMessage: 'The IMAP/SMTP servers for this account do not match our records. Please verify that any server names you entered are correct. If your IMAP/SMTP server has changed, first remove this account from N1, then try logging in again.'
@_resize()
2016-04-09 03:00:03 +08:00
OnboardingActions.moveToPage("account-settings")
2016-04-14 08:41:31 +08:00
return
2016-04-09 03:00:03 +08:00
2015-09-25 05:51:15 +08:00
if errorFieldNames
{pageNumber, errorMessage} = @_stateForMissingFieldNames(errorFieldNames)
if err.statusCode is -123 # timeout
errorMessage = "Request timed out. Please try again."
@setState
pageNumber: pageNumber
errorMessage: errorMessage
errorFieldNames: errorFieldNames || []
tryingToAuthenticate: false
@_resize()
_stateForMissingFieldNames: (fieldNames) ->
fieldLabels = []
fields = [].concat(@state.provider.settings, @state.provider.fields)
pageNumbers = [@state.pageNumber]
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)
pageNumber = Math.min.apply(null, pageNumbers)
errorMessage = @_messageForFieldLabels(fieldLabels)
{pageNumber, errorMessage}
2015-09-23 11:11:51 +08:00
2015-09-25 05:51:15 +08:00
_messageForFieldLabels: (labels) ->
if labels.length > 2
2015-09-23 11:11:51 +08:00
return "Please fix the highlighted fields."
2015-09-25 05:51:15 +08:00
else if labels.length is 2
return "Please provide your #{labels[0]} and #{labels[1]}."
2015-09-23 11:11:51 +08:00
else
2015-09-25 05:51:15 +08:00
return "Please provide your #{labels[0]}."
2015-09-23 11:11:51 +08:00
_pollForGmailAccount: (callback) =>
EdgehillAPI.request
path: "/oauth/google/token?key="+@state.provider.clientKey
method: "GET"
success: (json) =>
callback(json)
error: (err) =>
callback()
_resize: =>
setTimeout( =>
@props.onResize?()
2016-01-21 09:09:07 +08:00
, 10)
2015-09-23 11:11:51 +08:00
_fireMoveToPrevPage: =>
if @state.pageNumber > 0
2016-01-21 09:09:07 +08:00
@setState(pageNumber: @state.pageNumber - 1)
2015-09-23 11:11:51 +08:00
@_resize()
else
OnboardingActions.moveToPreviousPage()
module.exports = AccountSettingsPage