React = require 'react'
_ = require 'underscore'
{ipcRenderer, dialog, remote} = require 'electron'
{RetinaImg} = require 'nylas-component-kit'
{EdgehillAPI, NylasAPI, APIError, Actions} = require 'nylas-exports'
OnboardingActions = require './onboarding-actions'
NylasApiEnvironmentStore = require './nylas-api-environment-store'
Providers = require './account-types'
class AccountSettingsPage extends React.Component
@displayName: "AccountSettingsPage"
constructor: (@props) ->
@state =
provider: @props.pageData.provider
settings: {}
fields: {}
pageNumber: 0
errorFieldNames: []
errorMessage: null
show_advanced: false
@props.pageData.provider.settings.forEach (field) =>
if field.default?
@state.settings[field.name] = field.default
# Special case for gmail. Rather than showing a form, we poll in the
# background for completion of the gmail auth on the server.
if @state.provider.name is 'gmail'
pollAttemptId = 0
done = false
# polling with capped exponential backoff
delay = 1000
tries = 0
poll = (id,initial_delay) =>
_retry = =>
tries++
@_pollForGmailAccount((account_data) =>
if account_data?
done = true
{data} = account_data
account = JSON.parse(data)
@_onAccountReceived(account)
else if tries < 20 and id is pollAttemptId
setTimeout(_retry, delay)
delay *= 1.2 # exponential backoff
)
setTimeout(_retry,initial_delay)
ipcRenderer.on('browser-window-focus', ->
if not done # hack to deactivate this listener when done
pollAttemptId++
poll(pollAttemptId,0)
)
poll(pollAttemptId,5000)
render: ->
{@_renderTitle()}
{@_renderErrorMessage()}
_onSettingsChanged: (event) =>
field = event.target.dataset.field
format = event.target.dataset.format
int_formatter = (a) ->
i = parseInt(a)
if isNaN(i) then "" else i
formatter = if format is 'integer' then int_formatter else (a) -> a
settings = @state.settings
if event.target.type is 'checkbox'
settings[field] = event.target.checked
else
settings[field] = formatter(event.target.value)
setting_field = _.find(@state.provider.settings, ((e) -> return e['name'] == field))
# If the field defines an isValid method, try to validate
# the input.
if setting_field?.isValid?
if not setting_field.isValid(event.target.value)
errorFields = _.uniq(@state.errorFieldNames.concat Array(field))
@setState({errorFieldNames: errorFields})
else
errorFields = _.uniq((x for x in @state.errorFieldNames when x != field))
@setState({errorFieldNames: errorFields})
@setState({settings})
_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)
fields = _.extend(@state.fields, @state.settings)
for field in requiredFields
fieldName = field['name']
if not (fieldName of fields) or fields[fieldName] == ''
return false
return true
_onValueChanged: (event) =>
field = event.target.dataset.field
fields = @state.fields
fields[field] = event.target.value
provider_field = _.find(@state.provider.fields, ((e) -> return e['name'] == field))
# If the field defines an isValid method, try to validate
# the input.
if provider_field?.isValid?
if not provider_field.isValid(event.target.value)
errorFields = _.uniq(@state.errorFieldNames.concat [field])
@setState({errorFieldNames: errorFields})
else
errorFields = _.uniq((x for x in @state.errorFieldNames when x != field))
@setState({errorFieldNames: errorFields})
@setState({fields})
_onFieldKeyPress: (event) =>
if event.key in ['Enter', 'Return']
pages = @state.provider.pages || []
if pages.length > @state.pageNumber+1
@_onNextButton()
else
@_onSubmit()
_renderTitle: =>
if @state.provider.name is 'gmail'
Sign in to {@state.provider.displayName} in your browser.
else if @state.provider.pages?.length > 0
{@state.provider.pages[@state.pageNumber]}
else
Sign in to {@state.provider.displayName}
_renderErrorMessage: =>
if @state.errorMessage
{@state.errorMessage ? ""}
_fieldOnCurrentPage: (field) =>
!@state.provider.pages || field.page is @state.pageNumber
_renderFields: =>
@state.provider.fields?.filter(@_fieldOnCurrentPage)
.map (field, idx) =>
errclass = if field.name in @state.errorFieldNames then "error " else ""
_renderSettings: =>
@state.provider.settings?.filter(@_fieldOnCurrentPage)
.map (field, idx) =>
if field.type is 'checkbox'
else
errclass = if field.name in @state.errorFieldNames then "error " else ""
_renderButton: =>
pages = @state.provider.pages || []
if pages.length > @state.pageNumber+1
# We're not on the last page.
if @_noFormErrors() and @_allRequiredFieldsFilled()
else
# Disable the "Continue" button if the fields haven't been filled correctly.
else if @state.provider.name isnt 'gmail'
if @state.tryingToAuthenticate
else
if @_noFormErrors() and @_allRequiredFieldsFilled()
else
# Disable the "Add Account" button if the fields haven't been filled correctly.
_onNextButton: (event) =>
@setState(pageNumber: @state.pageNumber + 1)
@_resize()
_onSubmit: (event) =>
return if @state.tryingToAuthenticate
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
# handle special case for exchange/outlook/hotmail username field
if data.provider in ['exchange','outlook','hotmail'] 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 N1 server to register the account
# Otherwise process the error message from the server and highlight UI as needed
NylasAPI.makeRequest
path: "/auth?client_id=#{NylasAPI.AppID}&n1_id=#{NylasEnv.config.get('updateIdentity')}"
method: 'POST'
body: data
returnsModel: false
timeout: 60000
auth:
user: ''
pass: ''
sendImmediately: true
.then (json) =>
invite_code = NylasEnv.config.get('invitationCode')
json.invite_code = invite_code
json.email = data.email
EdgehillAPI.request
path: "/connect/nylas"
method: "POST"
timeout: 60000
body: json
success: @_onAccountReceived
error: @_onNetworkError
.catch(@_onNetworkError)
_onAccountReceived: (json) =>
Actions.recordUserEvent('Auth Successful', {
provider: @state.provider.name
})
try
OnboardingActions.accountJSONReceived(json)
catch e
NylasEnv.emitError(e)
@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()
_onNetworkError: (err) =>
errorMessage = err.message
Actions.recordUserEvent('Auth Failed', {
errorMessage: errorMessage
provider: @state.provider.name
})
if errorMessage == "Invite code required"
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!'
})
OnboardingActions.moveToPage("token-auth")
if errorMessage == "Invalid invite code"
# delay?
OnboardingActions.moveToPage("token-auth")
pageNumber = @state.pageNumber
errorFieldNames = err.body?.missing_fields || err.body?.missing_settings
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}
_messageForFieldLabels: (labels) ->
if labels.length > 2
return "Please fix the highlighted fields."
else if labels.length is 2
return "Please provide your #{labels[0]} and #{labels[1]}."
else
return "Please provide your #{labels[0]}."
_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?()
,10)
_fireMoveToPrevPage: =>
if @state.pageNumber > 0
@setState(pageNumber: @state.pageNumber-1)
@_resize()
else
OnboardingActions.moveToPreviousPage()
module.exports = AccountSettingsPage