eab6336f468819c3509afdee5590de95abc90b4f
[moodle.git] / payment / gateway / paypal / amd / src / gateways_modal.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * This module is responsible for PayPal content in the gateways modal.
18  *
19  * @module     pg_paypal/gateway_modal
20  * @copyright  2020 Shamim Rezaie <shamim@moodle.com>
21  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  */
24 import * as Repository from './repository';
25 import Templates from 'core/templates';
26 import Truncate from 'core/truncate';
27 import Ajax from 'core/ajax';
28 import ModalFactory from 'core/modal_factory';
29 import ModalEvents from 'core/modal_events';
30 import {get_string as getString} from 'core/str';
32 /**
33  * Creates and shows a modal that contains a placeholder.
34  *
35  * @returns {Promise<Modal>}
36  */
37 const showModalWithPlaceholder = async() => {
38     const modal = await ModalFactory.create({
39         body: await Templates.render('pg_paypal/paypal_button_placeholder', {})
40     });
41     modal.show();
42     return modal;
43 };
45 /**
46  * Process the payment.
47  *
48  * @param {string} component Name of the component that the componentid belongs to
49  * @param {number} componentid An internal identifier that is used by the component
50  * @param {string} description Description of the payment
51  * @param {processCallback} callback The callback function to call when processing is finished
52  * @returns {Promise<void>}
53  */
54 export const process = async(component, componentid, description, callback) => {
56     const [
57         modal,
58         paypalConfig,
59     ] = await Promise.all([
60         showModalWithPlaceholder(),
61         Repository.getConfigForJs(component, componentid),
62     ]);
63     const currency = paypalConfig.currency;
64     const amount = paypalConfig.cost; // Cost with surcharge.
66     modal.getRoot().on(ModalEvents.hidden, () => {
67         // Destroy when hidden.
68         modal.destroy();
69     });
71     const paypalScript = `https://www.paypal.com/sdk/js?client-id=${paypalConfig.clientid}&currency=${currency}`;
73     callExternalFunction(paypalScript, () => {
74         modal.setBody(''); // We have to clear the body. The render method in paypal.Buttons will render everything.
76         paypal.Buttons({ // eslint-disable-line
77             // Set up the transaction.
78             createOrder: function(data, actions) {
79                 return actions.order.create({
80                     purchase_units: [{ // eslint-disable-line
81                         amount: {
82                             currency_code: currency, // eslint-disable-line
83                             value: amount
84                         },
85                         description: Truncate.truncate(description, {length: 127, stripTags: true}),
86                     }],
87                     application_context: { // eslint-disable-line
88                         shipping_preference: 'NO_SHIPPING', // eslint-disable-line
89                         brand_name: Truncate.truncate(paypalConfig.brandname, {length: 127, stripTags: true}), // eslint-disable-line
90                     },
91                 });
92             },
93             // Finalise the transaction.
94             onApprove: function(data) {
95                 modal.getRoot().on(ModalEvents.outsideClick, (e) => {
96                     // Prevent closing the modal when clicking outside of it.
97                     e.preventDefault();
98                 });
100                 modal.setBody(getString('authorising', 'pg_paypal'));
102                 // Call server to validate and capture payment for order.
103                 return Ajax.call([{
104                     methodname: 'pg_paypal_create_transaction_complete',
105                     args: {
106                         component,
107                         componentid,
108                         orderid: data.orderID,
109                     },
110                 }])[0]
111                 .then(function(res) {
112                     modal.hide();
113                     return callback(res);
114                 });
115             }
116         }).render(modal.getBody()[0]);
117     });
118 };
120 /**
121  * The callback definition for process.
122  *
123  * @callback processCallback
124  * @param {bool} success
125  * @param {string} message
126  */
128 /**
129  * Calls a function from an external javascript file.
130  *
131  * @param {string} jsFile URL of the external JavaScript file
132  * @param {function} func The function to call
133  */
134 const callExternalFunction = (jsFile, func) => {
135     // Check to see if this file has already been loaded. If so just go straight to the func.
136     if (callExternalFunction.currentlyloaded == jsFile) {
137         func();
138         return;
139     }
141     // PayPal can only work with one currency at the same time. We have to unload the previously loaded script
142     // if it was loaded for a different currency. Weird way indeed, but the only way.
143     // See: https://github.com/paypal/paypal-checkout-components/issues/1180
144     if (callExternalFunction.currentlyloaded) {
145         const suspectedScript = document.querySelector(`script[src="${callExternalFunction.currentlyloaded}"]`);
146         if (suspectedScript) {
147             suspectedScript.parentNode.removeChild(suspectedScript);
148         }
149     }
151     const script = document.createElement('script');
153     if (script.readyState) {
154         script.onreadystatechange = function() {
155             if (this.readyState == 'complete' || this.readyState == 'loaded') {
156                 this.onreadystatechange = null;
157                 func();
158             }
159         };
160     } else {
161         script.onload = function() {
162             func();
163         };
164     }
166     script.setAttribute('src', jsFile);
167     document.head.appendChild(script);
169     callExternalFunction.currentlyloaded = jsFile;
170 };
172 /**
173  * Holds the full url of loaded external JavaScript file.
174  *
175  * @static
176  * @type {string}
177  */
178 callExternalFunction.currentlyloaded = '';