FE-79: added unified error handling. Added form buttons disabling / hiding functionality. Changing styles. Refactoring payform (#22)

This commit is contained in:
Ildar Galeev 2016-11-15 13:58:29 +03:00 committed by GitHub
parent 6c167522e4
commit 50865b10b6
13 changed files with 150 additions and 108 deletions

View File

@ -1,13 +1,14 @@
export default class Initialization {
static sendInit(endpoint, data) {
static sendInit(endpoint, invoiceId, token, email) {
const request = this.buildRequest(invoiceId, token, email);
return new Promise((resolve, reject) => {
fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
body: JSON.stringify(request)
}).then(response => {
if (response.status >= 200 && response.status < 300) {
resolve();
@ -17,4 +18,15 @@ export default class Initialization {
}).catch(() => reject('Error send to init endpoint'));
});
}
static buildRequest(invoiceId, token, email) {
return {
invoiceId: invoiceId,
token: token.token,
session: token.session,
contractInfo: {
email: email
}
}
}
}

View File

@ -0,0 +1,32 @@
export default class Tokenization {
constructor(tokenizer) {
this.Tokenizer = tokenizer;
}
setPublicKey(key) {
this.Tokenizer.setPublicKey(key);
}
createToken(cardHolder, cardNumber, expDate, cvv) {
const request = Tokenization.buildRequest(cardHolder, cardNumber, expDate, cvv);
const tokenizer = this.Tokenizer;
return new Promise((resolve, reject) => {
tokenizer.card.createToken(request, paymentTools => resolve(paymentTools), error => reject(error));
});
}
static buildRequest(cardHolder, cardNumber, expDate, cvv) {
return {
paymentToolType: 'CardData',
cardHolder: cardHolder,
cardNumber: Tokenization.replaceSpaces(cardNumber),
expDate: Tokenization.replaceSpaces(expDate),
cvv: cvv
}
}
static replaceSpaces(str) {
return str.replace(/\s+/g, '');
}
}

View File

@ -1,27 +0,0 @@
export default class RequestBuilder {
static buildTokenizationRequest(cardHolder, cardNumber, expDate, cvv) {
return {
paymentToolType: 'CardData',
cardHolder: cardHolder,
cardNumber: this.replaceSpaces(cardNumber),
expDate: this.replaceSpaces(expDate),
cvv: cvv
}
}
static buildInitRequest(invoiceId, token, email) {
return {
invoiceId: invoiceId,
token: token.token,
session: token.session,
contractInfo: {
email: email
}
}
}
static replaceSpaces(str) {
return str.replace(/\s+/g, '');
}
}

View File

@ -0,0 +1,18 @@
export default class CloseButton {
constructor() {
this.element = document.querySelector('.modal--close');
}
set onclick(handler) {
this.element.onclick = handler;
}
hide() {
this.element.style.display = 'none';
}
show() {
this.element.style.display = 'block';
}
}

View File

@ -1,6 +1,7 @@
export default class PayButton {
constructor() {
this.element = document.querySelector('.payform--pay-button');
this.disable();
}
renderText(amount, currency) {
@ -12,4 +13,16 @@ export default class PayButton {
setPayButtonColor(color) {
this.element.style.background = color;
}
disable() {
this.element.setAttribute('disabled', 'true');
}
enable() {
this.element.removeAttribute('disabled');
}
set onclick(handler) {
this.element.onclick = handler;
}
}

View File

@ -11,6 +11,10 @@ export default class TokenizerScript {
}
render() {
document.getElementsByTagName('head')[0].appendChild(this.element);
return new Promise((resolve, error) => {
document.getElementsByTagName('head')[0].appendChild(this.element);
this.element.onload = () => resolve();
this.element.onerror = () => error();
});
}
}

View File

@ -1,6 +1,7 @@
import 'whatwg-fetch';
import Initialization from './backend-communication/Initialization';
import EventPoller from './backend-communication/EventPoller';
import Tokenization from './backend-communication/Tokenization';
import Form from './elements/Form';
import Spinner from './elements/Spinner';
import Checkmark from './elements/Checkmark';
@ -9,7 +10,7 @@ import ErrorPanel from './elements/ErrorPanel';
import Form3ds from './elements/Form3ds';
import TokenizerScript from './elements/TokenizerScript';
import StyleLink from './elements/StyleLink';
import RequestBuilder from './builders/RequestBuilder';
import CloseButton from './elements/CloseButton';
import settings from '../settings';
import domReady from '../utils/domReady';
import Listener from '../communication/Listener';
@ -26,44 +27,47 @@ domReady(function () {
const checkmark = new Checkmark();
const errorPanel = new ErrorPanel();
const payButton = new PayButton();
const closeButton = new CloseButton();
closeButton.onclick = () => communicator.send({type: 'close'});
const communicator = new ParentCommunicator();
Listener.addListener(message => {
if (message.type === 'init' || message.type === 'resume') {
tokenizerScript.render();
styleLink.rerender();
params = message.data;
payButton.renderText(params.amount, params.currency);
if (params.logo) {
form.setLogo(params.logo);
}
if (params.name) {
form.setName(params.name);
}
if (params.buttonColor) {
payButton.setPayButtonColor(params.buttonColor);
}
styleLink.rerender();
customizeForm();
if (params.state && params.state === 'inProgress') {
spinner.show();
form.hide();
polling();
pollEvents();
}
tokenizerScript.render()
.then(() => payButton.enable())
.catch(() => errorPanel.show('Tokenizer is not available'));
}
});
window.payformClose = () => communicator.send({type: 'close'});
payButton.onclick = () => {
if (form.isValid()) {
spinner.show();
form.hide();
errorPanel.hide();
closeButton.hide();
const tokenization = new Tokenization(window.Tokenizer);
tokenization.setPublicKey(params.key);
tokenization.createToken(form.getCardHolder(), form.getCardNumber(), form.getExpDate(), form.getCvv())
.then(paymentTools => sendInitRequest(paymentTools))
.catch(error => resolveElementsAfterError('Create token error', error, 'Card tokenization failed'));
}
};
const polling = () => {
console.info('polling start');
function pollEvents() {
EventPoller.pollEvents(params.endpointEvents, params.invoiceId, params.orderId, settings.pollingTimeout, settings.pollingRetries).then(result => {
console.info('polling resolve, data:', result);
if (result.type === 'success') {
console.info('polling result: success, post message: done');
spinner.hide();
checkmark.show();
communicator.sendWithTimeout({type: 'done'}, settings.closeFormTimeout);
} else if (result.type === 'interact') {
console.info('polling result: interact, post message: interact, starts 3ds interaction...');
communicator.send({type: 'interact'});
const redirectUrl = `${params.locationHost}/cart/checkout/review`;
const form3ds = new Form3ds(result.data, redirectUrl);
@ -71,52 +75,37 @@ domReady(function () {
form3ds.submit();
}
}).catch(error => {
console.error('polling error, data:', error);
spinner.hide();
if (error.type === 'error') {
errorPanel.show(`Error:\n${error.data.eventType}\nStatus: ${error.data.status}`);
} else if (error.type === 'long polling') {
errorPanel.show('Too long polling');
} else {
errorPanel.show('Unknown error');
}
communicator.sendWithTimeout({type: 'error'}, settings.closeFormTimeout);
resolveElementsAfterError('Polling error:', error, 'An error occurred while processing your card');
});
};
}
const onTokenCreate = paymentTools => {
console.info('tokenization done, data:', paymentTools);
const initRequest = RequestBuilder.buildInitRequest(params.invoiceId, paymentTools, form.getEmail());
console.info('request to initialization endpoint start, data:', initRequest);
Initialization.sendInit(params.endpointInit, initRequest).then(() => {
console.info('request to initialization endpoint done');
polling();
});
};
window.pay = () => {
if (window.Tokenizer === undefined) {
form.hide();
errorPanel.show('Tokenizer.js is not available');
communicator.sendWithTimeout({type: 'error'}, settings.closeFormTimeout);
return false;
}
if (form.isValid()) {
spinner.show();
form.hide();
window.Tokenizer.setPublicKey(params.key);
const request = RequestBuilder.buildTokenizationRequest(
form.getCardHolder(),
form.getCardNumber(),
form.getExpDate(),
form.getCvv()
);
console.info('tokenization start, data:', request);
window.Tokenizer.card.createToken(request, onTokenCreate, error => {
spinner.hide();
errorPanel.show(`Error create token:\n${error.message}`);
function sendInitRequest(paymentTools) {
Initialization.sendInit(params.endpointInit, params.invoiceId, paymentTools, form.getEmail())
.then(() => pollEvents())
.catch(error => {
resolveElementsAfterError('Send init request error', error, error.message);
communicator.sendWithTimeout({type: 'error'}, settings.closeFormTimeout);
});
}
function customizeForm() {
payButton.renderText(params.amount, params.currency);
if (params.logo) {
form.setLogo(params.logo);
}
};
if (params.name) {
form.setName(params.name);
}
if (params.buttonColor) {
payButton.setPayButtonColor(params.buttonColor);
}
}
function resolveElementsAfterError(logMessage, error, panelMessage) {
console.error(logMessage, error);
spinner.hide();
form.show();
closeButton.show();
errorPanel.show(panelMessage);
}
});

View File

@ -32,7 +32,7 @@ html(lang='en')
path(fill-rule='evenodd', transform='translate(9, 9)', d='M8.8,4 C8.8,1.79086089 7.76640339,4.18628304e-07 5.5,0 C3.23359661,-4.1480896e-07 2.2,1.79086089 2.2,4 L3.2,4 C3.2,2.34314567 3.81102123,0.999999681 5.5,1 C7.18897877,1.00000032 7.80000001,2.34314567 7.80000001,4 L8.8,4 Z M1.99201702,4 C0.891856397,4 0,4.88670635 0,5.99810135 L0,10.0018986 C0,11.1054196 0.900176167,12 1.99201702,12 L9.00798298,12 C10.1081436,12 11,11.1132936 11,10.0018986 L11,5.99810135 C11,4.89458045 10.0998238,4 9.00798298,4 L1.99201702,4 Z M1.99754465,5 C1.44661595,5 1,5.45097518 1,5.99077797 L1,10.009222 C1,10.5564136 1.4463114,11 1.99754465,11 L9.00245535,11 C9.55338405,11 10,10.5490248 10,10.009222 L10,5.99077797 C10,5.44358641 9.5536886,5 9.00245535,5 L1.99754465,5 Z M1.99754465,5')
.modal--overlay
.modal--container
.modal--close(onclick="payformClose()")
.modal--close
.modal--body
.payform
.payform--header
@ -70,8 +70,9 @@ html(lang='en')
.payform--icon
svg(fill='#549928')
use(xmlns:xlink='http://www.w3.org/1999/xlink', xlink:href='#Icon-envelope-desktop')
button.payform--pay-button(type='button', form='payform', onclick="pay()") Оплатить
div.spinner(style='transform:scale(0.54);')
.error-panel
button.payform--pay-button(type='button', form='payform') Оплатить
.spinner(style='transform:scale(0.54);')
div(style='top:80px;left:93px;width:14px;height:40px;background:#00b2ff;-webkit-transform:rotate(0deg) translate(0,-60px);transform:rotate(0deg) translate(0,-60px);border-radius:10px;position:absolute;')
div(style='top:80px;left:93px;width:14px;height:40px;background:#00b2ff;-webkit-transform:rotate(30deg) translate(0,-60px);transform:rotate(30deg) translate(0,-60px);border-radius:10px;position:absolute;')
div(style='top:80px;left:93px;width:14px;height:40px;background:#00b2ff;-webkit-transform:rotate(60deg) translate(0,-60px);transform:rotate(60deg) translate(0,-60px);border-radius:10px;position:absolute;')
@ -84,9 +85,8 @@ html(lang='en')
div(style='top:80px;left:93px;width:14px;height:40px;background:#00b2ff;-webkit-transform:rotate(270deg) translate(0,-60px);transform:rotate(270deg) translate(0,-60px);border-radius:10px;position:absolute;')
div(style='top:80px;left:93px;width:14px;height:40px;background:#00b2ff;-webkit-transform:rotate(300deg) translate(0,-60px);transform:rotate(300deg) translate(0,-60px);border-radius:10px;position:absolute;')
div(style='top:80px;left:93px;width:14px;height:40px;background:#00b2ff;-webkit-transform:rotate(330deg) translate(0,-60px);transform:rotate(330deg) translate(0,-60px);border-radius:10px;position:absolute;')
div.checkmark.icon.icon--order-success.svg
.checkmark.icon.icon--order-success.svg
svg(xmlns="http://www.w3.org/2000/svg" width="72px" height="72px")
g(fill="none" stroke="#8EC343" stroke-width="2")
circle(cx="36" cy="36" r="35" style="stroke-dasharray:240px, 240px; stroke-dashoffset: 480px;")
path(d="M17.417,37.778l9.93,9.909l25.444-25.393" style="stroke-dasharray:50px, 50px; stroke-dashoffset: 0px;")
div.error-panel
path(d="M17.417,37.778l9.93,9.909l25.444-25.393" style="stroke-dasharray:50px, 50px; stroke-dashoffset: 0px;")

View File

@ -1,7 +1,7 @@
.checkmark {
margin-left: 78px;
margin-top: 57px;
margin-bottom: 66px;
margin-top: 99px;
margin-bottom: 98px;
}
/* animations */

View File

@ -1,8 +1,6 @@
.error-panel {
border-radius: 5px;
padding: 10px 15px;
font-size: 14px;
font-weight: 700;
padding: 9px 10px;
font-size: 13px;
background: #ff7b7b;
margin: 70px 0;
}

View File

@ -453,3 +453,6 @@ input::-ms-clear {
background-image: -webkit-linear-gradient(top, #328ac3, #277bbe);
background-image: linear-gradient(180deg, #328ac3, #277bbe)
}
.payform--pay-button[disabled] {
background: #828282;
}

View File

@ -24,11 +24,11 @@
}
.spinner {
margin-left: 17px;
position: relative;
background: none;
width: 200px;
height: 200px;
margin: 37px auto;
}
.spinner > div:nth-of-type(2) {

View File

@ -1,6 +1,6 @@
export default {
integrationClassName: 'rbkmoney-payform',
pollingTimeout: 1000,
closeFormTimeout: 2500,
pollingRetries: 20
closeFormTimeout: 3000,
pollingRetries: 10
};