4d68227e4b50532a3027977d5fb664c54edaa459
[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     paygw_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('paygw_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 itemId belongs to
49  * @param {string} paymentArea The area of the component that the itemId belongs to
50  * @param {number} itemId An internal identifier that is used by the component
51  * @param {string} description Description of the payment
52  * @param {processCallback} callback The callback function to call when processing is finished
53  * @returns {Promise<void>}
54  */
55 export const process = async(component, paymentArea, itemId, description, callback) => {
57     const [
58         modal,
59         paypalConfig,
60     ] = await Promise.all([
61         showModalWithPlaceholder(),
62         Repository.getConfigForJs(component, paymentArea, itemId),
63     ]);
64     const currency = paypalConfig.currency;
65     const amount = paypalConfig.cost; // Cost with surcharge.
67     modal.getRoot().on(ModalEvents.hidden, () => {
68         // Destroy when hidden.
69         modal.destroy();
70     });
72     const paypalScript = `https://www.paypal.com/sdk/js?client-id=${paypalConfig.clientid}&currency=${currency}`;
74     callExternalFunction(paypalScript, () => {
75         modal.setBody(''); // We have to clear the body. The render method in paypal.Buttons will render everything.
77         paypal.Buttons({ // eslint-disable-line
78             // Set up the transaction.
79             createOrder: function(data, actions) {
80                 return actions.order.create({
81                     purchase_units: [{ // eslint-disable-line
82                         amount: {
83                             currency_code: currency, // eslint-disable-line
84                             value: amount
85                         },
86                         description: Truncate.truncate(description, {length: 127, stripTags: true}),
87                     }],
88                     application_context: { // eslint-disable-line
89                         shipping_preference: 'NO_SHIPPING', // eslint-disable-line
90                         brand_name: Truncate.truncate(paypalConfig.brandname, {length: 127, stripTags: true}), // eslint-disable-line
91                     },
92                 });
93             },
94             // Finalise the transaction.
95             onApprove: function(data) {
96                 modal.getRoot().on(ModalEvents.outsideClick, (e) => {
97                     // Prevent closing the modal when clicking outside of it.
98                     e.preventDefault();
99                 });
101                 modal.setBody(getString('authorising', 'paygw_paypal'));
103                 // Call server to validate and capture payment for order.
104                 return Ajax.call([{
105                     methodname: 'paygw_paypal_create_transaction_complete',
106                     args: {
107                         component,
108                         paymentarea: paymentArea,
109                         itemid: itemId,
110                         orderid: data.orderID,
111                     },
112                 }])[0]
113                 .then(function(res) {
114                     modal.hide();
115                     return callback(res);
116                 });
117             }
118         }).render(modal.getBody()[0]);
119     });
120 };
122 /**
123  * The callback definition for process.
124  *
125  * @callback processCallback
126  * @param {bool} success
127  * @param {string} message
128  */
130 /**
131  * Calls a function from an external javascript file.
132  *
133  * @param {string} jsFile URL of the external JavaScript file
134  * @param {function} func The function to call
135  */
136 const callExternalFunction = (jsFile, func) => {
137     // Check to see if this file has already been loaded. If so just go straight to the func.
138     if (callExternalFunction.currentlyloaded == jsFile) {
139         func();
140         return;
141     }
143     // PayPal can only work with one currency at the same time. We have to unload the previously loaded script
144     // if it was loaded for a different currency. Weird way indeed, but the only way.
145     // See: https://github.com/paypal/paypal-checkout-components/issues/1180
146     if (callExternalFunction.currentlyloaded) {
147         const suspectedScript = document.querySelector(`script[src="${callExternalFunction.currentlyloaded}"]`);
148         if (suspectedScript) {
149             suspectedScript.parentNode.removeChild(suspectedScript);
150         }
151     }
153     const script = document.createElement('script');
155     if (script.readyState) {
156         script.onreadystatechange = function() {
157             if (this.readyState == 'complete' || this.readyState == 'loaded') {
158                 this.onreadystatechange = null;
159                 func();
160             }
161         };
162     } else {
163         script.onload = function() {
164             func();
165         };
166     }
168     script.setAttribute('src', jsFile);
169     document.head.appendChild(script);
171     callExternalFunction.currentlyloaded = jsFile;
172 };
174 /**
175  * Holds the full url of loaded external JavaScript file.
176  *
177  * @static
178  * @type {string}
179  */
180 callExternalFunction.currentlyloaded = '';