├── .gitignore
├── .jshintrc
├── CHANGELOG.md
├── Gruntfile.js
├── LICENSE-MIT
├── README.md
├── dist
├── rxp-js.js
└── rxp-js.min.js
├── examples
└── hpp
│ ├── helper.js
│ ├── json
│ └── process-a-payment.json
│ ├── process-a-payment-embedded-autoload-callback.html
│ ├── process-a-payment-embedded-autoload.html
│ ├── process-a-payment-embedded.html
│ ├── process-a-payment-lightbox-callback.html
│ ├── process-a-payment-lightbox.html
│ ├── proxy-request.php
│ ├── redirect-for-payment.html
│ └── response.php
├── lib
├── .jshintrc
├── rxp-hpp.js
└── rxp-remote.js
├── package.json
└── specs
├── functional
└── hpp
│ ├── embedded-positives_spec.js
│ ├── lightbox-positives_spec.js
│ └── redirect-positives_spec.js
├── helpers
└── hpp.js
├── intern.config.js
└── unit
├── rxp-hpp_spec.js
└── rxp-remote_spec.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | yarn.lock
3 | package-lock.json
4 | .DS_Store
5 | _SpecRunner.html
6 | .grunt/
7 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "curly": true,
3 | "eqeqeq": true,
4 | "immed": true,
5 | "latedef": true,
6 | "newcap": true,
7 | "noarg": true,
8 | "sub": true,
9 | "undef": true,
10 | "unused": true,
11 | "boss": true,
12 | "eqnull": true,
13 | "node": true
14 | }
15 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Changelog
6 |
7 | ## Latest Version - v1.5.5 (10/15/24)
8 | - Added a logger for the steps performed by the library
9 |
10 | ## v1.5.4 (07/02/24)
11 | #### Enhancements:
12 | - Allow digital wallets payments when the mode is set to embedded
13 |
14 | ## v1.5.3 (03/04/24)
15 | #### Enhancements:
16 | - Allow digital wallets payments when the mode is set to lightbox
17 |
18 | ## v1.5.2 (06/08/23)
19 | #### Enhancements:
20 | - Allow the communication from Unified Payments
21 | - Update for the code examples
22 | ---
23 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function(grunt) {
4 |
5 | // Project configuration.
6 | grunt.initConfig({
7 | // Metadata.
8 | pkg: grunt.file.readJSON('package.json'),
9 | banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>' +
10 | '\n * <%= pkg.description %>' +
11 | '<%= pkg.homepage ? "\\n * " + pkg.homepage : "" %>' +
12 | '\n * Licensed <%= _.map(pkg.licenses, "type").join(", ") %>' +
13 | '\n */\n',
14 | // Task configuration.
15 | concat: {
16 | options: {
17 | banner: '<%= banner %>',
18 | stripBanners: true
19 | },
20 | dist: {
21 | src: ['lib/*.js'],
22 | dest: 'dist/<%= pkg.name %>.js'
23 | },
24 | },
25 | uglify: {
26 | options: {
27 | banner: '<%= banner %>'
28 | },
29 | dist: {
30 | src: '<%= concat.dist.dest %>',
31 | dest: 'dist/<%= pkg.name %>.min.js'
32 | },
33 | },
34 | jshint: {
35 | options: {
36 | jshintrc: '.jshintrc'
37 | },
38 | gruntfile: {
39 | src: 'Gruntfile.js'
40 | },
41 | lib: {
42 | options: {
43 | jshintrc: 'lib/.jshintrc'
44 | },
45 | src: ['lib/**/*.js']
46 | }
47 | },
48 | watch: {
49 | gruntfile: {
50 | files: '<%= jshint.gruntfile.src %>',
51 | tasks: ['jshint:gruntfile']
52 | },
53 | lib: {
54 | files: '<%= jshint.lib.src %>',
55 | tasks: ['jshint:lib', 'jasmine']
56 | },
57 | specs: {
58 | files: 'specs/unit/*.js',
59 | tasks: ['jasmine']
60 | }
61 | },
62 | jasmine : {
63 | src : 'lib/*.js',
64 | options: {
65 | specs: 'specs/unit/*spec.js',
66 | helpers: 'specs/unit/*helper.js'
67 | }
68 | },
69 | intern: {
70 | client: {
71 | options: {
72 | config: 'specs/intern.config'
73 | }
74 | },
75 | runner: {
76 | options: {
77 | config: 'specs/intern.config',
78 | runType: 'runner',
79 | // leaveRemoteOpen: true
80 | }
81 | }
82 | },
83 | php: {
84 | test: {
85 | options: {
86 | port: '8989',
87 | silent: true,
88 | }
89 | }
90 | }
91 | });
92 |
93 | // These plugins provide necessary tasks.
94 | grunt.loadNpmTasks('grunt-contrib-concat');
95 | grunt.loadNpmTasks('grunt-contrib-uglify');
96 | grunt.loadNpmTasks('grunt-contrib-jshint');
97 | grunt.loadNpmTasks('grunt-contrib-watch');
98 | grunt.loadNpmTasks('grunt-contrib-jasmine');
99 | grunt.loadNpmTasks('intern');
100 | grunt.loadNpmTasks('grunt-php');
101 |
102 | grunt.registerTask('test:functional', ['php', 'intern:runner']);
103 | grunt.registerTask('test:unit', ['jasmine']);
104 | grunt.registerTask('test', ['test:unit', 'test:functional']);
105 |
106 | // Default task.
107 | grunt.registerTask('default', ['jshint', 'concat', 'uglify']);
108 |
109 | };
110 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Pay and Shop Ltd t/a Global Payments
4 |
5 | Permission is hereby granted, free of charge, to any person
6 | obtaining a copy of this software and associated documentation
7 | files (the "Software"), to deal in the Software without
8 | restriction, including without limitation the rights to use,
9 | copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the
11 | Software is furnished to do so, subject to the following
12 | conditions:
13 |
14 | The above copyright notice and this permission notice shall be
15 | included in all copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24 | OTHER DEALINGS IN THE SOFTWARE.
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Realex JS Library
2 | You can sign up for a Realex account at https://developer.realexpayments.com
3 |
4 | ## Hosted Payment Page (HPP) JS Library
5 |
6 | ### Usage
7 | The Javascript required to initialise the library is below. This code must only be executed when the DOM is fully loaded. (default method: lightbox)
8 | ```javascript
9 | RealexHpp.init(payButtonId, merchantUrl, jsonFromServerSdk[, options]);
10 | ```
11 | * payButtonId - The ID of the button used to launch the lightbox. Set to "autoload" if you want to load without having to press a button
12 | * merchantUrl - could be one of 2 types:
13 | - string - The URL to which the JSON response from Realex will be posted.
14 | - function - the callback function
15 | * jsonFromServerSdk - The JSON output from the Realex HPP Server SDK.
16 | * options/events
17 | - onResize (iframe embed) Send resize iframe events so the parent frame can be adjusted
18 |
19 | ### Enable the logger
20 | The following code enables a logger that will emit an event on each step performed by the library. Should be used for debugging only.
21 | ```javascript
22 | RealexHpp.setConfigItem('enableLogging', true);
23 | window.addEventListener(RealexHpp.constants.logEventName, function(e) {
24 | console.log(e.detail);
25 | });
26 | ```
27 |
28 | ### Consuming the resulting POST
29 | Once the payment has completed the Realex JSON response will be posted within to the supplied merchantUrl. The name of the field containing the JSON response is hppResponse.
30 |
31 | If you prefer to handle response manually, provide your own callback function in "merchantUrl". The answer will be pre-parsed to an object ready to be used.
32 |
33 | ## Examples
34 |
35 | * [embedded iFrame](examples/hpp/process-a-payment-embedded.html)
36 | * [embedded iFrame autoload](examples/hpp/process-a-payment-embedded-autoload.html)
37 | * [embedded iFrame autoload, callback](examples/hpp/process-a-payment-embedded-autoload-callback.html)
38 | * [lightbox/modal](examples/hpp/process-a-payment-lightbox.html)
39 |
40 | ## Remote JS Library
41 |
42 | ### Validation functions
43 | * validateCardNumber - validates card number format and performs a Luhn check
44 | * validateCardHolderName - validates card holder name is made up from ISO/IEC 8859-1:1998 characters
45 | * validateCvn - validates non-Amex CVN
46 | * validateAmexCvn - validates Amex CVN
47 | * validateExpiryDateFormat - validates expiry date format
48 | * validateExpiryDateNotInPast - validates expiry date is not in past
49 |
50 | ### Usage
51 | ```javascript
52 | RealexRemote.validateCardNumber(cardNumber);
53 | RealexRemote.validateCardHolderName(cardHolderName);
54 | RealexRemote.validateCvn(cvn);
55 | RealexRemote.validateAmexCvn(amexCvn);
56 | RealexRemote.validateExpiryDateFormat(expiryDate);
57 | RealexRemote.validateExpiryDateNotInPast(expiryDate);
58 | ```
59 |
60 | ## License
61 | See the LICENSE file.
62 |
--------------------------------------------------------------------------------
/dist/rxp-js.js:
--------------------------------------------------------------------------------
1 | /*! rxp-js - v1.5.5 - 2024-10-15
2 | * The official Realex Payments JS Library
3 | * https://github.com/realexpayments/rxp-js
4 | * Licensed MIT
5 | */
6 | Element.prototype.remove = function() {
7 | this.parentElement.removeChild(this);
8 | };
9 | NodeList.prototype.remove = HTMLCollection.prototype.remove = function() {
10 | for(var i = this.length - 1; i >= 0; i--) {
11 | if(this[i] && this[i].parentElement) {
12 | this[i].parentElement.removeChild(this[i]);
13 | }
14 | }
15 | };
16 | var RealexHpp = (function () {
17 |
18 | 'use strict';
19 |
20 | var hppUrl = "https://pay.realexpayments.com/pay";
21 |
22 | var allowedHppUrls = [
23 | 'https://pay.realexpayments.com/pay',
24 | 'https://pay.sandbox.realexpayments.com/pay'
25 | ];
26 |
27 | var randomId = randomId || Math.random().toString(16).substr(2,8);
28 |
29 | var setHppUrl = function(url) {
30 | hppUrl = url;
31 | };
32 |
33 | var mobileXSLowerBound = 360;
34 | var setMobileXSLowerBound = function (lowerBound) {
35 | mobileXSLowerBound = lowerBound;
36 | };
37 |
38 | var config = {
39 | enableLogging: false
40 | };
41 | var setConfigItem = function(configItem, value) {
42 | if (!config.hasOwnProperty(configItem)) {
43 | return;
44 | }
45 | config[configItem] = value;
46 | };
47 | var constants = {
48 | logEventName: 'rxp-log'
49 | };
50 | var eventMessages = {
51 | form: {
52 | append: 'Form appended to the iframe',
53 | create: 'Hidden form created',
54 | submit: 'Form submitted'
55 | },
56 | iFrame: {
57 | create: 'iFrame created',
58 | find: 'iFrame found'
59 | },
60 | initialize: function(mode) {
61 | return mode + ' initialized';
62 | }
63 | }
64 | var logEvent = function(message, data = {}) {
65 | if (!config.enableLogging) {
66 | return;
67 | }
68 |
69 | var event = new CustomEvent(constants.logEventName, { detail: { message: message, data: data } });
70 | window.dispatchEvent(event);
71 | };
72 |
73 | var isWindowsMobileOs = /Windows Phone|IEMobile/.test(navigator.userAgent);
74 | var isAndroidOrIOs = /Android|iPad|iPhone|iPod/.test(navigator.userAgent);
75 | var isMobileXS = function () {
76 | return (((window.innerWidth > 0) ? window.innerWidth : screen.width) <= mobileXSLowerBound ? true : false) ||
77 | (((window.innerHeight > 0) ? window.innerHeight : screen.Height) <= mobileXSLowerBound ? true : false);
78 | };
79 |
80 | // Display IFrame on WIndows Phone OS mobile devices
81 | var isMobileIFrame = isWindowsMobileOs;
82 |
83 | // For IOs/Android and small screen devices always open in new tab/window
84 | var isMobileNewTab = function () {
85 | return !isWindowsMobileOs && (isAndroidOrIOs || isMobileXS());
86 | };
87 |
88 | var tabWindow;
89 |
90 | var redirectUrl;
91 |
92 | /**
93 | * Shared functionality across lightbox, embedded, and redirect display modes.
94 | */
95 | var internal = {
96 | evtMsg: [],
97 | /**
98 | * Adds a new window message event listener and tracks it for later removal
99 | *
100 | * @param {Function} evtMsgFct
101 | */
102 | addEvtMsgListener: function(evtMsgFct) {
103 | this.evtMsg.push({ fct: evtMsgFct, opt: false });
104 | if (window.addEventListener) {
105 | window.addEventListener("message", evtMsgFct, false);
106 | } else {
107 | window.attachEvent('message', evtMsgFct);
108 | }
109 | },
110 | /**
111 | * Removes a previously set window message event listener
112 | */
113 | removeOldEvtMsgListener: function () {
114 | if (this.evtMsg.length > 0) {
115 | var evt = this.evtMsg.pop();
116 | if (window.addEventListener) {
117 | window.removeEventListener("message", evt.fct, evt.opt);
118 | } else {
119 | window.detachEvent('message', evt.fct);
120 | }
121 | }
122 | },
123 | /**
124 | * Shimmed base64 encode/decode support
125 | */
126 | base64:{
127 | encode:function(input) {
128 | var keyStr = "ABCDEFGHIJKLMNOP" +
129 | "QRSTUVWXYZabcdef" +
130 | "ghijklmnopqrstuv" +
131 | "wxyz0123456789+/" +
132 | "=";
133 | input = escape(input);
134 | var output = "";
135 | var chr1, chr2, chr3 = "";
136 | var enc1, enc2, enc3, enc4 = "";
137 | var i = 0;
138 |
139 | do {
140 | chr1 = input.charCodeAt(i++);
141 | chr2 = input.charCodeAt(i++);
142 | chr3 = input.charCodeAt(i++);
143 |
144 | enc1 = chr1 >> 2;
145 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
146 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
147 | enc4 = chr3 & 63;
148 |
149 | if (isNaN(chr2)) {
150 | enc3 = enc4 = 64;
151 | } else if (isNaN(chr3)) {
152 | enc4 = 64;
153 | }
154 |
155 | output = output +
156 | keyStr.charAt(enc1) +
157 | keyStr.charAt(enc2) +
158 | keyStr.charAt(enc3) +
159 | keyStr.charAt(enc4);
160 | chr1 = chr2 = chr3 = "";
161 | enc1 = enc2 = enc3 = enc4 = "";
162 | } while (i < input.length);
163 |
164 | return output;
165 | },
166 | decode:function(input) {
167 | if(typeof input === 'undefined') {
168 | return input;
169 | }
170 | var keyStr = "ABCDEFGHIJKLMNOP" +
171 | "QRSTUVWXYZabcdef" +
172 | "ghijklmnopqrstuv" +
173 | "wxyz0123456789+/" +
174 | "=";
175 | var output = "";
176 | var chr1, chr2, chr3 = "";
177 | var enc1, enc2, enc3, enc4 = "";
178 | var i = 0;
179 |
180 | // remove all characters that are not A-Z, a-z, 0-9, +, /, or =
181 | var base64test = /[^A-Za-z0-9\+\/\=]/g;
182 | if (base64test.exec(input)) {
183 | throw new Error("There were invalid base64 characters in the input text.\n" +
184 | "Valid base64 characters are A-Z, a-z, 0-9, '+', '/',and '='\n" +
185 | "Expect errors in decoding.");
186 | }
187 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
188 |
189 | do {
190 | enc1 = keyStr.indexOf(input.charAt(i++));
191 | enc2 = keyStr.indexOf(input.charAt(i++));
192 | enc3 = keyStr.indexOf(input.charAt(i++));
193 | enc4 = keyStr.indexOf(input.charAt(i++));
194 |
195 | chr1 = (enc1 << 2) | (enc2 >> 4);
196 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
197 | chr3 = ((enc3 & 3) << 6) | enc4;
198 |
199 | output = output + String.fromCharCode(chr1);
200 |
201 | if (enc3 !== 64) {
202 | output = output + String.fromCharCode(chr2);
203 | }
204 | if (enc4 !== 64) {
205 | output = output + String.fromCharCode(chr3);
206 | }
207 |
208 | chr1 = chr2 = chr3 = "";
209 | enc1 = enc2 = enc3 = enc4 = "";
210 |
211 | } while (i < input.length);
212 |
213 | return unescape(output);
214 | }
215 | },
216 | /**
217 | * Converts an HPP message to a developer-friendly version.
218 | *
219 | * The decode process has two steps:
220 | *
221 | * 1. Attempt to parse the string as JSON. If this fails, an error response
222 | * is provided as we expect that the HPP has errored out to the cardholder
223 | * 2. Attempt to base64 decode the data to cover both HPP versions 1 and 2.
224 | *
225 | * @param {any} answer
226 | * @returns null if answer is not a string, otherwise the data from the HPP
227 | */
228 | decodeAnswer:function(answer){ //internal.decodeAnswer
229 |
230 | var _r;
231 |
232 | if (typeof answer !== "string") {
233 | return null;
234 | }
235 |
236 | try {
237 | _r=JSON.parse(answer);
238 | } catch (e) {
239 | _r = { error: true, message: answer };
240 | }
241 |
242 | try {
243 | for(var r in _r){
244 | if(_r[r]) {
245 | _r[r]=internal.base64.decode(_r[r]);
246 | }
247 | }
248 | } catch (e) { /** */ }
249 | return _r;
250 | },
251 | /**
252 | * Creates a new input of type `hidden`. Does not append to DOM.
253 | *
254 | * @param {string} name Name for the new input
255 | * @param {string} value Value for the new input
256 | * @returns the created input
257 | */
258 | createFormHiddenInput: function (name, value) {
259 | var el = document.createElement("input");
260 | el.setAttribute("type", "hidden");
261 | el.setAttribute("name", name);
262 | el.setAttribute("value", value);
263 | return el;
264 | },
265 |
266 | /**
267 | * Determines a mobile device's orientation for width calculation
268 | *
269 | * @returns true if in landscape
270 | */
271 | checkDevicesOrientation: function () {
272 | if (window.orientation === 90 || window.orientation === -90) {
273 | return true;
274 | } else {
275 | return false;
276 | }
277 | },
278 |
279 | /**
280 | * Creates a semi-transparent overlay with full width/height to serve as
281 | * a background for the lightbox modal
282 | *
283 | * @returns the created overlay
284 | */
285 | createOverlay: function () {
286 | var overlay = document.createElement("div");
287 | overlay.setAttribute("id", "rxp-overlay-" + randomId);
288 | overlay.style.position = "fixed";
289 | overlay.style.width = "100%";
290 | overlay.style.height = "100%";
291 | overlay.style.top = "0";
292 | overlay.style.left = "0";
293 | overlay.style.transition = "all 0.3s ease-in-out";
294 | overlay.style.zIndex = "100";
295 |
296 | if (isMobileIFrame) {
297 | overlay.style.position = "absolute !important";
298 | overlay.style.WebkitOverflowScrolling = "touch";
299 | overlay.style.overflowX = "hidden";
300 | overlay.style.overflowY = "scroll";
301 | }
302 |
303 | document.body.appendChild(overlay);
304 |
305 | setTimeout(function () {
306 | overlay.style.background = "rgba(0, 0, 0, 0.7)";
307 | }, 1);
308 |
309 | return overlay;
310 | },
311 |
312 | /**
313 | * Closes a lightbox modal and all associated elements
314 | *
315 | * @param {HTMLImageElement} closeButton
316 | * @param {HTMLIFrameElement} iFrame
317 | * @param {HTMLImageElement} spinner
318 | * @param {HTMLDivElement} overlayElement
319 | */
320 | closeModal: function (closeButton, iFrame, spinner, overlayElement) {
321 | if (closeButton && closeButton.parentNode) {
322 | closeButton.parentNode.removeChild(closeButton);
323 | }
324 |
325 | if (iFrame && iFrame.parentNode) {
326 | iFrame.parentNode.removeChild(iFrame);
327 | }
328 |
329 | if (spinner && spinner.parentNode) {
330 | spinner.parentNode.removeChild(spinner);
331 | }
332 |
333 | if (!overlayElement) {
334 | return;
335 | }
336 |
337 | overlayElement.className = "";
338 | setTimeout(function () {
339 | if (overlayElement.parentNode) {
340 | overlayElement.parentNode.removeChild(overlayElement);
341 | }
342 | }, 300);
343 | },
344 |
345 | /**
346 | * Creates a close button for the lightbox modal
347 | *
348 | * @returns the created element
349 | */
350 | createCloseButton: function (overlayElement) {
351 | if (document.getElementById("rxp-frame-close-" + randomId) !== null) {
352 | return;
353 | }
354 |
355 | var closeButton = document.createElement("img");
356 | closeButton.setAttribute("id","rxp-frame-close-" + randomId);
357 | closeButton.setAttribute("src", "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAYAAAA7bUf6AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QUJFRjU1MEIzMUQ3MTFFNThGQjNERjg2NEZCRjFDOTUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QUJFRjU1MEMzMUQ3MTFFNThGQjNERjg2NEZCRjFDOTUiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpBQkVGNTUwOTMxRDcxMUU1OEZCM0RGODY0RkJGMUM5NSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpBQkVGNTUwQTMxRDcxMUU1OEZCM0RGODY0RkJGMUM5NSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PlHco5QAAAHpSURBVHjafFRdTsJAEF42JaTKn4glGIg++qgX4AAchHAJkiZcwnAQD8AF4NFHCaC2VgWkIQQsfl/jNJUik8Duzs/XmW9mN7Xb7VRc5vP5zWKxaK5Wq8Zmu72FqobfJG0YQ9M0+/l8/qFQKDzGY1JxENd1288vLy1s786KRZXJZCLber1Wn7MZt4PLarVnWdZ9AmQ8Hncc17UvymVdBMB/MgPQm+cFFcuy6/V6lzqDf57ntWGwYdBIVx0TfkBD6I9M35iRJgfIoAVjBLDZbA4CiJ5+9AdQi/EahibqDTkQx6fRSIHcPwA8Uy9A9Gcc47Xv+w2wzhRDYzqdVihLIbsIiCvP1NNOoX/29FQx3vgOgtt4FyRdCgPRarX4+goB9vkyAMh443cOEsIAAcjncuoI4TXWMAmCIGFhCQLAdZ8jym/cRJ+Y5nC5XCYAhINKpZLgSISZgoqh5iiLQrojAFICVwGS7tCfe5DbZzkP56XS4NVxwvTI/vXVVYIDnqmnnX70ZxzjNS8THHooK5hMpxHQIREA+tEfA9djfHR3MHkdx3Hspe9r3B+VzWaj2RESyR2mlCUE4MoGQDdxiwHURq2t94+PO9bMIYyTyDNLwMoM7g8+BfKeYGniyw2MdfSehF3Qmk1IvCc/AgwAaS86Etp38bUAAAAASUVORK5CYII=");
358 | closeButton.setAttribute("style","transition: all 0.5s ease-in-out; opacity: 0; float: left; position: absolute; left: 50%; margin-left: 173px; z-index: 99999999; top: 30px;");
359 |
360 | setTimeout(function () {
361 | closeButton.style.opacity = "1";
362 | },500);
363 |
364 | if (isMobileIFrame) {
365 | closeButton.style.position = "absolute";
366 | closeButton.style.float = "right";
367 | closeButton.style.top = "20px";
368 | closeButton.style.left = "initial";
369 | closeButton.style.marginLeft = "0px";
370 | closeButton.style.right = "20px";
371 | }
372 |
373 | return closeButton;
374 | },
375 |
376 | /**
377 | * Creates a form and appends the HPP request data as hidden input elements to
378 | * POST to the defined HPP URL.
379 | *
380 | * The created form is not appended to the DOM and is not submitted at this time.
381 | *
382 | * @param {Document} doc
383 | * @param {object} token HPP request data
384 | * @param {bool} ignorePostMessage If true, the HPP will redirect to the defined
385 | * defined redirect URL. If false, the HPP will send a postMessage
386 | * to the parent window to be handled by this library.
387 | * @returns the created form
388 | */
389 | createForm: function (doc, token, ignorePostMessage) {
390 | var form = document.createElement("form");
391 | form.setAttribute("method", "POST");
392 | form.setAttribute("action", hppUrl);
393 |
394 | var versionSet = false;
395 |
396 | for (var key in token) {
397 | if (key === "HPP_VERSION"){
398 | versionSet = true;
399 | }
400 | form.appendChild(internal.createFormHiddenInput(key, token[key]));
401 | }
402 |
403 | if (versionSet === false){
404 | form.appendChild(internal.createFormHiddenInput("HPP_VERSION", "2"));
405 | }
406 |
407 | if (ignorePostMessage) {
408 | form.appendChild(internal.createFormHiddenInput("MERCHANT_RESPONSE_URL", redirectUrl));
409 | } else {
410 | var parser = internal.getUrlParser(window.location.href);
411 | var hppOriginParam = parser.protocol + '//' + parser.host;
412 |
413 | form.appendChild(internal.createFormHiddenInput("HPP_POST_RESPONSE", hppOriginParam));
414 | form.appendChild(internal.createFormHiddenInput("HPP_POST_DIMENSIONS", hppOriginParam));
415 | }
416 | return form;
417 | },
418 |
419 | /**
420 | * Creates a visual spinner element to be shown with the lightbox overlay while the
421 | * HPP's iframe loads
422 | *
423 | * @returns the created spinner element
424 | */
425 | createSpinner: function () {
426 | var spinner = document.createElement("img");
427 | spinner.setAttribute("src", "data:image/gif;base64,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");
428 | spinner.setAttribute("id", "rxp-loader-" + randomId);
429 | spinner.style.left = "50%";
430 | spinner.style.position = "fixed";
431 | spinner.style.background = "#FFFFFF";
432 | spinner.style.borderRadius = "50%";
433 | spinner.style.width = "30px";
434 | spinner.style.zIndex = "200";
435 | spinner.style.marginLeft = "-15px";
436 | spinner.style.top = "120px";
437 | return spinner;
438 | },
439 |
440 | /**
441 | * Creates the HPP's form, spinner, iframe, and close button, appends them
442 | * to the DOM, and submits the form to load the HPP
443 | *
444 | * @param {HTMLDivElement} overlayElement
445 | * @param {object} token The HPP request data
446 | * @returns an object with the created spinner, iframe, and close button
447 | */
448 | createIFrame: function (overlayElement, token) {
449 | //Create the spinner
450 | var spinner = internal.createSpinner();
451 | document.body.appendChild(spinner);
452 |
453 | //Create the iframe
454 | var iFrame = document.createElement("iframe");
455 | iFrame.setAttribute("name", "rxp-frame-" + randomId);
456 | iFrame.setAttribute("id", "rxp-frame-" + randomId);
457 | iFrame.setAttribute("height", "562px");
458 | iFrame.setAttribute("frameBorder", "0");
459 | iFrame.setAttribute("width", "360px");
460 | iFrame.setAttribute("seamless", "seamless");
461 | iFrame.setAttribute("allow", "payment " + internal.getBaseUrl(hppUrl));
462 |
463 | iFrame.style.zIndex = "10001";
464 | iFrame.style.position = "absolute";
465 | iFrame.style.transition = "transform 0.5s ease-in-out";
466 | iFrame.style.transform = "scale(0.7)";
467 | iFrame.style.opacity = "0";
468 |
469 | overlayElement.appendChild(iFrame);
470 |
471 | if (isMobileIFrame) {
472 | iFrame.style.top = "0px";
473 | iFrame.style.bottom = "0px";
474 | iFrame.style.left = "0px";
475 | iFrame.style.marginLeft = "0px;";
476 | iFrame.style.width = "100%";
477 | iFrame.style.height = "100%";
478 | iFrame.style.minHeight = "100%";
479 | iFrame.style.WebkitTransform = "translate3d(0,0,0)";
480 | iFrame.style.transform = "translate3d(0, 0, 0)";
481 |
482 | var metaTag = document.createElement('meta');
483 | metaTag.name = "viewport";
484 | metaTag.content = "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0";
485 | document.getElementsByTagName('head')[0].appendChild(metaTag);
486 | } else {
487 | iFrame.style.top = "40px";
488 | iFrame.style.left = "50%";
489 | iFrame.style.marginLeft = "-180px";
490 | }
491 |
492 | logEvent(eventMessages.iFrame.create, { iFrame: iFrame });
493 |
494 | var closeButton;
495 |
496 | iFrame.onload = function () {
497 | iFrame.style.opacity = "1";
498 | iFrame.style.transform = "scale(1)";
499 | iFrame.style.backgroundColor = "#ffffff";
500 |
501 | if (spinner && spinner.parentNode) {
502 | spinner.parentNode.removeChild(spinner);
503 | }
504 |
505 | closeButton = internal.createCloseButton();
506 |
507 | if (overlayElement && closeButton) {
508 | overlayElement.appendChild(closeButton);
509 | closeButton.addEventListener("click", function () {
510 | internal.closeModal(closeButton, iFrame, spinner, overlayElement);
511 | }, true);
512 | }
513 | };
514 |
515 | var form = internal.createForm(document, token);
516 | logEvent(eventMessages.form.create, { form: form });
517 | if (iFrame.contentWindow.document.body) {
518 | iFrame.contentWindow.document.body.appendChild(form);
519 | logEvent(eventMessages.form.append);
520 | } else {
521 | iFrame.contentWindow.document.appendChild(form);
522 | logEvent(eventMessages.form.append);
523 | }
524 |
525 | form.submit();
526 | logEvent(eventMessages.form.submit);
527 |
528 | return {
529 | spinner: spinner,
530 | iFrame: iFrame,
531 | closeButton: closeButton
532 | };
533 | },
534 |
535 | /**
536 | * Opens the HPP in a new window
537 | *
538 | * Used in some mobile scenarios or when the browser viewport is
539 | * smaller than the HPP's inner width.
540 | *
541 | * Will automatically post the request data to the defined HPP
542 | * URL to load the HPP.
543 | *
544 | * @param {object} token The HPP request data
545 | * @returns the created window
546 | */
547 | openWindow: function (token) {
548 | //open new window
549 | var tabWindow = window.open();
550 |
551 | // browsers can prevent a new window from being created
552 | // e.g. mobile Safari
553 | if (!tabWindow) {
554 | return null;
555 | }
556 |
557 | var doc = tabWindow.document;
558 |
559 | //add meta tag to new window (needed for iOS 8 bug)
560 | var meta = doc.createElement("meta");
561 | var name = doc.createAttribute("name");
562 | name.value = "viewport";
563 | meta.setAttributeNode(name);
564 | var content = doc.createAttribute("content");
565 | content.value = "width=device-width";
566 | meta.setAttributeNode(content);
567 | doc.head.appendChild(meta);
568 |
569 | //create form, append to new window and submit
570 | var form = internal.createForm(doc, token);
571 | doc.body.appendChild(form);
572 | form.submit();
573 |
574 | return tabWindow;
575 | },
576 |
577 | /**
578 | * Creates a rudimentary URL parser using an anchor element
579 | *
580 | * @param {string} url
581 | * @returns the created anchor element
582 | */
583 | getUrlParser: function (url) {
584 | var parser = document.createElement('a');
585 | parser.href = url;
586 | return parser;
587 | },
588 |
589 | /**
590 | * Gets the hostname/origin from a URL. Used for origin checks
591 | *
592 | * @param {string} url
593 | * @returns the hostname/origin of the URL
594 | */
595 | getHostnameFromUrl: function (url) {
596 | return internal.getUrlParser(url).hostname;
597 | },
598 |
599 | /**
600 | * Gets the base URL from a URL. Used to set the 'allow payment' attribute.
601 | *
602 | * @param {string} url
603 | * @returns the base URL of the provided URL
604 | */
605 | getBaseUrl: function (url) {
606 | var urlParser = internal.getUrlParser(url);
607 | return urlParser.protocol + '//' + urlParser.hostname;
608 | },
609 |
610 | /**
611 | * Checks if the origin is HPP.
612 | *
613 | * @param {string} origin
614 | * @returns {boolean}
615 | */
616 | isHppOrigin: function(origin) {
617 | var result = false;
618 | allowedHppUrls.forEach(function (url) {
619 | if (internal.getHostnameFromUrl(url) === origin) {
620 | result = true;
621 | }
622 | });
623 |
624 | return result;
625 | },
626 |
627 | /**
628 | * Compares the origins from both arguments to validate we have received a postMessage
629 | * from the expected source
630 | *
631 | * @param {string} origin The origin attached to the recieved message
632 | * @param {string} hppUrl Our expected source origin
633 | * @returns true if the origins match
634 | */
635 | isMessageFromHpp: function (origin, hppUrl) {
636 | var originHostName = internal.getHostnameFromUrl(origin);
637 | return originHostName === internal.getHostnameFromUrl(hppUrl) || internal.isHppOrigin(originHostName);
638 | },
639 |
640 | /**
641 | * Handles messages from the HPP
642 | *
643 | * Messages from the HPP are one of:
644 | *
645 | * - iframe resize event
646 | * - transaction response
647 | * - error information
648 | *
649 | * @param {MessageEvent} e
650 | */
651 | receiveMessage: function (e) {
652 | //Check the origin of the response comes from HPP
653 | if (!internal.isMessageFromHpp(e.event.origin, hppUrl)) {
654 | return;
655 | }
656 |
657 | if (!e.event.data) {
658 | return;
659 | }
660 |
661 | var evtdata = internal.decodeAnswer(e.event.data);
662 |
663 | // we received an invalid message from the HPP iframe (e.g. from a browser plugin)
664 | // return early to prevent invalid processing
665 | if (evtdata === null) {
666 | return;
667 | }
668 |
669 | // check for iframe resize values
670 | if (evtdata.iframe) {
671 | if (!isMobileNewTab()) {
672 | var iframeWidth = evtdata.iframe.width;
673 | var iframeHeight = evtdata.iframe.height;
674 |
675 | var iFrame;
676 | var resized = false;
677 |
678 | if (e.embedded) {
679 | iFrame = e.instance.getIframe();
680 | } else {
681 | iFrame = document.getElementById("rxp-frame-" + randomId);
682 | }
683 | if (e.instance.events && e.instance.events.onResize) {
684 | e.instance.events.onResize(evtdata.iframe);
685 | }
686 |
687 | if (iframeWidth === "390px" && iframeHeight === "440px") {
688 | iFrame.setAttribute("width", iframeWidth);
689 | iFrame.setAttribute("height", iframeHeight);
690 | resized = true;
691 | }
692 |
693 | iFrame.style.backgroundColor="#ffffff";
694 |
695 | if (isMobileIFrame) {
696 | iFrame.style.marginLeft = "0px";
697 | iFrame.style.WebkitOverflowScrolling = "touch";
698 | iFrame.style.overflowX = "scroll";
699 | iFrame.style.overflowY = "scroll";
700 |
701 | if (!e.embedded) {
702 | var overlay = document.getElementById("rxp-overlay-" + randomId);
703 | overlay.style.overflowX = "scroll";
704 | overlay.style.overflowY = "scroll";
705 | }
706 | } else if (!e.embedded && resized) {
707 | iFrame.style.marginLeft = (parseInt(iframeWidth.replace("px", ""), 10) / 2 * -1) + "px";
708 | }
709 |
710 | if (!e.embedded && resized) {
711 | // wrap the below in a setTimeout to prevent a timing issue on a
712 | // cache-miss load
713 | setTimeout(function () {
714 | var closeButton = document.getElementById("rxp-frame-close-" + randomId);
715 | closeButton.style.marginLeft = ((parseInt(iframeWidth.replace("px", ""), 10) / 2) -7) + "px";
716 | }, 200);
717 | }
718 | }
719 | } else {
720 | var _close=function(){
721 | if (isMobileNewTab() && tabWindow) {
722 | //Close the new window
723 | tabWindow.close();
724 | } else {
725 | //Close the lightbox
726 | e.instance.close();
727 | }
728 | var overlay=document.getElementById("rxp-overlay-" + randomId);
729 | if(overlay) {
730 | overlay.remove();
731 | }
732 |
733 | };
734 | var response = e.event.data;
735 | //allow the script to intercept the answer, instead of redirecting to another page. (which is really a 90s thing)
736 | if (typeof e.url === 'function'){
737 | e.url(evtdata, _close);
738 | return;
739 | }
740 | _close();
741 | //Create a form and submit the hpp response to the merchant's response url
742 | var form = document.createElement("form");
743 | form.setAttribute("method", "POST");
744 | form.setAttribute("action", e.url);
745 | form.appendChild(internal.createFormHiddenInput("hppResponse", response));
746 | document.body.appendChild(form);
747 | form.submit();
748 | }
749 | }
750 | };
751 |
752 | /**
753 | * Public interface for the lightbox display mode
754 | */
755 | var RxpLightbox = (function () {
756 | var instance;
757 |
758 | function init() {
759 | var overlayElement;
760 | var spinner;
761 | var iFrame;
762 | var closeButton;
763 | var token;
764 | var isLandscape = internal.checkDevicesOrientation();
765 |
766 | if (isMobileIFrame) {
767 | if (window.addEventListener) {
768 | window.addEventListener("orientationchange", function () {
769 | isLandscape = internal.checkDevicesOrientation();
770 | }, false);
771 | }
772 | }
773 |
774 | return {
775 | lightbox: function () {
776 | if (isMobileNewTab()) {
777 | tabWindow = internal.openWindow(token);
778 | } else {
779 | overlayElement = internal.createOverlay();
780 | var temp = internal.createIFrame(overlayElement, token);
781 | spinner = temp.spinner;
782 | iFrame = temp.iFrame;
783 | closeButton = temp.closeButton;
784 | }
785 | },
786 | close: function () {
787 | internal.closeModal();
788 | },
789 | setToken: function (hppToken) {
790 | token = hppToken;
791 | }
792 | };
793 | }
794 |
795 | return {
796 | // Get the Singleton instance if one exists
797 | // or create one if it doesn't
798 | getInstance: function (hppToken) {
799 | if (!instance) {
800 | instance = init();
801 | }
802 |
803 | //Set the hpp token
804 | instance.setToken(hppToken);
805 |
806 | return instance;
807 | },
808 | init: function (idOfLightboxButton, merchantUrl, serverSdkJson) {
809 | logEvent(eventMessages.initialize('RxpLightbox'));
810 | //Get the lightbox instance (it's a singleton) and set the sdk json
811 | var lightboxInstance = RxpLightbox.getInstance(serverSdkJson);
812 |
813 | //if you want the form to load on function call, set to autoload
814 | if (idOfLightboxButton === 'autoload') {
815 | lightboxInstance.lightbox();
816 | }
817 | // Sets the event listener on the PAY button. The click will invoke the lightbox method
818 | else if (document.getElementById(idOfLightboxButton).addEventListener) {
819 | document.getElementById(idOfLightboxButton).addEventListener("click", lightboxInstance.lightbox, true);
820 | } else {
821 | document.getElementById(idOfLightboxButton).attachEvent('onclick', lightboxInstance.lightbox);
822 | }
823 | //avoid multiple message event listener binded to the window object.
824 | internal.removeOldEvtMsgListener();
825 | var evtMsgFct = function (event) {
826 | return internal.receiveMessage({ event: event, instance: lightboxInstance, url: merchantUrl, embedded: false });
827 | };
828 | internal.evtMsg.push({ fct: evtMsgFct, opt: false });
829 | internal.addEvtMsgListener(evtMsgFct);
830 | }
831 | };
832 | })();
833 |
834 | /**
835 | * Public interface for the embedded display mode
836 | */
837 | var RxpEmbedded = (function () {
838 | var instance;
839 |
840 | function init() {
841 | var overlayElement;
842 | var spinner;
843 | var iFrame;
844 | var closeButton;
845 | var token;
846 |
847 | return {
848 | embedded: function () {
849 | var form = internal.createForm(document, token);
850 | logEvent(eventMessages.form.create, { form: form });
851 | if (iFrame) {
852 | logEvent(eventMessages.iFrame.find, { iFrame: iFrame });
853 | if (iFrame.contentWindow.document.body) {
854 | iFrame.contentWindow.document.body.appendChild(form);
855 | logEvent(eventMessages.form.append);
856 | } else {
857 | iFrame.contentWindow.document.appendChild(form);
858 | logEvent(eventMessages.form.append);
859 | }
860 | form.submit();
861 | logEvent(eventMessages.form.submit);
862 | iFrame.style.display = "inherit";
863 | }
864 | },
865 | close: function () {
866 | iFrame.style.display = "none";
867 | },
868 | setToken: function (hppToken) {
869 | token = hppToken;
870 | },
871 | setIframe: function (iframeId) {
872 | iFrame = document.getElementById(iframeId);
873 | if (iFrame) {
874 | iFrame.setAttribute("allow", "payment " + internal.getBaseUrl(hppUrl));
875 | }
876 | },
877 | getIframe: function () {
878 | return iFrame;
879 | }
880 | };
881 | }
882 |
883 | return {
884 | // Get the Singleton instance if one exists
885 | // or create one if it doesn't
886 | getInstance: function (hppToken) {
887 | if (!instance) {
888 | instance = init();
889 | }
890 |
891 | //Set the hpp token
892 | instance.setToken(hppToken);
893 | return instance;
894 | },
895 | init: function (idOfEmbeddedButton, idOfTargetIframe, merchantUrl, serverSdkJson,events) {
896 | logEvent(eventMessages.initialize('RxpEmbedded'));
897 |
898 | //Get the embedded instance (it's a singleton) and set the sdk json
899 | var embeddedInstance = RxpEmbedded.getInstance(serverSdkJson);
900 | embeddedInstance.events=events;
901 |
902 | embeddedInstance.setIframe(idOfTargetIframe);
903 | //if you want the form to load on function call, set to autoload
904 | if (idOfEmbeddedButton === 'autoload') {
905 | embeddedInstance.embedded();
906 | }
907 | // Sets the event listener on the PAY button. The click will invoke the embedded method
908 | else if (document.getElementById(idOfEmbeddedButton).addEventListener) {
909 | document.getElementById(idOfEmbeddedButton).addEventListener("click", embeddedInstance.embedded, true);
910 | } else {
911 | document.getElementById(idOfEmbeddedButton).attachEvent('onclick', embeddedInstance.embedded);
912 | }
913 |
914 | //avoid multiple message event listener binded to the window object.
915 | internal.removeOldEvtMsgListener();
916 | var evtMsgFct = function (event) {
917 | return internal.receiveMessage({ event: event, instance: embeddedInstance, url: merchantUrl, embedded: true });
918 | };
919 | internal.evtMsg.push({ fct: evtMsgFct, opt: false });
920 | internal.addEvtMsgListener(evtMsgFct);
921 | }
922 | };
923 | })();
924 |
925 | /**
926 | * Public interface for the redirect display mode
927 | */
928 | var RxpRedirect = (function () {
929 | var instance;
930 |
931 | function init() {
932 | var overlayElement;
933 | var spinner;
934 | var iFrame;
935 | var closeButton;
936 | var token;
937 | var isLandscape = internal.checkDevicesOrientation();
938 |
939 | if (isMobileIFrame) {
940 | if (window.addEventListener) {
941 | window.addEventListener("orientationchange", function () {
942 | isLandscape = internal.checkDevicesOrientation();
943 | }, false);
944 | }
945 | }
946 |
947 | return {
948 | redirect: function () {
949 | var form = internal.createForm(document, token, true);
950 | logEvent(eventMessages.form.create, { form: form });
951 | document.body.append(form);
952 | form.submit();
953 | logEvent(eventMessages.form.submit);
954 | },
955 | setToken: function (hppToken) {
956 | token = hppToken;
957 | }
958 | };
959 | }
960 | return {
961 | // Get the singleton instance if one exists
962 | // or create one if it doesn't
963 | getInstance: function (hppToken) {
964 | if (!instance) {
965 | instance = init();
966 | }
967 |
968 | // Set the hpp token
969 | instance.setToken(hppToken);
970 |
971 | return instance;
972 | },
973 | init: function (idOfButton, merchantUrl, serverSdkJson) {
974 | logEvent(eventMessages.initialize('RxpRedirect'));
975 |
976 | // Get the redirect instance (it's a singleton) and set the sdk json
977 | var redirectInstance = RxpRedirect.getInstance(serverSdkJson);
978 | redirectUrl = merchantUrl;
979 |
980 | // Sets the event listener on the PAY button. The click will invoke the redirect method
981 | if (document.getElementById(idOfButton).addEventListener) {
982 | document.getElementById(idOfButton).addEventListener("click", redirectInstance.redirect, true);
983 | } else {
984 | document.getElementById(idOfButton).attachEvent('onclick', redirectInstance.redirect);
985 | }
986 |
987 | //avoid multiple message event listener binded to the window object.
988 | internal.removeOldEvtMsgListener();
989 | var evtMsgFct = function (event) {
990 | return internal.receiveMessage({ event: event, instance: redirectInstance, url: merchantUrl, embedded: false });
991 | };
992 | internal.evtMsg.push({ fct: evtMsgFct, opt: false });
993 | internal.addEvtMsgListener(evtMsgFct);
994 | }
995 | };
996 | }());
997 |
998 | /**
999 | * Public interface for the Realex HPP library
1000 | */
1001 | return {
1002 | init: RxpLightbox.init,
1003 | lightbox: {
1004 | init: RxpLightbox.init
1005 | },
1006 | embedded: {
1007 | init: RxpEmbedded.init
1008 | },
1009 | redirect: {
1010 | init: RxpRedirect.init
1011 | },
1012 | setHppUrl: setHppUrl,
1013 | setMobileXSLowerBound: setMobileXSLowerBound,
1014 | setConfigItem: setConfigItem,
1015 | constants: constants,
1016 | _internal: internal
1017 | };
1018 |
1019 | }());
1020 | var RealexRemote = (function() {
1021 |
1022 | 'use strict';
1023 |
1024 | /*
1025 | * Validate Card Number. Returns true if card number valid. Only allows
1026 | * non-empty numeric values between 12 and 19 characters. A Luhn check is
1027 | * also run against the card number.
1028 | */
1029 | var validateCardNumber = function(cardNumber) {
1030 | // test numeric and length between 12 and 19
1031 | if (!/^\d{12,19}$/.test(cardNumber)) {
1032 | return false;
1033 | }
1034 |
1035 | // luhn check
1036 | var sum = 0;
1037 | var digit = 0;
1038 | var addend = 0;
1039 | var timesTwo = false;
1040 |
1041 | for (var i = cardNumber.length - 1; i >= 0; i--) {
1042 | digit = parseInt(cardNumber.substring(i, i + 1), 10);
1043 | if (timesTwo) {
1044 | addend = digit * 2;
1045 | if (addend > 9) {
1046 | addend -= 9;
1047 | }
1048 | } else {
1049 | addend = digit;
1050 | }
1051 | sum += addend;
1052 | timesTwo = !timesTwo;
1053 | }
1054 |
1055 | var modulus = sum % 10;
1056 | if (modulus !== 0) {
1057 | return false;
1058 | }
1059 |
1060 | return true;
1061 | };
1062 |
1063 | /*
1064 | * Validate Card Holder Name. Returns true if card holder valid. Only allows
1065 | * non-empty ISO/IEC 8859-1 values 100 characters or less.
1066 | */
1067 | var validateCardHolderName = function(cardHolderName) {
1068 | // test for undefined
1069 | if (!cardHolderName) {
1070 | return false;
1071 | }
1072 |
1073 | // test white space only
1074 | if (!cardHolderName.trim()) {
1075 | return false;
1076 | }
1077 |
1078 | // test ISO/IEC 8859-1 characters between 1 and 100
1079 | if (!/^[\u0020-\u007E\u00A0-\u00FF]{1,100}$/.test(cardHolderName)) {
1080 | return false;
1081 | }
1082 |
1083 | return true;
1084 | };
1085 |
1086 | /*
1087 | * Validate CVN. Applies to non-Amex card types. Only allows 3 numeric
1088 | * characters.
1089 | */
1090 | var validateCvn = function(cvn) {
1091 | // test numeric length 3
1092 | if (!/^\d{3}$/.test(cvn)) {
1093 | return false;
1094 | }
1095 |
1096 | return true;
1097 | };
1098 |
1099 | /*
1100 | * Validate Amex CVN. Applies to Amex card types. Only allows 4 numeric
1101 | * characters.
1102 | */
1103 | var validateAmexCvn = function(cvn) {
1104 | // test numeric length 4
1105 | if (!/^\d{4}$/.test(cvn)) {
1106 | return false;
1107 | }
1108 |
1109 | return true;
1110 | };
1111 |
1112 | /*
1113 | * Validate Expiry Date Format. Only allows 4 numeric characters. Month must
1114 | * be between 1 and 12.
1115 | */
1116 | var validateExpiryDateFormat = function(expiryDate) {
1117 |
1118 | // test numeric of length 4
1119 | if (!/^\d{4}$/.test(expiryDate)) {
1120 | return false;
1121 | }
1122 |
1123 | var month = parseInt(expiryDate.substring(0, 2), 10);
1124 | var year = parseInt(expiryDate.substring(2, 4), 10);
1125 |
1126 | // test month range is 1-12
1127 | if (month < 1 || month > 12) {
1128 | return false;
1129 | }
1130 |
1131 | return true;
1132 | };
1133 |
1134 | /*
1135 | * Validate Expiry Date Not In Past. Also runs checks from
1136 | * validateExpiryDateFormat.
1137 | */
1138 | var validateExpiryDateNotInPast = function(expiryDate) {
1139 | // test valid format
1140 | if (!validateExpiryDateFormat(expiryDate)) {
1141 | return false;
1142 | }
1143 |
1144 | var month = parseInt(expiryDate.substring(0, 2), 10);
1145 | var year = parseInt(expiryDate.substring(2, 4), 10);
1146 |
1147 | // test date is not in the past
1148 | var now = new Date();
1149 | var currentMonth = now.getMonth() + 1;
1150 | var currentYear = now.getFullYear();
1151 | if (year < (currentYear % 100)) {
1152 | return false;
1153 | } else if (year === (currentYear % 100) && month < currentMonth) {
1154 | return false;
1155 | }
1156 |
1157 | return true;
1158 | };
1159 |
1160 | return {
1161 | validateCardNumber : validateCardNumber,
1162 | validateCardHolderName : validateCardHolderName,
1163 | validateCvn : validateCvn,
1164 | validateAmexCvn : validateAmexCvn,
1165 | validateExpiryDateFormat : validateExpiryDateFormat,
1166 | validateExpiryDateNotInPast : validateExpiryDateNotInPast
1167 | };
1168 | }());
1169 |
--------------------------------------------------------------------------------
/dist/rxp-js.min.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/globalpayments/rxp-js/f014bb09885a9d40d9b88c96eddf0df26e525088/dist/rxp-js.min.js
--------------------------------------------------------------------------------
/examples/hpp/helper.js:
--------------------------------------------------------------------------------
1 | function success(response) {
2 | console.log('Successful transaction. Message: ', response.MESSAGE);
3 | }
4 |
5 | $(document).ready(function() {
6 | $('#paymentMethod').on('change', function() {
7 | RealexHpp.setHppUrl(this.value);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/examples/hpp/json/process-a-payment.json:
--------------------------------------------------------------------------------
1 | {
2 | "MERCHANT_ID": "heartlandgpsandbox",
3 | "ACCOUNT": "hpp",
4 | "AMOUNT": "1000",
5 | "CURRENCY": "EUR",
6 | "AUTO_SETTLE_FLAG": "1",
7 |
8 | "HPP_CUSTOMER_EMAIL": "test@example.com",
9 | "HPP_CUSTOMER_PHONENUMBER_MOBILE": "44|789456123",
10 | "HPP_BILLING_STREET1": "Flat 123",
11 | "HPP_BILLING_STREET2": "House 456",
12 | "HPP_BILLING_STREET3": "Unit 4",
13 | "HPP_BILLING_CITY": "Halifax",
14 | "HPP_BILLING_POSTALCODE": "W5 9HR",
15 | "HPP_BILLING_COUNTRY": "826",
16 | "HPP_SHIPPING_STREET1": "Apartment 852",
17 | "HPP_SHIPPING_STREET2": "Complex 741",
18 | "HPP_SHIPPING_STREET3": "House 963",
19 | "HPP_SHIPPING_CITY": "Chicago",
20 | "HPP_SHIPPING_STATE": "IL",
21 | "HPP_SHIPPING_POSTALCODE": "50001",
22 | "HPP_SHIPPING_COUNTRY": "840",
23 | "HPP_ADDRESS_MATCH_INDICATOR": "FALSE",
24 | "HPP_CHALLENGE_REQUEST_INDICATOR": "NO_PREFERENCE"
25 | }
--------------------------------------------------------------------------------
/examples/hpp/process-a-payment-embedded-autoload-callback.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | HPP embed Demo
5 |
6 |
12 |
13 |
14 |
15 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/examples/hpp/process-a-payment-embedded-autoload.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | HPP embed Demo
5 |
6 |
12 |
13 |
14 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/examples/hpp/process-a-payment-embedded.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | HPP embed Demo
5 |
6 |
12 |
13 |
14 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/examples/hpp/process-a-payment-lightbox-callback.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | HPP Lightbox Demo
5 |
6 |
7 |
8 |
9 |
37 |
38 |
39 |
40 |
41 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/examples/hpp/process-a-payment-lightbox.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | HPP Lightbox Demo
5 |
6 |
7 |
8 |
9 |
30 |
31 |
32 |
33 |
34 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/examples/hpp/proxy-request.php:
--------------------------------------------------------------------------------
1 | $value) {
66 | if (!$value) {
67 | continue;
68 | }
69 | $response[$key] = $value;
70 | }
71 |
72 | $response["ORDER_ID"] = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz0123456789'), 0, 22);
73 | $response["TIMESTAMP"] = (new DateTime())->format("YmdHis");
74 | $response["SHA1HASH"] = generateHash($response, 'secret');
75 |
76 | $jsonResponse = json_encode($response);
77 |
78 | error_log('sending: ' . $jsonResponse);
79 | echo $jsonResponse;
80 |
--------------------------------------------------------------------------------
/examples/hpp/redirect-for-payment.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | HPP Redirect Demo
5 |
6 |
7 |
8 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/examples/hpp/response.php:
--------------------------------------------------------------------------------
1 | $v) {
18 | try {
19 | $hppResponse[$k] = base64_decode($v);
20 | } catch (Exception $e) {
21 | /* */
22 | }
23 | }
24 | }
25 | catch (Exception $e) {
26 | $hppResponse = $originalHppResponse;
27 | }
28 |
29 | ?>
30 |
31 |
32 | HPP Demo Response
33 |
34 |
35 |
36 | HPP Demo Response
37 |
40 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/lib/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "curly": true,
3 | "eqeqeq": true,
4 | "immed": true,
5 | "latedef": true,
6 | "newcap": true,
7 | "noarg": true,
8 | "sub": true,
9 | "undef": true,
10 | "boss": true,
11 | "eqnull": true,
12 | "predef": ["exports", "alert", "escape", "unescape"]
13 | }
14 |
--------------------------------------------------------------------------------
/lib/rxp-hpp.js:
--------------------------------------------------------------------------------
1 | /*jslint browser:true */
2 | Element.prototype.remove = function() {
3 | this.parentElement.removeChild(this);
4 | };
5 | NodeList.prototype.remove = HTMLCollection.prototype.remove = function() {
6 | for(var i = this.length - 1; i >= 0; i--) {
7 | if(this[i] && this[i].parentElement) {
8 | this[i].parentElement.removeChild(this[i]);
9 | }
10 | }
11 | };
12 | var RealexHpp = (function () {
13 |
14 | 'use strict';
15 |
16 | var hppUrl = "https://pay.realexpayments.com/pay";
17 |
18 | var allowedHppUrls = [
19 | 'https://pay.realexpayments.com/pay',
20 | 'https://pay.sandbox.realexpayments.com/pay'
21 | ];
22 |
23 | var randomId = randomId || Math.random().toString(16).substr(2,8);
24 |
25 | var setHppUrl = function(url) {
26 | hppUrl = url;
27 | };
28 |
29 | var mobileXSLowerBound = 360;
30 | var setMobileXSLowerBound = function (lowerBound) {
31 | mobileXSLowerBound = lowerBound;
32 | };
33 |
34 | var config = {
35 | enableLogging: false
36 | };
37 | var setConfigItem = function(configItem, value) {
38 | if (!config.hasOwnProperty(configItem)) {
39 | return;
40 | }
41 | config[configItem] = value;
42 | };
43 | var constants = {
44 | logEventName: 'rxp-log'
45 | };
46 | var eventMessages = {
47 | form: {
48 | append: 'Form appended to the iframe',
49 | create: 'Hidden form created',
50 | submit: 'Form submitted'
51 | },
52 | iFrame: {
53 | create: 'iFrame created',
54 | find: 'iFrame found'
55 | },
56 | initialize: function(mode) {
57 | return mode + ' initialized';
58 | }
59 | }
60 | var logEvent = function(message, data = {}) {
61 | if (!config.enableLogging) {
62 | return;
63 | }
64 |
65 | var event = new CustomEvent(constants.logEventName, { detail: { message: message, data: data } });
66 | window.dispatchEvent(event);
67 | };
68 |
69 | var isWindowsMobileOs = /Windows Phone|IEMobile/.test(navigator.userAgent);
70 | var isAndroidOrIOs = /Android|iPad|iPhone|iPod/.test(navigator.userAgent);
71 | var isMobileXS = function () {
72 | return (((window.innerWidth > 0) ? window.innerWidth : screen.width) <= mobileXSLowerBound ? true : false) ||
73 | (((window.innerHeight > 0) ? window.innerHeight : screen.Height) <= mobileXSLowerBound ? true : false);
74 | };
75 |
76 | // Display IFrame on WIndows Phone OS mobile devices
77 | var isMobileIFrame = isWindowsMobileOs;
78 |
79 | // For IOs/Android and small screen devices always open in new tab/window
80 | var isMobileNewTab = function () {
81 | return !isWindowsMobileOs && (isAndroidOrIOs || isMobileXS());
82 | };
83 |
84 | var tabWindow;
85 |
86 | var redirectUrl;
87 |
88 | /**
89 | * Shared functionality across lightbox, embedded, and redirect display modes.
90 | */
91 | var internal = {
92 | evtMsg: [],
93 | /**
94 | * Adds a new window message event listener and tracks it for later removal
95 | *
96 | * @param {Function} evtMsgFct
97 | */
98 | addEvtMsgListener: function(evtMsgFct) {
99 | this.evtMsg.push({ fct: evtMsgFct, opt: false });
100 | if (window.addEventListener) {
101 | window.addEventListener("message", evtMsgFct, false);
102 | } else {
103 | window.attachEvent('message', evtMsgFct);
104 | }
105 | },
106 | /**
107 | * Removes a previously set window message event listener
108 | */
109 | removeOldEvtMsgListener: function () {
110 | if (this.evtMsg.length > 0) {
111 | var evt = this.evtMsg.pop();
112 | if (window.addEventListener) {
113 | window.removeEventListener("message", evt.fct, evt.opt);
114 | } else {
115 | window.detachEvent('message', evt.fct);
116 | }
117 | }
118 | },
119 | /**
120 | * Shimmed base64 encode/decode support
121 | */
122 | base64:{
123 | encode:function(input) {
124 | var keyStr = "ABCDEFGHIJKLMNOP" +
125 | "QRSTUVWXYZabcdef" +
126 | "ghijklmnopqrstuv" +
127 | "wxyz0123456789+/" +
128 | "=";
129 | input = escape(input);
130 | var output = "";
131 | var chr1, chr2, chr3 = "";
132 | var enc1, enc2, enc3, enc4 = "";
133 | var i = 0;
134 |
135 | do {
136 | chr1 = input.charCodeAt(i++);
137 | chr2 = input.charCodeAt(i++);
138 | chr3 = input.charCodeAt(i++);
139 |
140 | enc1 = chr1 >> 2;
141 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
142 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
143 | enc4 = chr3 & 63;
144 |
145 | if (isNaN(chr2)) {
146 | enc3 = enc4 = 64;
147 | } else if (isNaN(chr3)) {
148 | enc4 = 64;
149 | }
150 |
151 | output = output +
152 | keyStr.charAt(enc1) +
153 | keyStr.charAt(enc2) +
154 | keyStr.charAt(enc3) +
155 | keyStr.charAt(enc4);
156 | chr1 = chr2 = chr3 = "";
157 | enc1 = enc2 = enc3 = enc4 = "";
158 | } while (i < input.length);
159 |
160 | return output;
161 | },
162 | decode:function(input) {
163 | if(typeof input === 'undefined') {
164 | return input;
165 | }
166 | var keyStr = "ABCDEFGHIJKLMNOP" +
167 | "QRSTUVWXYZabcdef" +
168 | "ghijklmnopqrstuv" +
169 | "wxyz0123456789+/" +
170 | "=";
171 | var output = "";
172 | var chr1, chr2, chr3 = "";
173 | var enc1, enc2, enc3, enc4 = "";
174 | var i = 0;
175 |
176 | // remove all characters that are not A-Z, a-z, 0-9, +, /, or =
177 | var base64test = /[^A-Za-z0-9\+\/\=]/g;
178 | if (base64test.exec(input)) {
179 | throw new Error("There were invalid base64 characters in the input text.\n" +
180 | "Valid base64 characters are A-Z, a-z, 0-9, '+', '/',and '='\n" +
181 | "Expect errors in decoding.");
182 | }
183 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
184 |
185 | do {
186 | enc1 = keyStr.indexOf(input.charAt(i++));
187 | enc2 = keyStr.indexOf(input.charAt(i++));
188 | enc3 = keyStr.indexOf(input.charAt(i++));
189 | enc4 = keyStr.indexOf(input.charAt(i++));
190 |
191 | chr1 = (enc1 << 2) | (enc2 >> 4);
192 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
193 | chr3 = ((enc3 & 3) << 6) | enc4;
194 |
195 | output = output + String.fromCharCode(chr1);
196 |
197 | if (enc3 !== 64) {
198 | output = output + String.fromCharCode(chr2);
199 | }
200 | if (enc4 !== 64) {
201 | output = output + String.fromCharCode(chr3);
202 | }
203 |
204 | chr1 = chr2 = chr3 = "";
205 | enc1 = enc2 = enc3 = enc4 = "";
206 |
207 | } while (i < input.length);
208 |
209 | return unescape(output);
210 | }
211 | },
212 | /**
213 | * Converts an HPP message to a developer-friendly version.
214 | *
215 | * The decode process has two steps:
216 | *
217 | * 1. Attempt to parse the string as JSON. If this fails, an error response
218 | * is provided as we expect that the HPP has errored out to the cardholder
219 | * 2. Attempt to base64 decode the data to cover both HPP versions 1 and 2.
220 | *
221 | * @param {any} answer
222 | * @returns null if answer is not a string, otherwise the data from the HPP
223 | */
224 | decodeAnswer:function(answer){ //internal.decodeAnswer
225 |
226 | var _r;
227 |
228 | if (typeof answer !== "string") {
229 | return null;
230 | }
231 |
232 | try {
233 | _r=JSON.parse(answer);
234 | } catch (e) {
235 | _r = { error: true, message: answer };
236 | }
237 |
238 | try {
239 | for(var r in _r){
240 | if(_r[r]) {
241 | _r[r]=internal.base64.decode(_r[r]);
242 | }
243 | }
244 | } catch (e) { /** */ }
245 | return _r;
246 | },
247 | /**
248 | * Creates a new input of type `hidden`. Does not append to DOM.
249 | *
250 | * @param {string} name Name for the new input
251 | * @param {string} value Value for the new input
252 | * @returns the created input
253 | */
254 | createFormHiddenInput: function (name, value) {
255 | var el = document.createElement("input");
256 | el.setAttribute("type", "hidden");
257 | el.setAttribute("name", name);
258 | el.setAttribute("value", value);
259 | return el;
260 | },
261 |
262 | /**
263 | * Determines a mobile device's orientation for width calculation
264 | *
265 | * @returns true if in landscape
266 | */
267 | checkDevicesOrientation: function () {
268 | if (window.orientation === 90 || window.orientation === -90) {
269 | return true;
270 | } else {
271 | return false;
272 | }
273 | },
274 |
275 | /**
276 | * Creates a semi-transparent overlay with full width/height to serve as
277 | * a background for the lightbox modal
278 | *
279 | * @returns the created overlay
280 | */
281 | createOverlay: function () {
282 | var overlay = document.createElement("div");
283 | overlay.setAttribute("id", "rxp-overlay-" + randomId);
284 | overlay.style.position = "fixed";
285 | overlay.style.width = "100%";
286 | overlay.style.height = "100%";
287 | overlay.style.top = "0";
288 | overlay.style.left = "0";
289 | overlay.style.transition = "all 0.3s ease-in-out";
290 | overlay.style.zIndex = "100";
291 |
292 | if (isMobileIFrame) {
293 | overlay.style.position = "absolute !important";
294 | overlay.style.WebkitOverflowScrolling = "touch";
295 | overlay.style.overflowX = "hidden";
296 | overlay.style.overflowY = "scroll";
297 | }
298 |
299 | document.body.appendChild(overlay);
300 |
301 | setTimeout(function () {
302 | overlay.style.background = "rgba(0, 0, 0, 0.7)";
303 | }, 1);
304 |
305 | return overlay;
306 | },
307 |
308 | /**
309 | * Closes a lightbox modal and all associated elements
310 | *
311 | * @param {HTMLImageElement} closeButton
312 | * @param {HTMLIFrameElement} iFrame
313 | * @param {HTMLImageElement} spinner
314 | * @param {HTMLDivElement} overlayElement
315 | */
316 | closeModal: function (closeButton, iFrame, spinner, overlayElement) {
317 | if (closeButton && closeButton.parentNode) {
318 | closeButton.parentNode.removeChild(closeButton);
319 | }
320 |
321 | if (iFrame && iFrame.parentNode) {
322 | iFrame.parentNode.removeChild(iFrame);
323 | }
324 |
325 | if (spinner && spinner.parentNode) {
326 | spinner.parentNode.removeChild(spinner);
327 | }
328 |
329 | if (!overlayElement) {
330 | return;
331 | }
332 |
333 | overlayElement.className = "";
334 | setTimeout(function () {
335 | if (overlayElement.parentNode) {
336 | overlayElement.parentNode.removeChild(overlayElement);
337 | }
338 | }, 300);
339 | },
340 |
341 | /**
342 | * Creates a close button for the lightbox modal
343 | *
344 | * @returns the created element
345 | */
346 | createCloseButton: function (overlayElement) {
347 | if (document.getElementById("rxp-frame-close-" + randomId) !== null) {
348 | return;
349 | }
350 |
351 | var closeButton = document.createElement("img");
352 | closeButton.setAttribute("id","rxp-frame-close-" + randomId);
353 | closeButton.setAttribute("src", "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAYAAAA7bUf6AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QUJFRjU1MEIzMUQ3MTFFNThGQjNERjg2NEZCRjFDOTUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QUJFRjU1MEMzMUQ3MTFFNThGQjNERjg2NEZCRjFDOTUiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpBQkVGNTUwOTMxRDcxMUU1OEZCM0RGODY0RkJGMUM5NSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpBQkVGNTUwQTMxRDcxMUU1OEZCM0RGODY0RkJGMUM5NSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PlHco5QAAAHpSURBVHjafFRdTsJAEF42JaTKn4glGIg++qgX4AAchHAJkiZcwnAQD8AF4NFHCaC2VgWkIQQsfl/jNJUik8Duzs/XmW9mN7Xb7VRc5vP5zWKxaK5Wq8Zmu72FqobfJG0YQ9M0+/l8/qFQKDzGY1JxENd1288vLy1s786KRZXJZCLber1Wn7MZt4PLarVnWdZ9AmQ8Hncc17UvymVdBMB/MgPQm+cFFcuy6/V6lzqDf57ntWGwYdBIVx0TfkBD6I9M35iRJgfIoAVjBLDZbA4CiJ5+9AdQi/EahibqDTkQx6fRSIHcPwA8Uy9A9Gcc47Xv+w2wzhRDYzqdVihLIbsIiCvP1NNOoX/29FQx3vgOgtt4FyRdCgPRarX4+goB9vkyAMh443cOEsIAAcjncuoI4TXWMAmCIGFhCQLAdZ8jym/cRJ+Y5nC5XCYAhINKpZLgSISZgoqh5iiLQrojAFICVwGS7tCfe5DbZzkP56XS4NVxwvTI/vXVVYIDnqmnnX70ZxzjNS8THHooK5hMpxHQIREA+tEfA9djfHR3MHkdx3Hspe9r3B+VzWaj2RESyR2mlCUE4MoGQDdxiwHURq2t94+PO9bMIYyTyDNLwMoM7g8+BfKeYGniyw2MdfSehF3Qmk1IvCc/AgwAaS86Etp38bUAAAAASUVORK5CYII=");
354 | closeButton.setAttribute("style","transition: all 0.5s ease-in-out; opacity: 0; float: left; position: absolute; left: 50%; margin-left: 173px; z-index: 99999999; top: 30px;");
355 |
356 | setTimeout(function () {
357 | closeButton.style.opacity = "1";
358 | },500);
359 |
360 | if (isMobileIFrame) {
361 | closeButton.style.position = "absolute";
362 | closeButton.style.float = "right";
363 | closeButton.style.top = "20px";
364 | closeButton.style.left = "initial";
365 | closeButton.style.marginLeft = "0px";
366 | closeButton.style.right = "20px";
367 | }
368 |
369 | return closeButton;
370 | },
371 |
372 | /**
373 | * Creates a form and appends the HPP request data as hidden input elements to
374 | * POST to the defined HPP URL.
375 | *
376 | * The created form is not appended to the DOM and is not submitted at this time.
377 | *
378 | * @param {Document} doc
379 | * @param {object} token HPP request data
380 | * @param {bool} ignorePostMessage If true, the HPP will redirect to the defined
381 | * defined redirect URL. If false, the HPP will send a postMessage
382 | * to the parent window to be handled by this library.
383 | * @returns the created form
384 | */
385 | createForm: function (doc, token, ignorePostMessage) {
386 | var form = document.createElement("form");
387 | form.setAttribute("method", "POST");
388 | form.setAttribute("action", hppUrl);
389 |
390 | var versionSet = false;
391 |
392 | for (var key in token) {
393 | if (key === "HPP_VERSION"){
394 | versionSet = true;
395 | }
396 | form.appendChild(internal.createFormHiddenInput(key, token[key]));
397 | }
398 |
399 | if (versionSet === false){
400 | form.appendChild(internal.createFormHiddenInput("HPP_VERSION", "2"));
401 | }
402 |
403 | if (ignorePostMessage) {
404 | form.appendChild(internal.createFormHiddenInput("MERCHANT_RESPONSE_URL", redirectUrl));
405 | } else {
406 | var parser = internal.getUrlParser(window.location.href);
407 | var hppOriginParam = parser.protocol + '//' + parser.host;
408 |
409 | form.appendChild(internal.createFormHiddenInput("HPP_POST_RESPONSE", hppOriginParam));
410 | form.appendChild(internal.createFormHiddenInput("HPP_POST_DIMENSIONS", hppOriginParam));
411 | }
412 | return form;
413 | },
414 |
415 | /**
416 | * Creates a visual spinner element to be shown with the lightbox overlay while the
417 | * HPP's iframe loads
418 | *
419 | * @returns the created spinner element
420 | */
421 | createSpinner: function () {
422 | var spinner = document.createElement("img");
423 | spinner.setAttribute("src", "data:image/gif;base64,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");
424 | spinner.setAttribute("id", "rxp-loader-" + randomId);
425 | spinner.style.left = "50%";
426 | spinner.style.position = "fixed";
427 | spinner.style.background = "#FFFFFF";
428 | spinner.style.borderRadius = "50%";
429 | spinner.style.width = "30px";
430 | spinner.style.zIndex = "200";
431 | spinner.style.marginLeft = "-15px";
432 | spinner.style.top = "120px";
433 | return spinner;
434 | },
435 |
436 | /**
437 | * Creates the HPP's form, spinner, iframe, and close button, appends them
438 | * to the DOM, and submits the form to load the HPP
439 | *
440 | * @param {HTMLDivElement} overlayElement
441 | * @param {object} token The HPP request data
442 | * @returns an object with the created spinner, iframe, and close button
443 | */
444 | createIFrame: function (overlayElement, token) {
445 | //Create the spinner
446 | var spinner = internal.createSpinner();
447 | document.body.appendChild(spinner);
448 |
449 | //Create the iframe
450 | var iFrame = document.createElement("iframe");
451 | iFrame.setAttribute("name", "rxp-frame-" + randomId);
452 | iFrame.setAttribute("id", "rxp-frame-" + randomId);
453 | iFrame.setAttribute("height", "562px");
454 | iFrame.setAttribute("frameBorder", "0");
455 | iFrame.setAttribute("width", "360px");
456 | iFrame.setAttribute("seamless", "seamless");
457 | iFrame.setAttribute("allow", "payment " + internal.getBaseUrl(hppUrl));
458 |
459 | iFrame.style.zIndex = "10001";
460 | iFrame.style.position = "absolute";
461 | iFrame.style.transition = "transform 0.5s ease-in-out";
462 | iFrame.style.transform = "scale(0.7)";
463 | iFrame.style.opacity = "0";
464 |
465 | overlayElement.appendChild(iFrame);
466 |
467 | if (isMobileIFrame) {
468 | iFrame.style.top = "0px";
469 | iFrame.style.bottom = "0px";
470 | iFrame.style.left = "0px";
471 | iFrame.style.marginLeft = "0px;";
472 | iFrame.style.width = "100%";
473 | iFrame.style.height = "100%";
474 | iFrame.style.minHeight = "100%";
475 | iFrame.style.WebkitTransform = "translate3d(0,0,0)";
476 | iFrame.style.transform = "translate3d(0, 0, 0)";
477 |
478 | var metaTag = document.createElement('meta');
479 | metaTag.name = "viewport";
480 | metaTag.content = "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0";
481 | document.getElementsByTagName('head')[0].appendChild(metaTag);
482 | } else {
483 | iFrame.style.top = "40px";
484 | iFrame.style.left = "50%";
485 | iFrame.style.marginLeft = "-180px";
486 | }
487 |
488 | logEvent(eventMessages.iFrame.create, { iFrame: iFrame });
489 |
490 | var closeButton;
491 |
492 | iFrame.onload = function () {
493 | iFrame.style.opacity = "1";
494 | iFrame.style.transform = "scale(1)";
495 | iFrame.style.backgroundColor = "#ffffff";
496 |
497 | if (spinner && spinner.parentNode) {
498 | spinner.parentNode.removeChild(spinner);
499 | }
500 |
501 | closeButton = internal.createCloseButton();
502 |
503 | if (overlayElement && closeButton) {
504 | overlayElement.appendChild(closeButton);
505 | closeButton.addEventListener("click", function () {
506 | internal.closeModal(closeButton, iFrame, spinner, overlayElement);
507 | }, true);
508 | }
509 | };
510 |
511 | var form = internal.createForm(document, token);
512 | logEvent(eventMessages.form.create, { form: form });
513 | if (iFrame.contentWindow.document.body) {
514 | iFrame.contentWindow.document.body.appendChild(form);
515 | logEvent(eventMessages.form.append);
516 | } else {
517 | iFrame.contentWindow.document.appendChild(form);
518 | logEvent(eventMessages.form.append);
519 | }
520 |
521 | form.submit();
522 | logEvent(eventMessages.form.submit);
523 |
524 | return {
525 | spinner: spinner,
526 | iFrame: iFrame,
527 | closeButton: closeButton
528 | };
529 | },
530 |
531 | /**
532 | * Opens the HPP in a new window
533 | *
534 | * Used in some mobile scenarios or when the browser viewport is
535 | * smaller than the HPP's inner width.
536 | *
537 | * Will automatically post the request data to the defined HPP
538 | * URL to load the HPP.
539 | *
540 | * @param {object} token The HPP request data
541 | * @returns the created window
542 | */
543 | openWindow: function (token) {
544 | //open new window
545 | var tabWindow = window.open();
546 |
547 | // browsers can prevent a new window from being created
548 | // e.g. mobile Safari
549 | if (!tabWindow) {
550 | return null;
551 | }
552 |
553 | var doc = tabWindow.document;
554 |
555 | //add meta tag to new window (needed for iOS 8 bug)
556 | var meta = doc.createElement("meta");
557 | var name = doc.createAttribute("name");
558 | name.value = "viewport";
559 | meta.setAttributeNode(name);
560 | var content = doc.createAttribute("content");
561 | content.value = "width=device-width";
562 | meta.setAttributeNode(content);
563 | doc.head.appendChild(meta);
564 |
565 | //create form, append to new window and submit
566 | var form = internal.createForm(doc, token);
567 | doc.body.appendChild(form);
568 | form.submit();
569 |
570 | return tabWindow;
571 | },
572 |
573 | /**
574 | * Creates a rudimentary URL parser using an anchor element
575 | *
576 | * @param {string} url
577 | * @returns the created anchor element
578 | */
579 | getUrlParser: function (url) {
580 | var parser = document.createElement('a');
581 | parser.href = url;
582 | return parser;
583 | },
584 |
585 | /**
586 | * Gets the hostname/origin from a URL. Used for origin checks
587 | *
588 | * @param {string} url
589 | * @returns the hostname/origin of the URL
590 | */
591 | getHostnameFromUrl: function (url) {
592 | return internal.getUrlParser(url).hostname;
593 | },
594 |
595 | /**
596 | * Gets the base URL from a URL. Used to set the 'allow payment' attribute.
597 | *
598 | * @param {string} url
599 | * @returns the base URL of the provided URL
600 | */
601 | getBaseUrl: function (url) {
602 | var urlParser = internal.getUrlParser(url);
603 | return urlParser.protocol + '//' + urlParser.hostname;
604 | },
605 |
606 | /**
607 | * Checks if the origin is HPP.
608 | *
609 | * @param {string} origin
610 | * @returns {boolean}
611 | */
612 | isHppOrigin: function(origin) {
613 | var result = false;
614 | allowedHppUrls.forEach(function (url) {
615 | if (internal.getHostnameFromUrl(url) === origin) {
616 | result = true;
617 | }
618 | });
619 |
620 | return result;
621 | },
622 |
623 | /**
624 | * Compares the origins from both arguments to validate we have received a postMessage
625 | * from the expected source
626 | *
627 | * @param {string} origin The origin attached to the recieved message
628 | * @param {string} hppUrl Our expected source origin
629 | * @returns true if the origins match
630 | */
631 | isMessageFromHpp: function (origin, hppUrl) {
632 | var originHostName = internal.getHostnameFromUrl(origin);
633 | return originHostName === internal.getHostnameFromUrl(hppUrl) || internal.isHppOrigin(originHostName);
634 | },
635 |
636 | /**
637 | * Handles messages from the HPP
638 | *
639 | * Messages from the HPP are one of:
640 | *
641 | * - iframe resize event
642 | * - transaction response
643 | * - error information
644 | *
645 | * @param {MessageEvent} e
646 | */
647 | receiveMessage: function (e) {
648 | //Check the origin of the response comes from HPP
649 | if (!internal.isMessageFromHpp(e.event.origin, hppUrl)) {
650 | return;
651 | }
652 |
653 | if (!e.event.data) {
654 | return;
655 | }
656 |
657 | var evtdata = internal.decodeAnswer(e.event.data);
658 |
659 | // we received an invalid message from the HPP iframe (e.g. from a browser plugin)
660 | // return early to prevent invalid processing
661 | if (evtdata === null) {
662 | return;
663 | }
664 |
665 | // check for iframe resize values
666 | if (evtdata.iframe) {
667 | if (!isMobileNewTab()) {
668 | var iframeWidth = evtdata.iframe.width;
669 | var iframeHeight = evtdata.iframe.height;
670 |
671 | var iFrame;
672 | var resized = false;
673 |
674 | if (e.embedded) {
675 | iFrame = e.instance.getIframe();
676 | } else {
677 | iFrame = document.getElementById("rxp-frame-" + randomId);
678 | }
679 | if (e.instance.events && e.instance.events.onResize) {
680 | e.instance.events.onResize(evtdata.iframe);
681 | }
682 |
683 | if (iframeWidth === "390px" && iframeHeight === "440px") {
684 | iFrame.setAttribute("width", iframeWidth);
685 | iFrame.setAttribute("height", iframeHeight);
686 | resized = true;
687 | }
688 |
689 | iFrame.style.backgroundColor="#ffffff";
690 |
691 | if (isMobileIFrame) {
692 | iFrame.style.marginLeft = "0px";
693 | iFrame.style.WebkitOverflowScrolling = "touch";
694 | iFrame.style.overflowX = "scroll";
695 | iFrame.style.overflowY = "scroll";
696 |
697 | if (!e.embedded) {
698 | var overlay = document.getElementById("rxp-overlay-" + randomId);
699 | overlay.style.overflowX = "scroll";
700 | overlay.style.overflowY = "scroll";
701 | }
702 | } else if (!e.embedded && resized) {
703 | iFrame.style.marginLeft = (parseInt(iframeWidth.replace("px", ""), 10) / 2 * -1) + "px";
704 | }
705 |
706 | if (!e.embedded && resized) {
707 | // wrap the below in a setTimeout to prevent a timing issue on a
708 | // cache-miss load
709 | setTimeout(function () {
710 | var closeButton = document.getElementById("rxp-frame-close-" + randomId);
711 | closeButton.style.marginLeft = ((parseInt(iframeWidth.replace("px", ""), 10) / 2) -7) + "px";
712 | }, 200);
713 | }
714 | }
715 | } else {
716 | var _close=function(){
717 | if (isMobileNewTab() && tabWindow) {
718 | //Close the new window
719 | tabWindow.close();
720 | } else {
721 | //Close the lightbox
722 | e.instance.close();
723 | }
724 | var overlay=document.getElementById("rxp-overlay-" + randomId);
725 | if(overlay) {
726 | overlay.remove();
727 | }
728 |
729 | };
730 | var response = e.event.data;
731 | //allow the script to intercept the answer, instead of redirecting to another page. (which is really a 90s thing)
732 | if (typeof e.url === 'function'){
733 | e.url(evtdata, _close);
734 | return;
735 | }
736 | _close();
737 | //Create a form and submit the hpp response to the merchant's response url
738 | var form = document.createElement("form");
739 | form.setAttribute("method", "POST");
740 | form.setAttribute("action", e.url);
741 | form.appendChild(internal.createFormHiddenInput("hppResponse", response));
742 | document.body.appendChild(form);
743 | form.submit();
744 | }
745 | }
746 | };
747 |
748 | /**
749 | * Public interface for the lightbox display mode
750 | */
751 | var RxpLightbox = (function () {
752 | var instance;
753 |
754 | function init() {
755 | var overlayElement;
756 | var spinner;
757 | var iFrame;
758 | var closeButton;
759 | var token;
760 | var isLandscape = internal.checkDevicesOrientation();
761 |
762 | if (isMobileIFrame) {
763 | if (window.addEventListener) {
764 | window.addEventListener("orientationchange", function () {
765 | isLandscape = internal.checkDevicesOrientation();
766 | }, false);
767 | }
768 | }
769 |
770 | return {
771 | lightbox: function () {
772 | if (isMobileNewTab()) {
773 | tabWindow = internal.openWindow(token);
774 | } else {
775 | overlayElement = internal.createOverlay();
776 | var temp = internal.createIFrame(overlayElement, token);
777 | spinner = temp.spinner;
778 | iFrame = temp.iFrame;
779 | closeButton = temp.closeButton;
780 | }
781 | },
782 | close: function () {
783 | internal.closeModal();
784 | },
785 | setToken: function (hppToken) {
786 | token = hppToken;
787 | }
788 | };
789 | }
790 |
791 | return {
792 | // Get the Singleton instance if one exists
793 | // or create one if it doesn't
794 | getInstance: function (hppToken) {
795 | if (!instance) {
796 | instance = init();
797 | }
798 |
799 | //Set the hpp token
800 | instance.setToken(hppToken);
801 |
802 | return instance;
803 | },
804 | init: function (idOfLightboxButton, merchantUrl, serverSdkJson) {
805 | logEvent(eventMessages.initialize('RxpLightbox'));
806 | //Get the lightbox instance (it's a singleton) and set the sdk json
807 | var lightboxInstance = RxpLightbox.getInstance(serverSdkJson);
808 |
809 | //if you want the form to load on function call, set to autoload
810 | if (idOfLightboxButton === 'autoload') {
811 | lightboxInstance.lightbox();
812 | }
813 | // Sets the event listener on the PAY button. The click will invoke the lightbox method
814 | else if (document.getElementById(idOfLightboxButton).addEventListener) {
815 | document.getElementById(idOfLightboxButton).addEventListener("click", lightboxInstance.lightbox, true);
816 | } else {
817 | document.getElementById(idOfLightboxButton).attachEvent('onclick', lightboxInstance.lightbox);
818 | }
819 | //avoid multiple message event listener binded to the window object.
820 | internal.removeOldEvtMsgListener();
821 | var evtMsgFct = function (event) {
822 | return internal.receiveMessage({ event: event, instance: lightboxInstance, url: merchantUrl, embedded: false });
823 | };
824 | internal.evtMsg.push({ fct: evtMsgFct, opt: false });
825 | internal.addEvtMsgListener(evtMsgFct);
826 | }
827 | };
828 | })();
829 |
830 | /**
831 | * Public interface for the embedded display mode
832 | */
833 | var RxpEmbedded = (function () {
834 | var instance;
835 |
836 | function init() {
837 | var overlayElement;
838 | var spinner;
839 | var iFrame;
840 | var closeButton;
841 | var token;
842 |
843 | return {
844 | embedded: function () {
845 | var form = internal.createForm(document, token);
846 | logEvent(eventMessages.form.create, { form: form });
847 | if (iFrame) {
848 | logEvent(eventMessages.iFrame.find, { iFrame: iFrame });
849 | if (iFrame.contentWindow.document.body) {
850 | iFrame.contentWindow.document.body.appendChild(form);
851 | logEvent(eventMessages.form.append);
852 | } else {
853 | iFrame.contentWindow.document.appendChild(form);
854 | logEvent(eventMessages.form.append);
855 | }
856 | form.submit();
857 | logEvent(eventMessages.form.submit);
858 | iFrame.style.display = "inherit";
859 | }
860 | },
861 | close: function () {
862 | iFrame.style.display = "none";
863 | },
864 | setToken: function (hppToken) {
865 | token = hppToken;
866 | },
867 | setIframe: function (iframeId) {
868 | iFrame = document.getElementById(iframeId);
869 | if (iFrame) {
870 | iFrame.setAttribute("allow", "payment " + internal.getBaseUrl(hppUrl));
871 | }
872 | },
873 | getIframe: function () {
874 | return iFrame;
875 | }
876 | };
877 | }
878 |
879 | return {
880 | // Get the Singleton instance if one exists
881 | // or create one if it doesn't
882 | getInstance: function (hppToken) {
883 | if (!instance) {
884 | instance = init();
885 | }
886 |
887 | //Set the hpp token
888 | instance.setToken(hppToken);
889 | return instance;
890 | },
891 | init: function (idOfEmbeddedButton, idOfTargetIframe, merchantUrl, serverSdkJson,events) {
892 | logEvent(eventMessages.initialize('RxpEmbedded'));
893 |
894 | //Get the embedded instance (it's a singleton) and set the sdk json
895 | var embeddedInstance = RxpEmbedded.getInstance(serverSdkJson);
896 | embeddedInstance.events=events;
897 |
898 | embeddedInstance.setIframe(idOfTargetIframe);
899 | //if you want the form to load on function call, set to autoload
900 | if (idOfEmbeddedButton === 'autoload') {
901 | embeddedInstance.embedded();
902 | }
903 | // Sets the event listener on the PAY button. The click will invoke the embedded method
904 | else if (document.getElementById(idOfEmbeddedButton).addEventListener) {
905 | document.getElementById(idOfEmbeddedButton).addEventListener("click", embeddedInstance.embedded, true);
906 | } else {
907 | document.getElementById(idOfEmbeddedButton).attachEvent('onclick', embeddedInstance.embedded);
908 | }
909 |
910 | //avoid multiple message event listener binded to the window object.
911 | internal.removeOldEvtMsgListener();
912 | var evtMsgFct = function (event) {
913 | return internal.receiveMessage({ event: event, instance: embeddedInstance, url: merchantUrl, embedded: true });
914 | };
915 | internal.evtMsg.push({ fct: evtMsgFct, opt: false });
916 | internal.addEvtMsgListener(evtMsgFct);
917 | }
918 | };
919 | })();
920 |
921 | /**
922 | * Public interface for the redirect display mode
923 | */
924 | var RxpRedirect = (function () {
925 | var instance;
926 |
927 | function init() {
928 | var overlayElement;
929 | var spinner;
930 | var iFrame;
931 | var closeButton;
932 | var token;
933 | var isLandscape = internal.checkDevicesOrientation();
934 |
935 | if (isMobileIFrame) {
936 | if (window.addEventListener) {
937 | window.addEventListener("orientationchange", function () {
938 | isLandscape = internal.checkDevicesOrientation();
939 | }, false);
940 | }
941 | }
942 |
943 | return {
944 | redirect: function () {
945 | var form = internal.createForm(document, token, true);
946 | logEvent(eventMessages.form.create, { form: form });
947 | document.body.append(form);
948 | form.submit();
949 | logEvent(eventMessages.form.submit);
950 | },
951 | setToken: function (hppToken) {
952 | token = hppToken;
953 | }
954 | };
955 | }
956 | return {
957 | // Get the singleton instance if one exists
958 | // or create one if it doesn't
959 | getInstance: function (hppToken) {
960 | if (!instance) {
961 | instance = init();
962 | }
963 |
964 | // Set the hpp token
965 | instance.setToken(hppToken);
966 |
967 | return instance;
968 | },
969 | init: function (idOfButton, merchantUrl, serverSdkJson) {
970 | logEvent(eventMessages.initialize('RxpRedirect'));
971 |
972 | // Get the redirect instance (it's a singleton) and set the sdk json
973 | var redirectInstance = RxpRedirect.getInstance(serverSdkJson);
974 | redirectUrl = merchantUrl;
975 |
976 | // Sets the event listener on the PAY button. The click will invoke the redirect method
977 | if (document.getElementById(idOfButton).addEventListener) {
978 | document.getElementById(idOfButton).addEventListener("click", redirectInstance.redirect, true);
979 | } else {
980 | document.getElementById(idOfButton).attachEvent('onclick', redirectInstance.redirect);
981 | }
982 |
983 | //avoid multiple message event listener binded to the window object.
984 | internal.removeOldEvtMsgListener();
985 | var evtMsgFct = function (event) {
986 | return internal.receiveMessage({ event: event, instance: redirectInstance, url: merchantUrl, embedded: false });
987 | };
988 | internal.evtMsg.push({ fct: evtMsgFct, opt: false });
989 | internal.addEvtMsgListener(evtMsgFct);
990 | }
991 | };
992 | }());
993 |
994 | /**
995 | * Public interface for the Realex HPP library
996 | */
997 | return {
998 | init: RxpLightbox.init,
999 | lightbox: {
1000 | init: RxpLightbox.init
1001 | },
1002 | embedded: {
1003 | init: RxpEmbedded.init
1004 | },
1005 | redirect: {
1006 | init: RxpRedirect.init
1007 | },
1008 | setHppUrl: setHppUrl,
1009 | setMobileXSLowerBound: setMobileXSLowerBound,
1010 | setConfigItem: setConfigItem,
1011 | constants: constants,
1012 | _internal: internal
1013 | };
1014 |
1015 | }());
--------------------------------------------------------------------------------
/lib/rxp-remote.js:
--------------------------------------------------------------------------------
1 | /*
2 | * rxp-remote.js
3 | * https://github.com/realexpayments/rxp-js
4 | *
5 | * Licensed under the MIT license.
6 | */
7 | var RealexRemote = (function() {
8 |
9 | 'use strict';
10 |
11 | /*
12 | * Validate Card Number. Returns true if card number valid. Only allows
13 | * non-empty numeric values between 12 and 19 characters. A Luhn check is
14 | * also run against the card number.
15 | */
16 | var validateCardNumber = function(cardNumber) {
17 | // test numeric and length between 12 and 19
18 | if (!/^\d{12,19}$/.test(cardNumber)) {
19 | return false;
20 | }
21 |
22 | // luhn check
23 | var sum = 0;
24 | var digit = 0;
25 | var addend = 0;
26 | var timesTwo = false;
27 |
28 | for (var i = cardNumber.length - 1; i >= 0; i--) {
29 | digit = parseInt(cardNumber.substring(i, i + 1), 10);
30 | if (timesTwo) {
31 | addend = digit * 2;
32 | if (addend > 9) {
33 | addend -= 9;
34 | }
35 | } else {
36 | addend = digit;
37 | }
38 | sum += addend;
39 | timesTwo = !timesTwo;
40 | }
41 |
42 | var modulus = sum % 10;
43 | if (modulus !== 0) {
44 | return false;
45 | }
46 |
47 | return true;
48 | };
49 |
50 | /*
51 | * Validate Card Holder Name. Returns true if card holder valid. Only allows
52 | * non-empty ISO/IEC 8859-1 values 100 characters or less.
53 | */
54 | var validateCardHolderName = function(cardHolderName) {
55 | // test for undefined
56 | if (!cardHolderName) {
57 | return false;
58 | }
59 |
60 | // test white space only
61 | if (!cardHolderName.trim()) {
62 | return false;
63 | }
64 |
65 | // test ISO/IEC 8859-1 characters between 1 and 100
66 | if (!/^[\u0020-\u007E\u00A0-\u00FF]{1,100}$/.test(cardHolderName)) {
67 | return false;
68 | }
69 |
70 | return true;
71 | };
72 |
73 | /*
74 | * Validate CVN. Applies to non-Amex card types. Only allows 3 numeric
75 | * characters.
76 | */
77 | var validateCvn = function(cvn) {
78 | // test numeric length 3
79 | if (!/^\d{3}$/.test(cvn)) {
80 | return false;
81 | }
82 |
83 | return true;
84 | };
85 |
86 | /*
87 | * Validate Amex CVN. Applies to Amex card types. Only allows 4 numeric
88 | * characters.
89 | */
90 | var validateAmexCvn = function(cvn) {
91 | // test numeric length 4
92 | if (!/^\d{4}$/.test(cvn)) {
93 | return false;
94 | }
95 |
96 | return true;
97 | };
98 |
99 | /*
100 | * Validate Expiry Date Format. Only allows 4 numeric characters. Month must
101 | * be between 1 and 12.
102 | */
103 | var validateExpiryDateFormat = function(expiryDate) {
104 |
105 | // test numeric of length 4
106 | if (!/^\d{4}$/.test(expiryDate)) {
107 | return false;
108 | }
109 |
110 | var month = parseInt(expiryDate.substring(0, 2), 10);
111 | var year = parseInt(expiryDate.substring(2, 4), 10);
112 |
113 | // test month range is 1-12
114 | if (month < 1 || month > 12) {
115 | return false;
116 | }
117 |
118 | return true;
119 | };
120 |
121 | /*
122 | * Validate Expiry Date Not In Past. Also runs checks from
123 | * validateExpiryDateFormat.
124 | */
125 | var validateExpiryDateNotInPast = function(expiryDate) {
126 | // test valid format
127 | if (!validateExpiryDateFormat(expiryDate)) {
128 | return false;
129 | }
130 |
131 | var month = parseInt(expiryDate.substring(0, 2), 10);
132 | var year = parseInt(expiryDate.substring(2, 4), 10);
133 |
134 | // test date is not in the past
135 | var now = new Date();
136 | var currentMonth = now.getMonth() + 1;
137 | var currentYear = now.getFullYear();
138 | if (year < (currentYear % 100)) {
139 | return false;
140 | } else if (year === (currentYear % 100) && month < currentMonth) {
141 | return false;
142 | }
143 |
144 | return true;
145 | };
146 |
147 | return {
148 | validateCardNumber : validateCardNumber,
149 | validateCardHolderName : validateCardHolderName,
150 | validateCvn : validateCvn,
151 | validateAmexCvn : validateAmexCvn,
152 | validateExpiryDateFormat : validateExpiryDateFormat,
153 | validateExpiryDateNotInPast : validateExpiryDateNotInPast
154 | };
155 | }());
156 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rxp-js",
3 | "description": "The official Realex Payments JS Library",
4 | "version": "1.5.5",
5 | "homepage": "https://github.com/realexpayments/rxp-js",
6 | "author": {
7 | "name": "Realex Developer",
8 | "url": "http://www.realexpayments.com"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/realexpayments/rxp-js"
13 | },
14 | "licenses": [
15 | {
16 | "type": "MIT",
17 | "url": "https://github.com/realexpayments/rxp-js/blob/master/LICENSE-MIT"
18 | }
19 | ],
20 | "main": "lib/rxp-js",
21 | "engines": {
22 | "node": ">= 0.10.0"
23 | },
24 | "devDependencies": {
25 | "chromedriver": "^2.31.0",
26 | "geckodriver": "^1.8.0",
27 | "grunt": "^1.0.1",
28 | "grunt-contrib-concat": "^1.0.1",
29 | "grunt-contrib-jasmine": "^1.1.0",
30 | "grunt-contrib-jshint": "^1.1.0",
31 | "grunt-contrib-uglify": "^3.0.1",
32 | "grunt-contrib-watch": "^1.0.0",
33 | "grunt-php": "^1.5.1",
34 | "intern": "^3.4.6"
35 | },
36 | "keywords": [],
37 | "scripts": {
38 | "grunt": "grunt"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/specs/functional/hpp/embedded-positives_spec.js:
--------------------------------------------------------------------------------
1 | define(function (require) {
2 | var bdd = require('intern!bdd');
3 | var assert = require('intern/chai!assert');
4 | var successHelper = require('intern/dojo/node!../../helpers/hpp').iframeSuccessHelper;
5 |
6 | bdd.describe('RealexRemote - HPP Embedded Positive Tests', function () {
7 | bdd.it('should process a payment successfully',
8 | successHelper(
9 | // url
10 | require.toUrl('http://localhost:8989/examples/hpp/process-a-payment-embedded.html'),
11 | // iframe selector
12 | '#targetIframe',
13 | // fields to enter
14 | [
15 | { name: 'pas_ccnum', type: 'text', value: '4111111111111111' },
16 | { name: 'pas_expiry', type: 'text', value: '1225' },
17 | { name: 'pas_cccvc', type: 'text', value: '012' },
18 | { name: 'pas_ccname', type: 'text', value: 'Jane Doe' },
19 | ],
20 | // callback to assert against result
21 | function (command) {
22 | return command
23 | .execute(() => document.body.innerText)
24 | .then(function (text) {
25 | // make our assertions on the HPP response
26 | var json = JSON.parse(text);
27 | json = JSON.parse(json.response);
28 | assert.isOk(json.AUTHCODE);
29 | })
30 | .end();
31 | }
32 | ).bind(this)
33 | );
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/specs/functional/hpp/lightbox-positives_spec.js:
--------------------------------------------------------------------------------
1 | define(function (require) {
2 | var bdd = require('intern!bdd');
3 | var assert = require('intern/chai!assert');
4 | var successHelper = require('intern/dojo/node!../../helpers/hpp').iframeSuccessHelper;
5 |
6 | bdd.describe('RealexRemote - HPP Lightbox Positive Tests', function () {
7 | bdd.it('should process a payment successfully',
8 | successHelper(
9 | // url
10 | require.toUrl('http://localhost:8989/examples/hpp/process-a-payment-lightbox.html'),
11 | // iframe selector
12 | '[id^="rxp-frame-"]',
13 | // fields to enter
14 | [
15 | { name: 'pas_ccnum', type: 'text', value: '4111111111111111' },
16 | { name: 'pas_expiry', type: 'text', value: '1225' },
17 | { name: 'pas_cccvc', type: 'text', value: '012' },
18 | { name: 'pas_ccname', type: 'text', value: 'Jane Doe' },
19 | ],
20 | // callback to assert against result
21 | function (command) {
22 | return command
23 | .execute(() => document.body.innerText)
24 | .then(function (text) {
25 | // make our assertions on the HPP response
26 | var json = JSON.parse(text);
27 | json = JSON.parse(json.response);
28 | assert.isOk(json.AUTHCODE);
29 | })
30 | .end();
31 | }
32 | ).bind(this)
33 | );
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/specs/functional/hpp/redirect-positives_spec.js:
--------------------------------------------------------------------------------
1 | define(function (require) {
2 | var bdd = require('intern!bdd');
3 | var assert = require('intern/chai!assert');
4 | var successHelper = require('intern/dojo/node!../../helpers/hpp').redirectSuccessHelper;
5 |
6 | bdd.describe('RealexRemote - HPP Redirect Positive Tests', function () {
7 | bdd.it('should process a payment successfully',
8 | successHelper(
9 | // url
10 | require.toUrl('http://localhost:8989/examples/hpp/redirect-for-payment.html'),
11 | // fields to enter
12 | [
13 | { name: 'pas_ccnum', type: 'text', value: '4111111111111111' },
14 | { name: 'pas_expiry', type: 'text', value: '1225' },
15 | { name: 'pas_cccvc', type: 'text', value: '012' },
16 | { name: 'pas_ccname', type: 'text', value: 'Jane Doe' },
17 | ],
18 | // callback to assert against result
19 | function (command) {
20 | return command
21 | .execute(() => document.body.innerText)
22 | .then(function (text) {
23 | // make our assertions on the HPP response
24 | assert.isOk(text.indexOf('Your transaction has been successful') !== -1);
25 | })
26 | .end();
27 | }
28 | ).bind(this)
29 | );
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/specs/helpers/hpp.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Sets the current command/session to the desired URL.
3 | *
4 | * Require's the desired URL to add a `loaded` class to the
5 | * body on load complete.
6 | *
7 | * @param {leadfoot/Command} command
8 | * @param {string} url
9 | */
10 | function loadUrlAndWait(command, url) {
11 | return command
12 | // navigate to our test page
13 | .get(url)
14 | .setFindTimeout(5000)
15 | // wait until the HPP request producer finishes
16 | .findByCssSelector('body.loaded');
17 | }
18 |
19 | /**
20 | * Sets a page's field base on the defined field type.
21 | *
22 | * @param {leadfoot/Command} command
23 | * @param {object} field
24 | */
25 | function setField(command, field) {
26 | switch (field.type) {
27 | case 'text':
28 | default:
29 | return command
30 | .findById(field.name)
31 | .click()
32 | .type(field.value)
33 | .end();
34 | }
35 | }
36 |
37 | /**
38 | * Set's the given fields. Will call `callback`
39 | * with the command/session to complete any assertions.
40 | *
41 | * @param {leadfoot/Command} command
42 | * @param {object[]} fields
43 | * @param {Function} callback
44 | */
45 | function setFieldsAndSubmit(command, fields, callback) {
46 | // start - enter form data
47 | for (var i = 0; i < fields.length; i++) {
48 | command = setField(command, fields[i]);
49 | }
50 | // end - enter form data
51 |
52 | command = command
53 | // submit HPP
54 | .findById('rxp-primary-btn')
55 | .click()
56 | .end()
57 | // wait for redirect to HPP response consumer
58 | // TODO: figure out a way to do this without `sleep`
59 | .sleep(1500);
60 | return callback(command);
61 | }
62 |
63 | /**
64 | * Focuses a frame and set's the given fields. Will call `callback`
65 | * with the command/session to complete any assertions.
66 | *
67 | * @param {leadfoot/Command} command
68 | * @param {object[]} fields
69 | * @param {Function} callback
70 | */
71 | function setFrameFieldsAndSubmit(command, fields, callback) {
72 | return function (iframe) {
73 | // focus to the iframe
74 | command = command.switchToFrame(iframe);
75 |
76 | // start - enter form data
77 | for (var i = 0; i < fields.length; i++) {
78 | command = setField(command, fields[i]);
79 | }
80 | // end - enter form data
81 |
82 | command = command
83 | // submit HPP
84 | .findById('rxp-primary-btn')
85 | .click()
86 | .end()
87 | // wait for redirect to HPP response consumer
88 | // TODO: figure out a way to do this without `sleep`
89 | .sleep(1500)
90 | // ensure we're targeting the parent and not a non-existing iframe
91 | .switchToParentFrame();
92 | return callback(command);
93 | };
94 | }
95 |
96 | /**
97 | * Completes an HPP lightbox with the given fields. Will call `callback`
98 | * with the command/session to complete any assertions.
99 | *
100 | * @param {string} url
101 | * @param {object[]} fields
102 | * @param {Function} callback
103 | */
104 | function iframeSuccessHelper(url, iframeSelector, fields, callback) {
105 | return function () {
106 | var command = this.remote;
107 | return loadUrlAndWait(command, url)
108 | // start HPP
109 | .findById('payButtonId')
110 | .click()
111 | .end()
112 | // find the first iframe with an id that starts with our identifier
113 | .findByCssSelector(iframeSelector)
114 | .then(setFrameFieldsAndSubmit(command, fields, callback))
115 | .end();
116 | };
117 | }
118 |
119 | /**
120 | * Completes an HPP redirect with the given fields. Will call `callback`
121 | * with the command/session to complete any assertions.
122 | *
123 | * @param {string} url
124 | * @param {object[]} fields
125 | * @param {Function} callback
126 | */
127 | function redirectSuccessHelper(url, fields, callback) {
128 | return function () {
129 | var command = this.remote;
130 | return loadUrlAndWait(command, url)
131 | // start HPP
132 | .findById('payButtonId')
133 | .click()
134 | .end()
135 | .then(() => setFieldsAndSubmit(command, fields, callback));
136 | };
137 | }
138 |
139 | module.exports = {
140 | iframeSuccessHelper: iframeSuccessHelper,
141 | redirectSuccessHelper: redirectSuccessHelper,
142 | };
143 |
--------------------------------------------------------------------------------
/specs/intern.config.js:
--------------------------------------------------------------------------------
1 | // Learn more about configuring this file at .
2 | // These default settings work OK for most people. The options that *must* be changed below are the
3 | // packages, suites, excludeInstrumentation, and (if you want functional tests) functionalSuites.
4 | define({
5 | // Browsers to run integration testing against. Options that will be permutated are browserName, version, platform,
6 | // and platformVersion; any other capabilities options specified for an environment will be copied as-is. Note that
7 | // browser and platform names, and version number formats, may differ between cloud testing systems.
8 | environments: [
9 | {
10 | browserName: 'chrome',
11 | // Run headless when possible
12 | // Comment below line when using `leaveRemoteOpen: true`
13 | chromeOptions: { args: ['headless', 'disable-gpu'], }, fixSessionCapabilities: false,
14 | },
15 | ],
16 |
17 | // Uncomment to keep browser automation session open to inspect results of tests
18 | // leaveRemoteOpen: true,
19 |
20 | // Name of the tunnel class to use for WebDriver tests.
21 | // See for built-in options
22 | tunnel: 'SeleniumTunnel',
23 |
24 | // Unit test suite(s) to run in each browser
25 | functionalSuites: [ 'specs/functional/**/*_spec.js' ],
26 |
27 | // A regular expression matching URLs to files that should not be included in code coverage analysis. Set to `true`
28 | // to completely disable code coverage.
29 | excludeInstrumentation: true
30 | });
31 |
--------------------------------------------------------------------------------
/specs/unit/rxp-hpp_spec.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Unit tests for rxp-hpp.js
3 | */
4 | describe('rxp-hpp library', function () {
5 | /*
6 | * Unit tests for createFormHiddenInput
7 | */
8 | describe('form input creation (createFormHiddenInput)', function () {
9 | it('creates element', function () {
10 | var field = RealexHpp._internal.createFormHiddenInput("name", "value");
11 | expect(field).not.toBe(null);
12 | expect(field.name).toBe("name");
13 | expect(field.value).toBe("value");
14 | });
15 |
16 | it('empty name', function () {
17 | var field = RealexHpp._internal.createFormHiddenInput("", "value");
18 | expect(field).not.toBe(null);
19 | expect(field.name).toBe("");
20 | expect(field.value).toBe("value");
21 | });
22 |
23 | it('empty value', function () {
24 | var field = RealexHpp._internal.createFormHiddenInput("name", "");
25 | expect(field).not.toBe(null);
26 | expect(field.name).toBe("name");
27 | expect(field.value).toBe("");
28 | });
29 |
30 | it('empty name and value', function () {
31 | var field = RealexHpp._internal.createFormHiddenInput("", "");
32 | expect(field).not.toBe(null);
33 | expect(field.name).toBe("");
34 | expect(field.value).toBe("");
35 | });
36 | });
37 |
38 | /*
39 | * Unit tests for checkDevicesOrientation
40 | */
41 | describe('device orientation (checkDevicesOrientation)', function () {
42 | it('0', function () {
43 | window.orientation = 0;
44 | var orientation = RealexHpp._internal.checkDevicesOrientation();
45 | expect(orientation).toBe(false);
46 | });
47 |
48 | it('90', function () {
49 | window.orientation = 90;
50 | var orientation = RealexHpp._internal.checkDevicesOrientation();
51 | expect(orientation).toBe(true);
52 | });
53 |
54 | it('180', function () {
55 | window.orientation = 180;
56 | var orientation = RealexHpp._internal.checkDevicesOrientation();
57 | expect(orientation).toBe(false);
58 | });
59 |
60 | it('-90', function () {
61 | window.orientation = -90;
62 | var orientation = RealexHpp._internal.checkDevicesOrientation();
63 | expect(orientation).toBe(true);
64 | });
65 | });
66 |
67 | /*
68 | * Unit tests for createOverlay
69 | */
70 | describe('lightbox overlay (createOverlay)', function () {
71 | it('creates an overlay', function () {
72 | var overlay = RealexHpp._internal.createOverlay();
73 | expect(overlay).not.toBe(null);
74 | expect(overlay.getAttribute('id')).toMatch(/rxp\-overlay\-/);
75 |
76 | var injectedOverlay = document.getElementById(overlay.getAttribute('id'));
77 | expect(overlay).toBe(injectedOverlay);
78 | });
79 | });
80 |
81 | /*
82 | * Unit tests for createCloseButton
83 | */
84 | describe('lightbox overlay close button (createCloseButton)', function () {
85 | it('creates an overlay', function () {
86 | var overlay = RealexHpp._internal.createOverlay();
87 | expect(overlay).not.toBe(null);
88 |
89 | var close = RealexHpp._internal.createCloseButton(overlay);
90 | expect(close).not.toBe(null);
91 | expect(close.getAttribute('id')).toMatch(/rxp\-frame\-close\-/);
92 | });
93 | });
94 |
95 | /*
96 | * Unit tests for createForm
97 | */
98 | describe('request form(createForm)', function () {
99 | it('creates form with no extra data', function () {
100 | var form = RealexHpp._internal.createForm(document, {});
101 | expect(form).not.toBe(null);
102 | expect(form.children.length).toBe(3);
103 | });
104 |
105 | it('creates redirect form with no extra data', function () {
106 | var form = RealexHpp._internal.createForm(document, {}, true);
107 | expect(form).not.toBe(null);
108 | expect(form.children.length).toBe(2);
109 | });
110 |
111 | it('creates form with extra data', function () {
112 | var form = RealexHpp._internal.createForm(document, {NAME: 'value'});
113 | expect(form).not.toBe(null);
114 | expect(form.children.length).toBe(4);
115 | });
116 |
117 | it('creates redirect form with extra data', function () {
118 | var form = RealexHpp._internal.createForm(document, {NAME: 'value'}, true);
119 | expect(form).not.toBe(null);
120 | expect(form.children.length).toBe(3);
121 | });
122 | });
123 |
124 | /*
125 | * Unit tests for createSpinner
126 | */
127 | describe('lightbox load indicator (createSpinner)', function () {
128 | it('creates an image', function () {
129 | var spinner = RealexHpp._internal.createSpinner();
130 | expect(spinner).not.toBe(null);
131 | expect(spinner.getAttribute('id')).toMatch(/rxp\-loader\-/);
132 | });
133 | });
134 |
135 | /*
136 | * Unit tests for getUrlParser
137 | */
138 | describe('url parsing (getUrlParser)', function () {
139 | var testUrl = 'http://hostname.com/path?query=true';
140 |
141 | it('parses url', function () {
142 | var url = RealexHpp._internal.getUrlParser(testUrl);
143 | expect(url).not.toBe(null);
144 | expect(url.hostname).toBe('hostname.com');
145 | expect(url.pathname).toBe('/path');
146 | });
147 | });
148 |
149 | /*
150 | * Unit tests for getHostnameFromUrl
151 | */
152 | describe('url parsing (getHostnameFromUrl)', function () {
153 | var testUrl = 'http://hostname.com/path?query=true';
154 |
155 | it('parses hostname', function () {
156 | var host = RealexHpp._internal.getHostnameFromUrl(testUrl);
157 | expect(host).toBe('hostname.com');
158 | });
159 | });
160 |
161 |
162 | /*
163 | * Unit tests for getHostnameFromUrl
164 | */
165 | describe('url parsing (getHostnameFromUrl)', function () {
166 | var testUrl = 'http://hostname.com/path?query=true';
167 |
168 | it('same host returns true', function () {
169 | var host = RealexHpp._internal.isMessageFromHpp(testUrl, testUrl);
170 | expect(host).toBe(true);
171 | });
172 |
173 | it('different hosts return false', function () {
174 | var host = RealexHpp._internal.isMessageFromHpp(testUrl, '#');
175 | expect(host).toBe(false);
176 | });
177 | });
178 | });
179 |
--------------------------------------------------------------------------------
/specs/unit/rxp-remote_spec.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Unit tests for rxp-remote.js
3 | */
4 | describe( 'rxp-remote library', function () {
5 |
6 | /*
7 | * Unit tests for validateCardNumber
8 | */
9 | describe( 'card validation (validateCardNumber)', function () {
10 | it('valid card', function () {
11 | expect(RealexRemote.validateCardNumber('424242424242424242')).toBe(true);
12 | });
13 |
14 | it('non-numeric card', function () {
15 | expect(RealexRemote.validateCardNumber('a24242424242424242')).toBe(false);
16 | });
17 |
18 | it('card with spaces', function () {
19 | expect(RealexRemote.validateCardNumber('4242 424242424242')).toBe(false);
20 | });
21 |
22 | it('empty card', function () {
23 | expect(RealexRemote.validateCardNumber('')).toBe(false);
24 | });
25 |
26 | it('undefined card', function () {
27 | expect(RealexRemote.validateCardNumber()).toBe(false);
28 | });
29 |
30 | it('white space only', function () {
31 | expect(RealexRemote.validateCardNumber(' ')).toBe(false);
32 | });
33 |
34 | it('length < 12', function () {
35 | expect(RealexRemote.validateCardNumber('42424242420')).toBe(false);
36 | });
37 |
38 | it('length > 19', function () {
39 | expect(RealexRemote.validateCardNumber('42424242424242424242')).toBe(false);
40 | });
41 |
42 | it('length = 12', function () {
43 | expect(RealexRemote.validateCardNumber('424242424242')).toBe(true);
44 | });
45 |
46 | it('length = 19', function () {
47 | expect(RealexRemote.validateCardNumber('4242424242424242428')).toBe(true);
48 | });
49 |
50 | it('luhn check', function () {
51 | expect(RealexRemote.validateCardNumber('4242424242424242427')).toBe(false);
52 | });
53 | });
54 |
55 | /*
56 | * Unit tests for validateCardHolderName
57 | */
58 | describe( 'card holder name validation (validateCardHolderName)', function () {
59 | it('valid name', function () {
60 | expect(RealexRemote.validateCardHolderName('Joe Smith')).toBe(true);
61 | });
62 |
63 | it('empty name', function () {
64 | expect(RealexRemote.validateCardHolderName('')).toBe(false);
65 | });
66 |
67 | it('undefined name', function () {
68 | expect(RealexRemote.validateCardHolderName()).toBe(false);
69 | });
70 |
71 | it('white space only', function () {
72 | expect(RealexRemote.validateCardHolderName(' ')).toBe(false);
73 | });
74 |
75 | it('name of 100 characters', function () {
76 | expect(RealexRemote.validateCardHolderName('abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij')).toBe(true);
77 | });
78 |
79 | it('name over 100 characters', function () {
80 | expect(RealexRemote.validateCardHolderName('abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghija')).toBe(false);
81 | });
82 |
83 | it('ISO/IEC 8859-1 characters 1', function () {
84 | expect(RealexRemote.validateCardHolderName('!\" # $ % & \' ( ) * + - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R')).toBe(true);
85 | });
86 |
87 | it('ISO/IEC 8859-1 characters 2', function () {
88 | expect(RealexRemote.validateCardHolderName('S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~ ¡ ¢ £ ¤ ¥')).toBe(true);
89 | });
90 |
91 | it('ISO/IEC 8859-1 characters 3', function () {
92 | expect(RealexRemote.validateCardHolderName('¦ § ¨ © ª « ¬ ® ¯ ° ± ² ³ ´ µ ¶ · ¸ ¹ º » ¼ ½ ¾ ¿ À Á Â Ã Ä Å Æ Ç È É Ê Ë Ì Í Î Ï Ð Ñ Ò Ó Ô Õ Ö')).toBe(true);
93 | });
94 |
95 | it('ISO/IEC 8859-1 characters 4', function () {
96 | expect(RealexRemote.validateCardHolderName('× Ø Ù Ú Û Ü Ý Þ ß à á â ã ä å æ ç è é ê ë ì í î ï ð ñ ò ó ô õ ö ÷ ø ù ú û ü ý þ ÿ')).toBe(true);
97 | });
98 |
99 | it('non-ISO/IEC 8859-1 characters', function () {
100 | expect(RealexRemote.validateCardHolderName('€')).toBe(false);
101 | });
102 |
103 | });
104 |
105 | /*
106 | * Unit tests for validateAmexCvn
107 | */
108 | describe( 'CVN Amex validation (validateAmexCvn)', function () {
109 | it('valid Amex CVN', function () {
110 | expect(RealexRemote.validateAmexCvn('1234')).toBe(true);
111 | });
112 |
113 | it('empty CVN', function () {
114 | expect(RealexRemote.validateAmexCvn('')).toBe(false);
115 | });
116 |
117 | it('undefined CVN', function () {
118 | expect(RealexRemote.validateAmexCvn()).toBe(false);
119 | });
120 |
121 | it('white space only', function () {
122 | expect(RealexRemote.validateAmexCvn(' ')).toBe(false);
123 | });
124 |
125 | it('Amex CVN of 5 numbers', function () {
126 | expect(RealexRemote.validateAmexCvn('12345')).toBe(false);
127 | });
128 |
129 | it('Amex CVN of 3 numbers', function () {
130 | expect(RealexRemote.validateAmexCvn('123')).toBe(false);
131 | });
132 |
133 | it('non-numeric Amex CVN of 4 characters', function () {
134 | expect(RealexRemote.validateAmexCvn('123a')).toBe(false);
135 | });
136 |
137 | });
138 |
139 | /*
140 | * Unit tests for validateCvn
141 | */
142 | describe( 'CVN non-Amex validation (validateCvn)', function () {
143 | it('valid non-Amex CVN', function () {
144 | expect(RealexRemote.validateCvn('123')).toBe(true);
145 | });
146 |
147 | it('empty CVN', function () {
148 | expect(RealexRemote.validateCvn('')).toBe(false);
149 | });
150 |
151 | it('undefined CVN', function () {
152 | expect(RealexRemote.validateCvn()).toBe(false);
153 | });
154 |
155 | it('white space only', function () {
156 | expect(RealexRemote.validateCvn(' ')).toBe(false);
157 | });
158 |
159 | it('non-Amex CVN of 4 numbers', function () {
160 | expect(RealexRemote.validateCvn('1234')).toBe(false);
161 | });
162 |
163 | it('non-Amex CVN of 2 numbers', function () {
164 | expect(RealexRemote.validateCvn('12')).toBe(false);
165 | });
166 |
167 | it('non-numeric non-Amex CVN of 3 characters', function () {
168 | expect(RealexRemote.validateCvn('12a')).toBe(false);
169 | });
170 | });
171 |
172 | /*
173 | * Unit tests for validateExpiryDateFormat
174 | */
175 | describe( 'Expiry date format validation (validateExpiryDateFormat)', function () {
176 |
177 | it('valid date 1299', function () {
178 | expect(RealexRemote.validateExpiryDateFormat('1299')).toBe(true);
179 | });
180 |
181 | it('valid date 0199', function () {
182 | expect(RealexRemote.validateExpiryDateFormat('0199')).toBe(true);
183 | });
184 |
185 | it('non-numeric date', function () {
186 | expect(RealexRemote.validateExpiryDateFormat('a199')).toBe(false);
187 | });
188 |
189 | it('date with spaces', function () {
190 | expect(RealexRemote.validateExpiryDateFormat('1 99')).toBe(false);
191 | });
192 |
193 | it('empty date', function () {
194 | expect(RealexRemote.validateExpiryDateFormat('')).toBe(false);
195 | });
196 |
197 | it('undefined date', function () {
198 | expect(RealexRemote.validateExpiryDateFormat()).toBe(false);
199 | });
200 |
201 | it('white space only', function () {
202 | expect(RealexRemote.validateExpiryDateFormat(' ')).toBe(false);
203 | });
204 |
205 | it('length > 4', function () {
206 | expect(RealexRemote.validateExpiryDateFormat('12099')).toBe(false);
207 | });
208 |
209 | it('length < 4', function () {
210 | expect(RealexRemote.validateExpiryDateFormat('199')).toBe(false);
211 | });
212 |
213 | it('invalid month 00', function () {
214 | expect(RealexRemote.validateExpiryDateFormat('0099')).toBe(false);
215 | });
216 |
217 | it('invalid month 13', function () {
218 | expect(RealexRemote.validateExpiryDateFormat('1399')).toBe(false);
219 | });
220 | });
221 |
222 | /*
223 | * Unit tests for validateExpiryDateNotInPast
224 | */
225 | describe( 'Expiry date not in past validation (validateExpiryDateNotInPast)', function () {
226 |
227 | it('date in past', function () {
228 | expect(RealexRemote.validateExpiryDateNotInPast('0615')).toBe(false);
229 | });
230 |
231 | it('current month', function () {
232 | var now = new Date();
233 | var nowMonth = '' + (now.getMonth() + 1);
234 | nowMonth = nowMonth.length < 2 ? '0' + nowMonth : nowMonth;
235 | var nowYear = ('' + now.getFullYear()).substr(2,4);
236 | expect(RealexRemote.validateExpiryDateNotInPast(nowMonth + nowYear)).toBe(true);
237 | });
238 | });
239 |
240 | });
--------------------------------------------------------------------------------