| 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 ); |