// Copyright 2017 The Nomulus Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. goog.provide('registry.registrar.Payment'); goog.require('goog.Uri'); goog.require('goog.asserts'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); goog.require('goog.events.EventType'); goog.require('goog.json'); goog.require('goog.object'); goog.require('goog.soy'); goog.require('registry.Component'); goog.require('registry.MenuButton'); goog.require('registry.Session'); goog.require('registry.forms'); goog.require('registry.registrar.BrainFrame'); goog.require('registry.soy.registrar.console'); goog.require('registry.soy.registrar.payment'); goog.require('registry.util'); goog.forwardDeclare('goog.events.BrowserEvent'); goog.forwardDeclare('registry.registrar.Console'); /** * Page allowing registrar to send money to registry. * *
This page contains a form that asks the user to enter an arbitrary amount * and a payment method, which can be credit card or PayPal. Multiple * currencies are supported. * *
We don't have any access whatsoever to the credit card information. We * embed an iframe run by Braintree Payments. The user can then provide his * credit card (or PayPal) details directly to Braintree. Braintree then gives * us a nonce value representing the payment method, which we can use to issue * the transaction. * *
To use Braintree's iframe, we need to load a script from their server. We * don't want that script to have access to the Registrar Console. If Braintree * got pwnd, the attacker would be able to issue EPP commands as a registrar. * *
We fix this problem by embedding the Braintree iframe inside another * sandbox iframe that's hosted from a Cloud Storage bucket. This frame is * defined by {@code brainframe.html}. It's basically an empty shell that sends * a request back to the production environment for {@code brainframe.js}. * *
The importance of the Cloud Storage bucket is that it is served from a * separate domain. This causes the browser to forbid the iframe from accessing * the contents of the parent frame. The HTML5 {@code sandbox} attribute does * this too, but we can't use it, because the Venmo functionality in the * Braintree JS SDK needs to be able to access {@code document.cookie}, which * is forbidden in a sandbox environment. This HTML5 {@code sandbox} feature is * also not available in older versions of Internet Explorer. * *
This page starts off as a loading glyph, while we issue an RPC to the * backend. We ask for a Braintree token, which currencies are available, and * the location of the brainframe HTML file. Once we get that data, we render * the form. * *
Once the sandbox iframe inside that form has loaded, it'll send us a * message asking for the token. We give it the token, which it uses to load * the Braintree iframe. * *
To make sure the sandbox iframe is the same size as the Braintree iframe, * the sandbox iframe will send us messages on occasion asking to be resized. * *
The disabled state of the submit button is managed judiciously. It's * disabled initially, until we get a READY message from the sandbox iframe, * indicating that the Braintree iframe is fully loaded. We also disable the * submit button during the submit process. * *
When the user presses the submit button, we send a message to the sandbox * iframe asking it to submit the Braintree iframe. When the Braintree iframe * is submitted, it gives the sandbox iframe the the payment method nonce, * which it passes along to us. Then we pass the form data to the backend via * the payment RPC, which invokes the Braintree Java API to issue the * transaction. * *
If the payment RPC fails, we'll either show an error on a field, or in a * bloody butterbar. If it succeeds, then the backend will give us the * transaction ID assigned by Braintree, which we then render on a success * page. * *
The success page contains a "Make Another Payment" button which, if
* clicked, will reset the state of this page back to the beginning.
*
* @param {!registry.registrar.Console} console
* @param {string} xsrfToken Security token to pass back to the server.
* @constructor
* @extends {registry.Component}
* @final
*/
registry.registrar.Payment = function(console, xsrfToken) {
registry.registrar.Payment.base(this, 'constructor', console);
/**
* Element in which this page is rendered.
* @private {!Element}
* @const
*/
this.content_ = goog.dom.getRequiredElement('reg-content');
/**
* Braintree API nonce token generated by the backend. This value is a
* prerequisite to rendering the Braintree iframe.
* @private {string}
*/
this.token_ = '';
/**
* Braintree API nonce value for payment method selected by user.
* @private {string}
*/
this.paymentMethodNonce_ = '';
/**
* Currency drop-down widget in form.
* @private {?registry.MenuButton}
*/
this.currencyMenu_ = null;
/**
* XHR client to {@code RegistrarPaymentSetupAction}.
* @private {!registry.Session.}
* @const
*/
this.setupRpc_ =
new registry.Session(new goog.Uri('/registrar-payment-setup'),
xsrfToken,
registry.Session.ContentType.JSON);
/**
* XHR client to {@code RegistrarPaymentAction}.
* @private {!registry.Session.}
* @const
*/
this.paymentRpc_ =
new registry.Session(new goog.Uri('/registrar-payment'),
xsrfToken,
registry.Session.ContentType.JSON);
this.listen(goog.global.window,
goog.events.EventType.MESSAGE,
this.onMessage_);
};
goog.inherits(registry.registrar.Payment, registry.Component);
/** @override */
registry.registrar.Payment.prototype.bindToDom = function(id) {
registry.registrar.Payment.base(this, 'bindToDom', id);
if (!goog.isNull(goog.dom.getElement('reg-app-buttons'))) {
goog.dom.removeChildren(goog.dom.getElement('reg-app-buttons'));
}
if (!registry.registrar.Payment.isBrowserSupported_()) {
goog.soy.renderElement(this.content_,
registry.soy.registrar.payment.unsupported);
return;
}
goog.soy.renderElement(this.content_, registry.soy.registrar.console.loading);
this.setupRpc_.sendXhrIo({}, goog.bind(this.onSetup_, this));
};
/**
* Handler invoked when we receive information from our backend, such as a
* Braintree token, which is necessary for us to render the payment form.
* @param {!registry.rpc.PaymentSetup.Response} response
* @private
*/
registry.registrar.Payment.prototype.onSetup_ = function(response) {
if (response.status != 'SUCCESS') {
if (response.message == 'not-using-cc-billing') {
goog.soy.renderElement(this.content_,
registry.soy.registrar.payment.notUsingCcBilling);
} else {
registry.forms.displayError(response.message);
}
return;
}
var result = response.results[0];
this.token_ = result.token;
this.paymentMethodNonce_ = '';
goog.soy.renderElement(this.content_,
registry.soy.registrar.payment.form,
result);
this.listen(
goog.dom.getRequiredElementByClass(goog.getCssName('reg-payment-form')),
goog.events.EventType.SUBMIT,
this.onSubmit_);
this.currencyMenu_ =
new registry.MenuButton(goog.dom.getRequiredElement('currency'));
this.registerDisposable(this.currencyMenu_);
};
/**
* Handler invoked when payment form is submitted.
* @param {!goog.events.BrowserEvent} e
* @private
*/
registry.registrar.Payment.prototype.onSubmit_ = function(e) {
e.preventDefault();
this.submit_();
};
/**
* Submits payment form.
* @private
*/
registry.registrar.Payment.prototype.submit_ = function() {
registry.forms.resetErrors();
registry.registrar.Payment.setEnabled_(false);
if (this.paymentMethodNonce_ == '') {
this.send_(
'type', registry.registrar.BrainFrame.MessageType.SUBMIT_REQUEST);
return;
}
this.paymentRpc_.sendXhrIo(
{
amount: goog.dom.getRequiredElement('amount').value,
currency: this.currencyMenu_.getValue(),
paymentMethodNonce: this.paymentMethodNonce_
},
goog.bind(this.onPayment_, this));
};
/**
* Callback for backend payment RPC that issues the transaction.
* @param {!registry.rpc.Payment.Response} response
* @private
*/
registry.registrar.Payment.prototype.onPayment_ = function(response) {
registry.registrar.Payment.setEnabled_(true);
if (response.status != 'SUCCESS') {
registry.forms.displayError(response.message, response.field);
return;
}
goog.soy.renderElement(this.content_,
registry.soy.registrar.payment.success,
response.results[0]);
this.listenOnce(
goog.dom.getRequiredElementByClass(goog.getCssName('reg-payment-again')),
goog.events.EventType.CLICK,
this.bindToDom);
};
/**
* Handler invoked when {@code brainframe.js} sends us a message.
* @param {!goog.events.BrowserEvent} e
* @private
*/
registry.registrar.Payment.prototype.onMessage_ = function(e) {
var msg = /** @type {!MessageEvent. We remove the iframe from the page as soon as this happens, because the
* UI would be busted otherwise. The Braintree UI for changing the payment
* method (after it's been entered) does not appear to stop respond to submit
* events. It also causes ugly scroll bars to appear inside the iframe.
*
* This approach is also advantageous for screenshot testing. We do not want
* our continuous integration testing system to talk to Braintree's servers. So
* we mock out the brainframe with {@code integration-test-brainframe.html}
* which only sends us a METHOD message, which we then render ourselves.
*
* @param {!braintreepayments.PaymentMethod} pm
* @private
*/
registry.registrar.Payment.prototype.setPaymentMethod_ = function(pm) {
registry.forms.resetErrors();
goog.dom.removeNode(
goog.dom.getElementByClass(goog.getCssName('reg-payment-form-method')));
var paymentMethodInfoBox =
goog.dom.getRequiredElementByClass(
goog.getCssName('reg-payment-form-method-info'));
switch (pm.type) {
case 'CreditCard':
goog.soy.renderElement(
paymentMethodInfoBox,
registry.soy.registrar.payment.methodInfoCard,
{cardType: pm.details.cardType, lastTwo: pm.details.lastTwo});
break;
case 'PayPalAccount':
goog.soy.renderElement(
paymentMethodInfoBox,
registry.soy.registrar.payment.methodInfoPaypal,
{email: pm.details.email});
break;
default:
throw Error('Unknown payment method: ' + pm.type);
}
registry.util.setVisible(paymentMethodInfoBox, true);
this.paymentMethodNonce_ = pm.nonce;
};
/**
* Sends message to brainframe.
* @param {...*} var_args Passed along to {@code goog.object.create}.
* @private
*/
registry.registrar.Payment.prototype.send_ = function(var_args) {
goog.asserts.assert(arguments[0] == 'type');
var brainframeWindow =
goog.dom.getFrameContentWindow(
goog.dom.getRequiredElementByClass(
goog.getCssName('reg-payment-form-method')));
// We send a string value to support older versions of IE.
brainframeWindow.postMessage(
goog.json.serialize(goog.object.create.apply(null, arguments)),
'*');
};
/**
* Enables submit button and hides mini loading glyph.
* @param {boolean} enabled
* @private
*/
registry.registrar.Payment.setEnabled_ = function(enabled) {
registry.forms.setEnabled(
goog.dom.getRequiredElementByClass(
goog.getCssName('reg-payment-form-submit')), enabled);
registry.util.setVisible(
goog.dom.getRequiredElementByClass(
goog.getCssName('reg-payment-form-loader')), !enabled);
};
/**
* Returns {@code true} if browser has all the features we need.
* @return {boolean}
* @private
* @see "http://caniuse.com/#feat=dispatchevent"
*/
registry.registrar.Payment.isBrowserSupported_ = function() {
// dispatchEvent is used by brainframe.js and is IE 9+.
return goog.object.containsKey(
goog.dom.createElement(goog.dom.TagName.FORM),
'dispatchEvent');
};