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()}
{@_renderFields()} {@_renderSettings()} {@_renderButton()}
_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