PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
charitable / assets / js / square / charitable-square.js

charitable-square.js in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13, at assets/js/square/charitable-square.js

532 lines 17.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 CHARITABLE = window.CHARITABLE || {};
2
3 (function ($) {
4
5 var $body = $('body');
6 const clientId = CHARITABLE_SQUARE_VARS.client_id;
7 const locationId = CHARITABLE_SQUARE_VARS.location_id;
8 const loadingSVG = `<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
9 <svg width="40" height="10" viewBox="0 0 120 30" xmlns="http://www.w3.org/2000/svg" fill="#f89d35">
10 <circle cx="15" cy="15" r="15">
11 <animate attributeName="r" from="15" to="15"
12 begin="0s" dur="0.8s"
13 values="15;9;15" calcMode="linear"
14 repeatCount="indefinite" />
15 <animate attributeName="fill-opacity" from="1" to="1"
16 begin="0s" dur="0.8s"
17 values="1;.5;1" calcMode="linear"
18 repeatCount="indefinite" />
19 </circle>
20 <circle cx="60" cy="15" r="9" fill-opacity="0.3">
21 <animate attributeName="r" from="9" to="9"
22 begin="0s" dur="0.8s"
23 values="9;15;9" calcMode="linear"
24 repeatCount="indefinite" />
25 <animate attributeName="fill-opacity" from="0.5" to="0.5"
26 begin="0s" dur="0.8s"
27 values=".5;1;.5" calcMode="linear"
28 repeatCount="indefinite" />
29 </circle>
30 <circle cx="105" cy="15" r="15">
31 <animate attributeName="r" from="15" to="15"
32 begin="0s" dur="0.8s"
33 values="15;9;15" calcMode="linear"
34 repeatCount="indefinite" />
35 <animate attributeName="fill-opacity" from="1" to="1"
36 begin="0s" dur="0.8s"
37 values="1;.5;1" calcMode="linear"
38 repeatCount="indefinite" />
39 </circle>
40 </svg>
41 `;
42
43 $('#square-card-container').html(loadingSVG);
44
45 // Browser detection for Apple Pay compatibility
46 function isSafari() {
47 const userAgent = navigator.userAgent;
48 return /Safari/.test(userAgent) && !/Chrome/.test(userAgent);
49 }
50
51 // Temporarily disable Google Pay and Apple Pay until further testing
52 // These options are now permanently disabled but code is preserved for potential future use
53 var google_pay_enabled = false, // CHARITABLE_SQUARE_VARS.enable_google_pay,
54 apple_pay_enabled = false, // CHARITABLE_SQUARE_VARS.enable_apple_pay && isSafari(), // Only enable if Safari
55 google_pay_button = $('#google-pay-button'),
56 apple_pay_button = $('#apple-pay-button'),
57 show_google_pay_button = google_pay_enabled && google_pay_button.length > 0,
58 show_apple_pay_button = apple_pay_enabled && apple_pay_button.length > 0;
59
60 const wallets = {};
61 let isRecurring = false;
62 let amountBeingInitialized = 1;
63
64 async function initializeCard(payments) {
65
66 const card = await payments.card();
67
68 $('#square-card-container').html('');
69
70 await card.attach('#square-card-container');
71
72 return card;
73 }
74
75 function showWalletProcessing(helper) {
76 $('#charitable-gateway-fields').addClass('square-wallet-processing');
77 helper.show_processing();
78 }
79
80 function cancelWalletProcessing(helper) {
81 helper.cancel_processing();
82 $('#charitable-gateway-fields').removeClass('square-wallet-processing');
83 }
84
85 function setRecurringSupportClass(helper) {
86 // Dynamically find the Square gateway fields container
87 var squareGatewayFields = $('[id^="charitable-gateway-fields-square"]');
88 if (helper.is_recurring_donation()) {
89 squareGatewayFields.addClass('needs-recurring-support');
90 } else {
91 squareGatewayFields.removeClass('needs-recurring-support');
92 }
93 }
94
95 // Handle Wallet Pay payments
96 async function handleWalletPayEvent(event) {
97 event.preventDefault();
98 const helper = event.currentTarget.helper;
99 const type = event.currentTarget.type;
100 showWalletProcessing(helper)
101 helper.clear_errors();
102 helper.validate_amount();
103 helper.validate_required_fields();
104
105 if ( helper.is_recurring_donation() ) {
106 helper.add_error( CHARITABLE_SQUARE_VARS.wallet_not_supported )
107 }
108
109 if (helper.errors.length > 0) {
110 cancelWalletProcessing(helper);
111 return false;
112 }
113
114 try {
115 const result = await wallets[type].tokenize();
116 if (result.status === 'OK') {
117 helper.get_input('square_token').val(result.token);
118 helper.remove_pending_process_by_name('square');
119
120 /* Continue on to process the donation. */
121 $body.trigger( 'charitable:form:process', helper );
122 }
123 } catch (e) {
124 helper.add_error(e.message);
125 helper.remove_pending_process_by_name('square');
126 cancelWalletProcessing(helper);
127 }
128
129 };
130
131 async function initializeWallets(payments, helper) {
132 if (helper.is_recurring_donation()) {
133 return;
134 }
135 try {
136 // Use a default amount of 1.00 if no amount is set
137 const amount = helper.get_amount() ? helper.get_amount().toString() : '1.00';
138 amountBeingInitialized = amount;
139
140 const paymentRequest = payments.paymentRequest({
141 countryCode: CHARITABLE_VARS.country,
142 currencyCode: CHARITABLE_VARS.currency,
143 total: {
144 amount: amount,
145 label: 'Total',
146 },
147 });
148
149 // Initialize both Google Pay and Apple Pay
150 const walletTypes = [];
151 if (show_google_pay_button) {
152 walletTypes.push('googlePay');
153 }
154 if (show_apple_pay_button) {
155 walletTypes.push('applePay');
156
157 }
158 if (walletTypes.length === 0) {
159 return;
160 }
161 const promises = [];
162 for (let walletType of walletTypes) {
163 promises.push(initializeWallet(walletType, paymentRequest, payments, helper));
164 }
165 await Promise.all(promises).catch(e => {
166 if (e.name === 'PaymentMethodUnsupportedError') {
167 console.log(e.message);
168 } else {
169 console.error(e.message);
170 }
171 });
172
173 // Show the wallet buttons container
174 $('#charitable-square-wallet-buttons').show();
175 }
176 catch (e) {
177 console.error('Error initializing wallets:', e);
178 }
179 }
180
181 /*
182 * Initialize a wallet with the passed in paymentRequests
183 */
184 async function initializeWallet(walletType, paymentRequest, payments, helper) {
185 try {
186 // Only destroy and recreate if the wallet doesn't exist or if it's a different amount
187 if (!wallets[walletType] || (walletType === 'applePay' && wallets[walletType]._paymentRequest?.total?.amount !== paymentRequest.total.amount)) {
188 if (wallets[walletType]) {
189 await wallets[walletType].destroy();
190 }
191
192 wallets[walletType] = await payments[walletType](paymentRequest);
193 }
194
195 const id = walletType.split("P")[0] + '-pay-button';
196
197 // For Apple Pay, we need to handle it differently
198 if (walletType === 'applePay') {
199 const applePay = wallets[walletType];
200 if (!applePay) {
201 throw new Error('Apple Pay is not available');
202 }
203
204 let buttonElement = document.getElementById(id);
205
206 // Create the button element if it doesn't exist
207 if (!buttonElement) {
208 buttonElement = document.createElement('button');
209 buttonElement.id = id;
210 buttonElement.className = 'apple-pay-button';
211 document.getElementById('charitable-square-wallet-buttons').appendChild(buttonElement);
212 }
213
214 // Apply Apple Pay button styles
215 buttonElement.style.cssText = `
216 -apple-pay-button-style: black;
217 -apple-pay-button-type: plain;
218 appearance: -apple-pay-button;
219 display: inline-block;
220 width: 200px;
221 height: 40px;
222 border-radius: 5px;
223 padding: 0;
224 margin: 10px auto;
225 background: #000;
226 cursor: pointer;
227 `;
228
229 // Remove any existing click handlers to prevent duplicates
230 buttonElement.replaceWith(buttonElement.cloneNode(true));
231 buttonElement = document.getElementById(id);
232
233 // Add click handler
234 buttonElement.addEventListener('click', async (event) => {
235 event.preventDefault();
236 try {
237 if (!applePay || applePay._destroyed) {
238 throw new Error('Apple Pay session has expired. Please try again.');
239 }
240 const result = await applePay.tokenize();
241 if (result.status === 'OK') {
242 helper.get_input('square_token').val(result.token);
243 helper.remove_pending_process_by_name('square');
244 $body.trigger('charitable:form:process', helper);
245 }
246 } catch (error) {
247 console.error('Apple Pay tokenization error:', error);
248 helper.add_error(error.message);
249 // Reinitialize Apple Pay on error
250 await initializeWallet(walletType, paymentRequest, payments, helper);
251 }
252 });
253
254 buttonElement.helper = helper;
255 buttonElement.type = walletType;
256 buttonElement.style.display = 'block';
257 } else {
258 // For Google Pay and other wallet types
259 const buttonElement = document.getElementById(id);
260 if (buttonElement) {
261 // Check if the wallet is already attached
262 if (!buttonElement.hasAttribute('data-wallet-attached')) {
263 try {
264 await wallets[walletType].attach('#' + id);
265 buttonElement.setAttribute('data-wallet-attached', 'true');
266
267 // Remove any existing click handlers
268 buttonElement.replaceWith(buttonElement.cloneNode(true));
269 const newButtonElement = document.getElementById(id);
270
271 // Add new click handler
272 newButtonElement.addEventListener('click', handleWalletPayEvent);
273 newButtonElement.helper = helper;
274 newButtonElement.type = walletType;
275 newButtonElement.style.display = 'block';
276 } catch (error) {
277 console.error(`Error attaching ${walletType}:`, error);
278 // If attachment fails, try to reinitialize
279 await wallets[walletType].destroy();
280 wallets[walletType] = await payments[walletType](paymentRequest);
281 await wallets[walletType].attach('#' + id);
282 buttonElement.setAttribute('data-wallet-attached', 'true');
283 }
284 }
285 }
286 }
287
288 $('#charitable-square-wallet-buttons').addClass('wallet-enabled').show();
289 $('#charitable-square-card-payment-fields').addClass('wallet-enabled');
290
291 } catch (e) {
292 console.error(`Error initializing ${walletType}:`, e);
293 console.error('Wallet object:', wallets[walletType]);
294 console.error('PaymentRequest:', paymentRequest);
295 console.error('Payments object:', payments);
296
297 // Hide the button if initialization failed
298 const id = walletType.split("P")[0] + '-pay-button';
299 const buttonElement = document.getElementById(id);
300 if (buttonElement) {
301 buttonElement.style.display = 'none';
302 }
303 }
304 }
305
306 // This function tokenizes a payment method.
307 // The 'error' thrown from this async function denotes a failed tokenization,
308 // which is due to buyer error (such as an expired card). It is up to the
309 // developer to handle the error and provide the buyer the chance to fix
310 // their mistakes.
311 async function tokenize(paymentMethod) {
312
313 const tokenResult = await paymentMethod.tokenize();
314
315 if (tokenResult.status === 'OK') {
316 return tokenResult.token;
317 } else {
318 let errorMessage = 'Unable to validate card.Please check your details.';
319 if (tokenResult.errors) {
320 errors = tokenResult.errors.map(function (item) { return item.message });
321 errorMessage = errors.join('. ');
322 }
323 throw new Error(errorMessage);
324 }
325 }
326
327 /**
328 * Initialize the Square handler.
329 *
330 * The 'charitable:form:initialize' event is only triggered once.
331 */
332 $body.on( 'charitable:form:initialize', async function( event, helper ) {
333
334 if (!window.Square) {
335 throw new Error('Square.js failed to load properly');
336 }
337
338 let payments;
339
340 try {
341 payments = window.Square.payments(clientId, locationId);
342 } catch (e) {
343 const statusContainer = document.getElementById(
344 'square-payment-status-container'
345 );
346 document.getElementById('square-card-container').innerHTML = '';
347 statusContainer.className = 'charitable-notice';
348 statusContainer.style.visibility = 'visible';
349 statusContainer.innerHTML = 'Square: ' + e.message;
350 return;
351 }
352
353 let card;
354
355 try {
356 card = await initializeCard(payments);
357 await initializeWallets(payments, helper);
358 } catch (e) {
359 console.error('Initializing Card failed', e);
360 return;
361 }
362
363 // Handle Credit card payments
364 async function handlePaymentMethodSubmission(event, paymentMethod, helper) {
365
366 event.preventDefault();
367
368 // This is not the main donation form, so skip processing.
369 if ( 'make_donation' !== helper.get_input( 'charitable_action' ).val() ) {
370 return;
371 }
372
373 // If we're not using Square, do not process any further
374 if ( !helper.get_payment_method().startsWith('square') ) {
375 return;
376 }
377
378 if (helper.errors.length > 0) {
379 helper.cancel_processing();
380 return;
381 }
382
383 // Pause further processing until we've handled Square response.
384 helper.add_pending_process('square');
385
386 try {
387 const token = await tokenize(paymentMethod);
388 helper.get_input('square_token').val(token);
389
390 const verificationToken = await verifyBuyer(payments, token, helper); // 3DS card verification.
391 helper.get_input('square_verification_token').val(verificationToken);
392
393 helper.remove_pending_process_by_name('square');
394
395 } catch (e) {
396 helper.add_error(e.message);
397 helper.remove_pending_process_by_name('square');
398 }
399 }
400
401 function getBillingContactFromForm() {
402 const billingContact = {};
403 const addressLines = [];
404 if (helper.get_input('address').val()) {
405 addressLines.push(helper.get_input('address').val());
406 }
407 if (helper.get_input('address_2').val()) {
408 addressLines.push(helper.get_input('address_2').val());
409 }
410 const fields = {
411 'givenName': helper.get_input('first_name').val(),
412 'familyName': helper.get_input('last_name').val(),
413 'emailAddress': helper.get_input('email').val(),
414 'addressLines': addressLines,
415 'phone': helper.get_input('phone').val(),
416 'country': helper.get_input('country').val(),
417 'city': helper.get_input('city').val(),
418 'postalCode': helper.get_input('postcode').val(),
419 'region': helper.get_input('state').val()
420 }
421 for (let key in fields) {
422 if (fields[key] && fields[key].length) {
423 billingContact[key] = fields[key];
424 }
425 }
426 return billingContact;
427 }
428
429 /* This function exists to handle SCA payments. */
430 async function verifyBuyer(payments, token, helper) {
431
432 const intent = helper.is_recurring_donation() ? 'STORE' : 'CHARGE';
433
434 const verificationDetails = {
435 amount: helper.get_amount().toString(),
436 billingContact: getBillingContactFromForm(),
437 currencyCode: CHARITABLE_VARS.currency,
438 intent: intent,
439 };
440
441 const verificationResults = await payments.verifyBuyer(
442 token,
443 verificationDetails
444 );
445 return verificationResults.token;
446
447 }
448
449 $body.on(
450 'charitable:form:validate',
451 async function (event, helper) {
452
453 await handlePaymentMethodSubmission(event, card, helper);
454 }
455 );
456
457 $body.on('charitable:form:amount:changed', async function (event, helper) {
458 const amount = helper.get_amount().toString();
459 if (amount !== amountBeingInitialized) {
460 amountBeingInitialized = amount;
461 try {
462 const paymentRequest = payments.paymentRequest({
463 countryCode: CHARITABLE_VARS.country,
464 currencyCode: CHARITABLE_VARS.currency,
465 total: {
466 amount: amount,
467 label: 'Total',
468 },
469 });
470
471 // Reinitialize wallets with new amount, respecting enabled settings
472 const walletTypes = [];
473 if (show_google_pay_button) {
474 walletTypes.push('googlePay');
475 }
476 if (show_apple_pay_button) {
477 walletTypes.push('applePay');
478 }
479
480 const promises = [];
481 for (let walletType of walletTypes) {
482 promises.push(initializeWallet(walletType, paymentRequest, payments, helper));
483 }
484 await Promise.all(promises);
485 } catch (e) {
486 console.error('Error updating wallet amount:', e);
487 }
488 }
489 });
490
491 setRecurringSupportClass(helper);
492 $('#charitable-donation-form').on('change', 'input[name=recurring_donation]', async () => {
493 setRecurringSupportClass(helper);
494 await initializeWallets(payments, helper);
495 });
496
497 } );
498
499 /**
500 * Handle Square iframe height recalculation for modal forms.
501 * This ensures proper sizing when donation forms are loaded in modals.
502 */
503 function triggerSquareResize() {
504 // Check if Square is the active gateway (either by selection or as the only option)
505 const $squareGateway = $('input[name="gateway"][value="square"], input[name="gateway"][value="square_core"]');
506 const $squareFields = $('#charitable-gateway-fields-square, #charitable-gateway-fields-square_core');
507
508 // If Square gateway is selected or Square fields are visible (single gateway scenario)
509 if ($squareGateway.is(':checked') || $squareFields.is(':visible')) {
510 setTimeout(function() {
511 window.dispatchEvent(new Event('resize'));
512 }, 150);
513 }
514 }
515
516 // Listen for modal open events
517 $body.on('charitable:modal:open', function() {
518 triggerSquareResize();
519 });
520
521 // Listen for form loaded events (especially important for AJAX-loaded forms in modals)
522 $body.on('charitable:form:loaded', function(event, helper) {
523 triggerSquareResize();
524 });
525
526 // Listen for modal resize events (in case modal content changes)
527 $body.on('charitable:modal:resize', function() {
528 triggerSquareResize();
529 });
530
531
532 } )( jQuery );