├── .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 | Global Payments logo 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 |
38 |
39 |
40 |
41 | Try Again 42 |
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 | }); --------------------------------------------------------------------------------