define(function(require, exports, module) { 'use strict'; var Account = require('models/account'); var OAuthWindow = require('oauth_window'); var Presets = require('common/presets'); var URI = require('utils/uri'); var View = require('view'); var co = require('ext/co'); var core = require('core'); var isOffline = require('common/is_offline'); var router = require('router'); require('dom!modify-account-view'); var DEFAULT_AUTH_TYPE = 'basic'; var OAUTH_AUTH_CREDENTIALS = [ 'client_id', 'scope', 'redirect_uri', 'state' ]; function ModifyAccount(options) { View.apply(this, arguments); this.deleteRecord = this.deleteRecord.bind(this); this.cancel = this.cancel.bind(this); this.displayOAuth2 = this.displayOAuth2.bind(this); this.hideHeaderAndForm = this.hideHeaderAndForm.bind(this); this.cancelDelete = this.cancelDelete.bind(this); // bound so we can add remove listeners this._boundSaveUpdateModel = this.save.bind(this, { updateModel: true }); } module.exports = ModifyAccount; ModifyAccount.prototype = { __proto__: View.prototype, _changeToken: 0, selectors: { element: '#modify-account-view', form: '.modify-account-form', fields: '*[name]', saveButton: '#modify-account-view .save', deleteButton: '#modify-account-view .delete-confirm', deleteRecordButton: '.delete-record', cancelDeleteButton: '#modify-account-view .delete-cancel', header: '#modify-account-header', status: '#modify-account-view section[role="status"]', errors: '#modify-account-view .errors', oauth2Window: '#oauth2', oauth2SignIn: '#modify-account-view .force-oauth2' }, progressClass: 'in-progress', removeDialogClass: 'remove-dialog', get authenticationType() { if (this.preset && this.preset.authenticationType) { return this.preset.authenticationType; } return DEFAULT_AUTH_TYPE; }, get oauth2Window() { return this._findElement('oauth2Window'); }, get oauth2SignIn() { return this._findElement('oauth2SignIn'); }, get deleteRecordButton() { return this._findElement('deleteRecordButton'); }, get deleteButton() { return this._findElement('deleteButton'); }, get cancelDeleteButton() { return this._findElement('cancelDeleteButton'); }, get header() { return this._findElement('header'); }, get saveButton() { return this._findElement('saveButton'); }, get form() { return this._findElement('form'); }, get fields() { if (!this._fields) { var result = this._fields = {}; var elements = this.element.querySelectorAll( this.selectors.fields ); var i = 0; var len = elements.length; for (i; i < len; i++) { var el = elements[i]; result[el.getAttribute('name')] = el; } } return this._fields; }, updateForm: function() { var update = ['user', 'fullUrl']; update.forEach(function(name) { var field = this.fields[name]; field.value = this.model[name]; }, this); }, updateModel: function() { var update = ['user', 'password', 'fullUrl']; update.forEach(function(name) { var field = this.fields[name]; var value = field.value; if (name === 'fullUrl') { // Prepend a scheme if url has neither port nor scheme var port = URI.getPort(value); var scheme = URI.getScheme(value); if (!port && !scheme) { value = 'https://' + value; } } this.model[name] = value; }, this); }, deleteRecord: function(e) { if (e) { e.preventDefault(); } // begin the removal (which will emit the preRemove event) but don't wait // for it to complete... core.bridge.deleteAccount(this.model._id); // semi-hack clear the :target - harmless in tests // but important in the current UI because css :target // does not get cleared (for some reason) window.location.replace('#'); // TODO: in the future we may want to store the entry // url of this view and use that instead of this // hard coded value... router.show('/advanced-settings/'); }, cancel: function(event) { if (event) { event.preventDefault(); } window.history.back(); }, cancelDelete: function(event) { this.element.classList.remove(this.removeDialogClass); this.cancel(event); }, isOffline: isOffline, save: co.wrap(function *(options, e) { if (e) { e.preventDefault(); } var list = this.element.classList; if (this.isOffline()) { this.showErrors([{name: 'offline'}]); return; } list.add(this.progressClass); this.errors.textContent = ''; if (options && options.updateModel) { this.updateModel(); } try { yield core.bridge.createAccount(this.model); list.remove(this.progressClass); router.go(this.completeUrl); } catch(err) { list.remove(this.progressClass); this.showErrors(err); } }), hideHeaderAndForm: function() { this.element.classList.add(this.removeDialogClass); }, displayOAuth2: function(event) { if (event) { event.preventDefault(); } var self = this; this.oauth2Window.classList.add(View.ACTIVE); navigator.mozApps.getSelf().onsuccess = function(e) { var app = e.target.result; app.clearBrowserData().onsuccess = function() { self._redirectToOAuthFlow(); }; }; }, /** * @param {String} preset name of value in Calendar.Presets. */ _createModel: function(preset, callback) { var settings = Presets[preset]; var model = new Account(settings.options); model.preset = preset; return model; }, _redirectToOAuthFlow: function() { var apiCredentials = this.preset.apiCredentials; var params = { /* * code response type for now might change when we can use window.open */ response_type: 'code', /* offline so we get refresh_token[s] */ access_type: 'offline', /* we us force so we always get a refresh_token */ approval_prompt: 'force' }; OAUTH_AUTH_CREDENTIALS.forEach(function(key) { if (key in apiCredentials) { params[key] = apiCredentials[key]; } }); var oauth = this._oauthDialog = new OAuthWindow( this.oauth2Window, apiCredentials.authorizationUrl, params ); var self = this; oauth.open(); oauth.onabort = function() { self.cancel(); }; oauth.oncomplete = function(params) { if ('error' in params) { // Ruh roh return self.cancel(); } if (!params.code) { return console.error('authentication error'); } // Fistpump! self.model.oauth = { code: params.code }; self.save(); }; }, render: function() { if (!this.model) { throw new Error('must provider model to ModifyAccount'); } this.form.addEventListener('submit', this._boundSaveUpdateModel); this.saveButton.addEventListener('click', this._boundSaveUpdateModel); this.header.addEventListener('action', this.cancel); this.deleteRecordButton.addEventListener('click', this.hideHeaderAndForm); if (this.model._id) { this.type = 'update'; this.deleteButton.addEventListener('click', this.deleteRecord); this.cancelDeleteButton.addEventListener('click', this.cancelDelete); } else { this.type = 'create'; } var list = this.element.classList; list.add(this.type); list.add('preset-' + this.model.preset); list.add('provider-' + this.model.providerType); list.add('auth-' + this.authenticationType); if (this.model.error) { list.add('error'); } if (this.authenticationType === 'oauth2') { this.oauth2SignIn.addEventListener('click', this.displayOAuth2); if (this.type === 'create') { this.displayOAuth2(); } this.fields.user.disabled = true; this.saveButton.disabled = true; } this.form.reset(); this.updateForm(); var usernameType = this.model.usernameType; this.fields.user.type = (usernameType === undefined) ? 'text' : usernameType; this.header.runFontFitSoon(); }, destroy: function() { var list = this.element.classList; list.remove(this.type); list.remove('preset-' + this.model.preset); list.remove('provider-' + this.model.providerType); list.remove('auth-' + this.authenticationType); list.remove('error'); list.remove(this.removeDialogClass); this.fields.user.disabled = false; this.saveButton.disabled = false; this._fields = null; this.form.reset(); this.deleteRecordButton.removeEventListener('click', this.hideHeaderAndForm); this.oauth2SignIn.removeEventListener('click', this.displayOAuth2); this.saveButton.removeEventListener('click', this._boundSaveUpdateModel); this.deleteButton.removeEventListener('click', this.deleteRecord); this.cancelDeleteButton.removeEventListener('click', this.cancelDelete); this.header.removeEventListener('action', this.cancel); this.form.removeEventListener('submit', this._boundSaveUpdateModel); }, dispatch: co.wrap(function *(data) { if (this.model) { this.destroy(); } var params = data.params; var changeToken = ++this._changeToken; this.completeUrl = '/settings/'; try { var model; if (params.id) { model = yield core.bridge.getAccount(params.id); } else if (params.preset) { model = this._createModel(params.preset); } this.preset = Presets[model.preset]; // race condition another dispatch has queued // while we where waiting for an async event. if (this._changeToken !== changeToken) { return; } this.model = model; this.render(); } catch (err) { console.error('Error displaying model in ModifyAccount', data, err); } }), oninactive: function() { View.prototype.oninactive.apply(this, arguments); if (this._oauthDialog) { this._oauthDialog.close(); this._oauthDialog = null; } } }; });