MDL-69166 core_payment: Use promises instead of callbacks
[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 ModalFactory from 'core/modal_factory';
28 import ModalEvents from 'core/modal_events';
29 import {get_string as getString} from 'core/str';
31 /**
32  * Creates and shows a modal that contains a placeholder.
33  *
34  * @returns {Promise<Modal>}
35  */
36 const showModalWithPlaceholder = async() => {
37     const modal = await ModalFactory.create({
38         body: await Templates.render('paygw_paypal/paypal_button_placeholder', {})
39     });
40     modal.show();
41     return modal;
42 };
44 /**
45  * Process the payment.
46  *
47  * @param {string} component Name of the component that the itemId belongs to
48  * @param {string} paymentArea The area of the component that the itemId belongs to
49  * @param {number} itemId An internal identifier that is used by the component
50  * @param {string} description Description of the payment
51  * @returns {Promise<string>}
52  */
53 export const process = (component, paymentArea, itemId, description) => {
54     return Promise.all([
55         showModalWithPlaceholder(),
56         Repository.getConfigForJs(component, paymentArea, itemId),
57     ])
58     .then(([modal, paypalConfig]) => {
59         modal.getRoot().on(ModalEvents.hidden, () => {
60             // Destroy when hidden.
61             modal.destroy();
62         });
64         return Promise.all([
65             modal,
66             paypalConfig,
67             switchSdk(paypalConfig.clientid, paypalConfig.currency),
68         ]);
69     })
70     .then(([modal, paypalConfig]) => {
71         // We have to clear the body. The render method in paypal.Buttons will render everything.
72         modal.setBody('');
74         return new Promise(resolve => {
75             window.paypal.Buttons({
76                 // Set up the transaction.
77                 createOrder: function(data, actions) {
78                     return actions.order.create({
79                         purchase_units: [{ // eslint-disable-line
80                             amount: {
81                                 currency_code: paypalConfig.currency_code, // eslint-disable-line
82                                 value: paypalConfig.cost,
83                             },
84                             description: Truncate.truncate(description, {length: 127, stripTags: true}),
85                         }],
86                         application_context: { // eslint-disable-line
87                             shipping_preference: 'NO_SHIPPING', // eslint-disable-line
88                             brand_name: Truncate.truncate(paypalConfig.brandname, {length: 127, stripTags: true}), // eslint-disable-line
89                         },
90                     });
91                 },
92                 // Finalise the transaction.
93                 onApprove: function(data) {
94                     modal.getRoot().on(ModalEvents.outsideClick, (e) => {
95                         // Prevent closing the modal when clicking outside of it.
96                         e.preventDefault();
97                     });
99                     modal.setBody(getString('authorising', 'paygw_paypal'));
101                     Repository.markTransactionComplete(component, paymentArea, itemId, data.orderID)
102                     .then(res => {
103                         modal.hide();
104                         return res;
105                     })
106                     .then(resolve);
107                 }
108             }).render(modal.getBody()[0]);
109         });
110     })
111     .then(res => {
112         if (res.success) {
113             return Promise.resolve(res.message);
114         }
116         return Promise.reject(res.message);
117     });
118 };
120 /**
121  * Unloads the previously loaded PayPal JavaScript SDK, and loads a new one.
122  *
123  * @param {string} clientId PayPal client ID
124  * @param {string} currency The currency
125  * @returns {Promise}
126  */
127 const switchSdk = (clientId, currency) => {
128     const sdkUrl = `https://www.paypal.com/sdk/js?client-id=${clientId}&currency=${currency}`;
130     // Check to see if this file has already been loaded. If so just go straight to the func.
131     if (switchSdk.currentlyloaded === sdkUrl) {
132         return Promise.resolve();
133     }
135     // PayPal can only work with one currency at the same time. We have to unload the previously loaded script
136     // if it was loaded for a different currency. Weird way indeed, but the only way.
137     // See: https://github.com/paypal/paypal-checkout-components/issues/1180
138     if (switchSdk.currentlyloaded) {
139         const suspectedScript = document.querySelector(`script[src="${switchSdk.currentlyloaded}"]`);
140         if (suspectedScript) {
141             suspectedScript.parentNode.removeChild(suspectedScript);
142         }
143     }
145     const script = document.createElement('script');
147     return new Promise(resolve => {
148         if (script.readyState) {
149             script.onreadystatechange = function() {
150                 if (this.readyState == 'complete' || this.readyState == 'loaded') {
151                     this.onreadystatechange = null;
152                     resolve();
153                 }
154             };
155         } else {
156             script.onload = function() {
157                 resolve();
158             };
159         }
161         script.setAttribute('src', sdkUrl);
162         document.head.appendChild(script);
164         switchSdk.currentlyloaded = sdkUrl;
165     });
166 };
168 /**
169  * Holds the full url of loaded PayPal JavaScript SDK.
170  *
171  * @static
172  * @type {string}
173  */
174 switchSdk.currentlyloaded = '';