From 950e69dd4cc1e352e878ea18dfd9dd6a457af468 Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Sat, 18 Jan 2020 03:20:41 +1100 Subject: [PATCH] MDL-69166 pg_paypal: capture payment directly --- .../paypal/amd/build/gateways_modal.min.js | 2 +- .../amd/build/gateways_modal.min.js.map | 2 +- .../gateway/paypal/amd/src/gateways_modal.js | 53 ++++++------- .../classes/external/transaction_complete.php | 74 ++++++++++--------- .../gateway/paypal/classes/paypal_helper.php | 15 ++++ payment/gateway/paypal/db/install.xml | 3 - payment/gateway/paypal/lang/en/pg_paypal.php | 8 +- payment/gateway/paypal/settings.php | 3 - 8 files changed, 86 insertions(+), 74 deletions(-) diff --git a/payment/gateway/paypal/amd/build/gateways_modal.min.js b/payment/gateway/paypal/amd/build/gateways_modal.min.js index 6a780da4d44..fa4872a725e 100644 --- a/payment/gateway/paypal/amd/build/gateways_modal.min.js +++ b/payment/gateway/paypal/amd/build/gateways_modal.min.js @@ -1,2 +1,2 @@ -function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("pg_paypal/gateways_modal",["exports","./repository","core/templates","core/truncate","core/ajax","core/modal_factory","core/modal_events"],function(a,b,c,d,e,f,g){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.process=void 0;b=j(b);c=h(c);d=h(d);e=h(e);f=h(f);g=h(g);function h(a){return a&&a.__esModule?a:{default:a}}function i(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;i=function(){return a};return a}function j(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=i();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function k(a,b){return r(a)||q(a,b)||m(a,b)||l()}function l(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function m(a,b){if(!a)return;if("string"==typeof a)return p(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return p(a,b)}function p(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);c");paypal.Buttons({createOrder:function createOrder(a,b){return b.order.create({purchase_units:[{amount:{currency_code:f,value:c},description:d.default.truncate(j,{length:127,stripTags:!0})}],application_context:{shipping_preference:"NO_SHIPPING",brand_name:d.default.truncate(p.brandname,{length:127,stripTags:!0})}})},onApprove:function onApprove(a,b){b.order.authorize().then(function(b){var c=b.purchase_units[0].payments.authorizations[0].id;return e.default.call([{methodname:"pg_paypal_transaction_complete",args:{component:h,componentid:i,orderid:a.orderID,authorizationid:c}}])[0].then(function(a){o.hide();return l(a)})})}}).render(o.getBody()[0])});case 9:case"end":return a.stop();}}},a)}));return function(){return a.apply(this,arguments)}}();a.process=v;var w=function(a,b){if(w.currentlyloaded==a){b();return}if(w.currentlyloaded){var d=document.querySelector("script[src=\"".concat(w.currentlyloaded,"\"]"));if(d){d.parentNode.removeChild(d)}}var c=document.createElement("script");if(c.readyState){c.onreadystatechange=function(){if("complete"==this.readyState||"loaded"==this.readyState){this.onreadystatechange=null;b()}}}else{c.onload=function(){b()}}c.setAttribute("src",a);document.head.appendChild(c);w.currentlyloaded=a};w.currentlyloaded=""}); +function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("pg_paypal/gateways_modal",["exports","./repository","core/templates","core/truncate","core/ajax","core/modal_factory","core/modal_events","core/str"],function(a,b,c,d,e,f,g,h){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.process=void 0;b=k(b);c=i(c);d=i(d);e=i(e);f=i(f);g=i(g);function i(a){return a&&a.__esModule?a:{default:a}}function j(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;j=function(){return a};return a}function k(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=j();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function l(a,b){return r(a)||q(a,b)||n(a,b)||m()}function m(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function n(a,b){if(!a)return;if("string"==typeof a)return p(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return p(a,b)}function p(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);c.\n\n/**\n * This module is responsible for PayPal content in the gateways modal.\n *\n * @module pg_paypal/gateway_modal\n * @copyright 2020 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Repository from './repository';\nimport Templates from 'core/templates';\nimport Truncate from 'core/truncate';\nimport Ajax from 'core/ajax';\nimport ModalFactory from 'core/modal_factory';\nimport ModalEvents from 'core/modal_events';\n\n/**\n * Creates and shows a modal that contains a placeholder.\n *\n * @returns {Promise}\n */\nconst showPlaceholder = async() => {\n const modal = await ModalFactory.create({\n type: ModalFactory.types.CANCEL,\n body: await Templates.render('pg_paypal/paypal_button_placeholder', {})\n });\n modal.show();\n return modal;\n};\n\n/**\n * Process the payment.\n *\n * @param {double} amount Amount of payment\n * @param {string} currency The currency in the three-character ISO-4217 format\n * @param {string} component Name of the component that the componentid belongs to\n * @param {number} componentid An internal identifier that is used by the component\n * @param {string} description Description of the payment\n * @param {processCallback} callback The callback function to call when processing is finished\n * @returns {Promise}\n */\nexport const process = async(amount, currency, component, componentid, description, callback) => {\n\n const [\n modal,\n paypalConfig,\n ] = await Promise.all([\n showPlaceholder(),\n Repository.getConfigForJs(),\n ]);\n\n modal.getRoot().on(ModalEvents.hidden, () => {\n // Destroy when hidden.\n modal.destroy();\n });\n\n const paypalScript = `https://www.paypal.com/sdk/js?client-id=${paypalConfig.clientid}¤cy=${currency}&intent=authorize`;\n\n callExternalFunction(paypalScript, () => {\n modal.setBody('
'); // This is a hack. Instead of emptying the body, we put an empty form there so the modal\n // is not closed when user clicks outside of modal.\n paypal.Buttons({ // eslint-disable-line\n createOrder: function(data, actions) {\n // This function sets up the details of the transaction, including the amount and line item details.\n return actions.order.create({\n purchase_units: [{ // eslint-disable-line\n amount: {\n currency_code: currency, // eslint-disable-line\n value: amount\n },\n description: Truncate.truncate(description, {length: 127, stripTags: true}),\n }],\n application_context: { // eslint-disable-line\n shipping_preference: 'NO_SHIPPING', // eslint-disable-line\n brand_name: Truncate.truncate(paypalConfig.brandname, {length: 127, stripTags: true}), // eslint-disable-line\n },\n });\n },\n onApprove: function(data, actions) {\n // Authorize the transaction.\n actions.order.authorize().then(function(authorization) {\n // Get the authorization id.\n const authorizationID = authorization.purchase_units[0].payments.authorizations[0].id;\n\n // Call your server to validate and capture the transaction.\n return Ajax.call([{\n methodname: 'pg_paypal_transaction_complete',\n args: {\n component,\n componentid,\n orderid: data.orderID,\n authorizationid: authorizationID,\n },\n }])[0]\n .then(function(res) {\n modal.hide();\n return callback(res);\n });\n });\n }\n }).render(modal.getBody()[0]);\n });\n};\n\n/**\n * The callback definition for process.\n *\n * @callback processCallback\n * @param {bool} success\n * @param {string} message\n */\n\n/**\n * Calls a function from an external javascript file.\n *\n * @param {string} jsFile URL of the external JavaScript file\n * @param {function} func The function to call\n */\nconst callExternalFunction = (jsFile, func) => {\n // Check to see if this file has already been loaded. If so just go straight to the func.\n if (callExternalFunction.currentlyloaded == jsFile) {\n func();\n return;\n }\n\n // PayPal can only work with one currency at the same time. We have to unload the previously loaded script\n // if it was loaded for a different currency. Weird way indeed, but the only way.\n // See: https://github.com/paypal/paypal-checkout-components/issues/1180\n if (callExternalFunction.currentlyloaded) {\n const suspectedScript = document.querySelector(`script[src=\"${callExternalFunction.currentlyloaded}\"]`);\n if (suspectedScript) {\n suspectedScript.parentNode.removeChild(suspectedScript);\n }\n }\n\n const script = document.createElement('script');\n\n if (script.readyState) {\n script.onreadystatechange = function() {\n if (this.readyState == 'complete' || this.readyState == 'loaded') {\n this.onreadystatechange = null;\n func();\n }\n };\n } else {\n script.onload = function() {\n func();\n };\n }\n\n script.setAttribute('src', jsFile);\n document.head.appendChild(script);\n\n callExternalFunction.currentlyloaded = jsFile;\n};\n\n/**\n * Holds the full url of loaded external JavaScript file.\n *\n * @static\n * @type {string}\n */\ncallExternalFunction.currentlyloaded = '';\n"],"file":"gateways_modal.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/gateways_modal.js"],"names":["showModalWithPlaceholder","ModalFactory","Templates","render","body","create","modal","show","process","amount","currency","component","componentid","description","callback","Promise","all","Repository","getConfigForJs","paypalConfig","getRoot","on","ModalEvents","outsideClick","e","preventDefault","hidden","destroy","paypalScript","clientid","callExternalFunction","setBody","paypal","Buttons","createOrder","data","actions","order","purchase_units","currency_code","value","Truncate","truncate","length","stripTags","application_context","shipping_preference","brand_name","brandname","onApprove","Ajax","call","methodname","args","orderid","orderID","then","res","hide","getBody","jsFile","func","currentlyloaded","suspectedScript","document","querySelector","parentNode","removeChild","script","createElement","readyState","onreadystatechange","onload","setAttribute","head","appendChild"],"mappings":"2iBAuBA,OACA,OACA,OACA,OACA,OACA,O,u3DAQMA,CAAAA,CAAwB,4CAAG,yGACTC,SADS,gBAEbC,WAAUC,MAAV,CAAiB,qCAAjB,CAAwD,EAAxD,CAFa,0BAEzBC,IAFyB,4BACIC,MADJ,wBACvBC,CADuB,QAI7BA,CAAK,CAACC,IAAN,GAJ6B,yBAKtBD,CALsB,2CAAH,uD,CAmBjBE,CAAO,4CAAG,WAAMC,CAAN,CAAcC,CAAd,CAAwBC,CAAxB,CAAmCC,CAAnC,CAAgDC,CAAhD,CAA6DC,CAA7D,gHAKTC,CAAAA,OAAO,CAACC,GAAR,CAAY,CAClBhB,CAAwB,EADN,CAElBiB,CAAU,CAACC,cAAX,EAFkB,CAAZ,CALS,0BAGfZ,CAHe,MAIfa,CAJe,MAUnBb,CAAK,CAACc,OAAN,GAAgBC,EAAhB,CAAmBC,UAAYC,YAA/B,CAA6C,SAACC,CAAD,CAAO,CAEhDA,CAAC,CAACC,cAAF,EACH,CAHD,EAKAnB,CAAK,CAACc,OAAN,GAAgBC,EAAhB,CAAmBC,UAAYI,MAA/B,CAAuC,UAAM,CAEzCpB,CAAK,CAACqB,OAAN,EACH,CAHD,EAKMC,CApBa,mDAoB6CT,CAAY,CAACU,QApB1D,sBAoB+EnB,CApB/E,EAsBnBoB,CAAoB,CAACF,CAAD,CAAe,UAAM,CACrCtB,CAAK,CAACyB,OAAN,CAAc,EAAd,EAEAC,MAAM,CAACC,OAAP,CAAe,CAEXC,WAAW,CAAE,qBAASC,CAAT,CAAeC,CAAf,CAAwB,CACjC,MAAOA,CAAAA,CAAO,CAACC,KAAR,CAAchC,MAAd,CAAqB,CACxBiC,cAAc,CAAE,CAAC,CACb7B,MAAM,CAAE,CACJ8B,aAAa,CAAE7B,CADX,CAEJ8B,KAAK,CAAE/B,CAFH,CADK,CAKbI,WAAW,CAAE4B,UAASC,QAAT,CAAkB7B,CAAlB,CAA+B,CAAC8B,MAAM,CAAE,GAAT,CAAcC,SAAS,GAAvB,CAA/B,CALA,CAAD,CADQ,CAQxBC,mBAAmB,CAAE,CACjBC,mBAAmB,CAAE,aADJ,CAEjBC,UAAU,CAAEN,UAASC,QAAT,CAAkBvB,CAAY,CAAC6B,SAA/B,CAA0C,CAACL,MAAM,CAAE,GAAT,CAAcC,SAAS,GAAvB,CAA1C,CAFK,CARG,CAArB,CAaV,CAhBU,CAkBXK,SAAS,CAAE,mBAASd,CAAT,CAAe,CACtB7B,CAAK,CAACyB,OAAN,CAAc,iBAAU,aAAV,CAAyB,WAAzB,CAAd,EAGA,MAAOmB,WAAKC,IAAL,CAAU,CAAC,CACdC,UAAU,CAAE,uCADE,CAEdC,IAAI,CAAE,CACF1C,SAAS,CAATA,CADE,CAEFC,WAAW,CAAXA,CAFE,CAGF0C,OAAO,CAAEnB,CAAI,CAACoB,OAHZ,CAFQ,CAAD,CAAV,EAOH,CAPG,EAQNC,IARM,CAQD,SAASC,CAAT,CAAc,CAChBnD,CAAK,CAACoD,IAAN,GACA,MAAO5C,CAAAA,CAAQ,CAAC2C,CAAD,CAClB,CAXM,CAYV,CAlCU,CAAf,EAmCGtD,MAnCH,CAmCUG,CAAK,CAACqD,OAAN,GAAgB,CAAhB,CAnCV,CAoCH,CAvCmB,CAApB,CAtBmB,yCAAH,uD,aA8EpB,GAAM7B,CAAAA,CAAoB,CAAG,SAAC8B,CAAD,CAASC,CAAT,CAAkB,CAE3C,GAAI/B,CAAoB,CAACgC,eAArB,EAAwCF,CAA5C,CAAoD,CAChDC,CAAI,GACJ,MACH,CAKD,GAAI/B,CAAoB,CAACgC,eAAzB,CAA0C,CACtC,GAAMC,CAAAA,CAAe,CAAGC,QAAQ,CAACC,aAAT,wBAAsCnC,CAAoB,CAACgC,eAA3D,QAAxB,CACA,GAAIC,CAAJ,CAAqB,CACjBA,CAAe,CAACG,UAAhB,CAA2BC,WAA3B,CAAuCJ,CAAvC,CACH,CACJ,CAED,GAAMK,CAAAA,CAAM,CAAGJ,QAAQ,CAACK,aAAT,CAAuB,QAAvB,CAAf,CAEA,GAAID,CAAM,CAACE,UAAX,CAAuB,CACnBF,CAAM,CAACG,kBAAP,CAA4B,UAAW,CACnC,GAAuB,UAAnB,OAAKD,UAAL,EAAoD,QAAnB,OAAKA,UAA1C,CAAkE,CAC9D,KAAKC,kBAAL,CAA0B,IAA1B,CACAV,CAAI,EACP,CACJ,CACJ,CAPD,IAOO,CACHO,CAAM,CAACI,MAAP,CAAgB,UAAW,CACvBX,CAAI,EACP,CACJ,CAEDO,CAAM,CAACK,YAAP,CAAoB,KAApB,CAA2Bb,CAA3B,EACAI,QAAQ,CAACU,IAAT,CAAcC,WAAd,CAA0BP,CAA1B,EAEAtC,CAAoB,CAACgC,eAArB,CAAuCF,CAC1C,CApCD,CA4CA9B,CAAoB,CAACgC,eAArB,CAAuC,E","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module is responsible for PayPal content in the gateways modal.\n *\n * @module pg_paypal/gateway_modal\n * @copyright 2020 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Repository from './repository';\nimport Templates from 'core/templates';\nimport Truncate from 'core/truncate';\nimport Ajax from 'core/ajax';\nimport ModalFactory from 'core/modal_factory';\nimport ModalEvents from 'core/modal_events';\nimport {get_string as getString} from 'core/str';\n\n/**\n * Creates and shows a modal that contains a placeholder.\n *\n * @returns {Promise}\n */\nconst showModalWithPlaceholder = async() => {\n const modal = await ModalFactory.create({\n body: await Templates.render('pg_paypal/paypal_button_placeholder', {})\n });\n modal.show();\n return modal;\n};\n\n/**\n * Process the payment.\n *\n * @param {double} amount Amount of payment\n * @param {string} currency The currency in the three-character ISO-4217 format\n * @param {string} component Name of the component that the componentid belongs to\n * @param {number} componentid An internal identifier that is used by the component\n * @param {string} description Description of the payment\n * @param {processCallback} callback The callback function to call when processing is finished\n * @returns {Promise}\n */\nexport const process = async(amount, currency, component, componentid, description, callback) => {\n\n const [\n modal,\n paypalConfig,\n ] = await Promise.all([\n showModalWithPlaceholder(),\n Repository.getConfigForJs(),\n ]);\n\n modal.getRoot().on(ModalEvents.outsideClick, (e) => {\n // Prevent closing the modal when clicking outside of it.\n e.preventDefault();\n });\n\n modal.getRoot().on(ModalEvents.hidden, () => {\n // Destroy when hidden.\n modal.destroy();\n });\n\n const paypalScript = `https://www.paypal.com/sdk/js?client-id=${paypalConfig.clientid}¤cy=${currency}`;\n\n callExternalFunction(paypalScript, () => {\n modal.setBody(''); // We have to clear the body. The render method in paypal.Buttons will render everything.\n\n paypal.Buttons({ // eslint-disable-line\n // Set up the transaction.\n createOrder: function(data, actions) {\n return actions.order.create({\n purchase_units: [{ // eslint-disable-line\n amount: {\n currency_code: currency, // eslint-disable-line\n value: amount\n },\n description: Truncate.truncate(description, {length: 127, stripTags: true}),\n }],\n application_context: { // eslint-disable-line\n shipping_preference: 'NO_SHIPPING', // eslint-disable-line\n brand_name: Truncate.truncate(paypalConfig.brandname, {length: 127, stripTags: true}), // eslint-disable-line\n },\n });\n },\n // Finalise the transaction.\n onApprove: function(data) {\n modal.setBody(getString('authorising', 'pg_paypal'));\n\n // Call server to validate and capture payment for order.\n return Ajax.call([{\n methodname: 'pg_paypal_create_transaction_complete',\n args: {\n component,\n componentid,\n orderid: data.orderID,\n },\n }])[0]\n .then(function(res) {\n modal.hide();\n return callback(res);\n });\n }\n }).render(modal.getBody()[0]);\n });\n};\n\n/**\n * The callback definition for process.\n *\n * @callback processCallback\n * @param {bool} success\n * @param {string} message\n */\n\n/**\n * Calls a function from an external javascript file.\n *\n * @param {string} jsFile URL of the external JavaScript file\n * @param {function} func The function to call\n */\nconst callExternalFunction = (jsFile, func) => {\n // Check to see if this file has already been loaded. If so just go straight to the func.\n if (callExternalFunction.currentlyloaded == jsFile) {\n func();\n return;\n }\n\n // PayPal can only work with one currency at the same time. We have to unload the previously loaded script\n // if it was loaded for a different currency. Weird way indeed, but the only way.\n // See: https://github.com/paypal/paypal-checkout-components/issues/1180\n if (callExternalFunction.currentlyloaded) {\n const suspectedScript = document.querySelector(`script[src=\"${callExternalFunction.currentlyloaded}\"]`);\n if (suspectedScript) {\n suspectedScript.parentNode.removeChild(suspectedScript);\n }\n }\n\n const script = document.createElement('script');\n\n if (script.readyState) {\n script.onreadystatechange = function() {\n if (this.readyState == 'complete' || this.readyState == 'loaded') {\n this.onreadystatechange = null;\n func();\n }\n };\n } else {\n script.onload = function() {\n func();\n };\n }\n\n script.setAttribute('src', jsFile);\n document.head.appendChild(script);\n\n callExternalFunction.currentlyloaded = jsFile;\n};\n\n/**\n * Holds the full url of loaded external JavaScript file.\n *\n * @static\n * @type {string}\n */\ncallExternalFunction.currentlyloaded = '';\n"],"file":"gateways_modal.min.js"} \ No newline at end of file diff --git a/payment/gateway/paypal/amd/src/gateways_modal.js b/payment/gateway/paypal/amd/src/gateways_modal.js index ba4416ea5c1..45fbfb13146 100644 --- a/payment/gateway/paypal/amd/src/gateways_modal.js +++ b/payment/gateway/paypal/amd/src/gateways_modal.js @@ -27,15 +27,15 @@ import Truncate from 'core/truncate'; import Ajax from 'core/ajax'; import ModalFactory from 'core/modal_factory'; import ModalEvents from 'core/modal_events'; +import {get_string as getString} from 'core/str'; /** * Creates and shows a modal that contains a placeholder. * * @returns {Promise} */ -const showPlaceholder = async() => { +const showModalWithPlaceholder = async() => { const modal = await ModalFactory.create({ - type: ModalFactory.types.CANCEL, body: await Templates.render('pg_paypal/paypal_button_placeholder', {}) }); modal.show(); @@ -59,23 +59,28 @@ export const process = async(amount, currency, component, componentid, descripti modal, paypalConfig, ] = await Promise.all([ - showPlaceholder(), + showModalWithPlaceholder(), Repository.getConfigForJs(), ]); + modal.getRoot().on(ModalEvents.outsideClick, (e) => { + // Prevent closing the modal when clicking outside of it. + e.preventDefault(); + }); + modal.getRoot().on(ModalEvents.hidden, () => { // Destroy when hidden. modal.destroy(); }); - const paypalScript = `https://www.paypal.com/sdk/js?client-id=${paypalConfig.clientid}¤cy=${currency}&intent=authorize`; + const paypalScript = `https://www.paypal.com/sdk/js?client-id=${paypalConfig.clientid}¤cy=${currency}`; callExternalFunction(paypalScript, () => { - modal.setBody('
'); // This is a hack. Instead of emptying the body, we put an empty form there so the modal - // is not closed when user clicks outside of modal. + modal.setBody(''); // We have to clear the body. The render method in paypal.Buttons will render everything. + paypal.Buttons({ // eslint-disable-line + // Set up the transaction. createOrder: function(data, actions) { - // This function sets up the details of the transaction, including the amount and line item details. return actions.order.create({ purchase_units: [{ // eslint-disable-line amount: { @@ -90,26 +95,22 @@ export const process = async(amount, currency, component, componentid, descripti }, }); }, - onApprove: function(data, actions) { - // Authorize the transaction. - actions.order.authorize().then(function(authorization) { - // Get the authorization id. - const authorizationID = authorization.purchase_units[0].payments.authorizations[0].id; + // Finalise the transaction. + onApprove: function(data) { + modal.setBody(getString('authorising', 'pg_paypal')); - // Call your server to validate and capture the transaction. - return Ajax.call([{ - methodname: 'pg_paypal_transaction_complete', - args: { - component, - componentid, - orderid: data.orderID, - authorizationid: authorizationID, - }, - }])[0] - .then(function(res) { - modal.hide(); - return callback(res); - }); + // Call server to validate and capture payment for order. + return Ajax.call([{ + methodname: 'pg_paypal_create_transaction_complete', + args: { + component, + componentid, + orderid: data.orderID, + }, + }])[0] + .then(function(res) { + modal.hide(); + return callback(res); }); } }).render(modal.getBody()[0]); diff --git a/payment/gateway/paypal/classes/external/transaction_complete.php b/payment/gateway/paypal/classes/external/transaction_complete.php index 46037a9cd08..2b95d2ae72e 100644 --- a/payment/gateway/paypal/classes/external/transaction_complete.php +++ b/payment/gateway/paypal/classes/external/transaction_complete.php @@ -29,6 +29,8 @@ namespace pg_paypal\external; use external_api; use external_function_parameters; use external_value; +use core_payment\helper as payment_helper; +use pg_paypal\paypal_helper; defined('MOODLE_INTERNAL') || die(); @@ -46,7 +48,6 @@ class transaction_complete extends external_api { 'component' => new external_value(PARAM_COMPONENT, 'The component name'), 'componentid' => new external_value(PARAM_INT, 'The item id in the context of the component'), 'orderid' => new external_value(PARAM_TEXT, 'The order id coming back from PayPal'), - 'authorizationid' => new external_value(PARAM_TEXT, 'The authorization id coming back from PayPal'), ]); } @@ -57,18 +58,15 @@ class transaction_complete extends external_api { * @param string $component Name of the component that the componentid belongs to * @param int $componentid An internal identifier that is used by the component * @param string $orderid PayPal order ID - * @param string $authorizationid The PayPal-generated ID for the authorized payment * @return array */ - public static function execute(string $component, int $componentid, string $orderid, - string $authorizationid): array { + public static function execute(string $component, int $componentid, string $orderid): array { global $USER, $DB; self::validate_parameters(self::execute_parameters(), [ 'component' => $component, 'componentid' => $componentid, 'orderid' => $orderid, - 'authorizationid' => $authorizationid, ]); $config = get_config('pg_paypal'); @@ -77,52 +75,56 @@ class transaction_complete extends external_api { [ 'amount' => $amount, 'currency' => $currency - ] = \core_payment\helper::get_cost($component, $componentid); + ] = payment_helper::get_cost($component, $componentid); - $paypalhelper = new \pg_paypal\paypal_helper($config->clientid, $config->secret, $sandbox); - $authorization = $paypalhelper->capture_authorization($authorizationid, $amount, $currency); + $paypalhelper = new paypal_helper($config->clientid, $config->secret, $sandbox); + $orderdetails = $paypalhelper->get_order_details($orderid); $success = false; $message = ''; - if ($authorization) { - switch ($authorization['status']) { - case 'COMPLETED': - $success = true; - // Everything is correct. Let's give them what they paid for. - try { - \core_payment\helper::deliver_order($component, $componentid); + if ($orderdetails) { + if ($orderdetails['status'] == paypal_helper::ORDER_STATUS_APPROVED && + $orderdetails['intent'] == paypal_helper::ORDER_INTENT_CAPTURE) { + $item = $orderdetails['purchase_units'][0]; + if ($item['amount']['value'] == $amount && $item['amount']['currency_code'] == $currency) { + $capture = $paypalhelper->capture_order($orderid); + if ($capture && $capture['status'] == paypal_helper::CAPTURE_STATUS_COMPLETED) { + $success = true; + // Everything is correct. Let's give them what they paid for. + try { + payment_helper::deliver_order($component, $componentid); - $paymentid = \core_payment\helper::save_payment($component, $componentid, (int)$USER->id, $amount, $currency, - 'paypal'); + $paymentid = payment_helper::save_payment($component, $componentid, (int) $USER->id, $amount, + $currency, 'paypal'); - // Store PayPal extra information. - $record = new \stdClass(); - $record->paymentid = $paymentid; - $record->pp_orderid = $orderid; - $record->pp_authorizationid = $authorizationid; - $record->pp_paymentid = $authorization->id; // The PayPal-generated ID for the captured payment. - $record->pp_status = 'COMPLETED'; + // Store PayPal extra information. + $record = new \stdClass(); + $record->paymentid = $paymentid; + $record->pp_orderid = $orderid; - $DB->insert_record('pg_paypal', $record); - } catch (\Exception $e) { - debugging('Exception while trying to process payment: ' . $e->getMessage(), DEBUG_DEVELOPER); + $DB->insert_record('pg_paypal', $record); + } catch (\Exception $e) { + debugging('Exception while trying to process payment: ' . $e->getMessage(), DEBUG_DEVELOPER); + $success = false; + $message = get_string('internalerror', 'pg_paypal'); + } + } else { $success = false; - $message = get_string('internalerror', 'pg_paypal'); + $message = get_string('paymentnotcleared', 'pg_paypal'); } - break; - case 'PENDING': + } else { $success = false; - $message = get_string('echecknotsupported', 'pg_paypal'); - break; - default: - $success = false; - $message = get_string('paymentnotcleared', 'pg_paypal'); + $message = get_string('amountmismatch', 'pg_paypal'); + } + } else { + $success = false; + $message = get_string('paymentnotcleared', 'pg_paypal'); } } else { // Could not capture authorization! $success = false; - $message = get_string('captureauthorizationfailed', 'pg_paypal'); + $message = get_string('cannotfetchorderdatails', 'pg_paypal'); } return [ diff --git a/payment/gateway/paypal/classes/paypal_helper.php b/payment/gateway/paypal/classes/paypal_helper.php index 62bc637e604..f74984e058e 100644 --- a/payment/gateway/paypal/classes/paypal_helper.php +++ b/payment/gateway/paypal/classes/paypal_helper.php @@ -32,6 +32,21 @@ require_once($CFG->libdir . '/filelib.php'); class paypal_helper { + /** + * @var string The payment was authorized or the authorized payment was captured for the order. + */ + public const CAPTURE_STATUS_COMPLETED = 'COMPLETED'; + + /** + * @var string The merchant intends to capture payment immediately after the customer makes a payment. + */ + public const ORDER_INTENT_CAPTURE = 'CAPTURE'; + + /** + * @var string The customer approved the payment. + */ + public const ORDER_STATUS_APPROVED = 'APPROVED'; + /** * @var string The base API URL */ diff --git a/payment/gateway/paypal/db/install.xml b/payment/gateway/paypal/db/install.xml index e93613145c8..808f99e8399 100755 --- a/payment/gateway/paypal/db/install.xml +++ b/payment/gateway/paypal/db/install.xml @@ -9,9 +9,6 @@ - - - diff --git a/payment/gateway/paypal/lang/en/pg_paypal.php b/payment/gateway/paypal/lang/en/pg_paypal.php index 81ed8bd28e7..761e9efb278 100644 --- a/payment/gateway/paypal/lang/en/pg_paypal.php +++ b/payment/gateway/paypal/lang/en/pg_paypal.php @@ -22,19 +22,19 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['amountmismatch'] = 'The amount you attempted to pay does not match the required fee. Your account has not been debited.'; +$string['authorising'] = 'Authorising the payment. Please wait...'; $string['brandname'] = 'Brand name'; $string['brandname_desc'] = 'The optional label that overrides the business name in the PayPal account on the PayPal site.'; -$string['captureauthorizationfailed'] = 'Could not capture authorization.'; +$string['cannotfetchorderdatails'] = 'Could not fetch payment details from PayPal. Your account has not been debited.'; $string['clientid'] = 'Client ID'; $string['clientid_desc'] = 'The client ID that PayPal generated for your application.'; -$string['echecknotsupported'] = 'E-check is not supported.'; $string['environment'] = 'Environment'; $string['environment_desc'] = 'You can set this to Sandbox if you are using sandbox accounts (for testing purpose only).'; $string['gatewaydescription'] = 'PayPal is an authorised payment gateway provider for processing credit card transactions.'; $string['gatewayname'] = 'PayPal'; -$string['internalerror'] = 'Internal error.'; +$string['internalerror'] = 'An internal error has occurred. Please contact us.'; $string['live'] = 'Live'; -$string['paymentauthorizationwarning'] = 'For PayPal payment authorizations, you must enable this feature on your PayPal account.'; $string['paymentnotcleared'] = 'payment not cleared by PayPal.'; $string['pluginname'] = 'PayPal'; $string['pluginname_desc'] = 'The PayPal plugin allows you to receive payments via PayPal.'; diff --git a/payment/gateway/paypal/settings.php b/payment/gateway/paypal/settings.php index 4e5761534dc..cdd850af9ed 100644 --- a/payment/gateway/paypal/settings.php +++ b/payment/gateway/paypal/settings.php @@ -28,9 +28,6 @@ if ($ADMIN->fulltree) { $settings->add(new admin_setting_heading('pg_paypal_settings', '', get_string('pluginname_desc', 'pg_paypal'))); - $warning = $OUTPUT->notification(get_string('paymentauthorizationwarning', 'pg_paypal'), 'warning'); - $settings->add(new admin_setting_heading('pg_paypal/warning', '', $warning)); - $settings->add(new admin_setting_configtext('pg_paypal/brandname', get_string('brandname', 'pg_paypal'), get_string('brandname', 'pg_paypal'), '', PARAM_TEXT)); $settings->add(new admin_setting_configtext('pg_paypal/clientid', get_string('clientid', 'pg_paypal'),