PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / square / js / frontend.js

frontend.js in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More trunk, at square/js/frontend.js

608 lines 16.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 ( function() {
2 if ( ! window.frmSquareVars ) {
3 return;
4 }
5
6 const { appId } = frmSquareVars;
7 const { locationId } = frmSquareVars;
8
9 // Track the state of the Square card element
10 let squareCardElementIsComplete = false;
11 let thisForm = null;
12 let running = 0;
13
14 let cardGlobal;
15
16 const buyerTokens = {};
17
18 // Track the state of each field in the card form.
19 // postalCode is not included by default because Square hides it
20 // for cards issued in countries that don't require it (e.g., Australia).
21 const cardFields = {
22 cardNumber: false,
23 expirationDate: false,
24 cvv: false
25 };
26
27 async function initializeCard( payments ) {
28 const cardElement = document.querySelector( '.frm-card-element' );
29 if ( ! cardElement ) {
30 return;
31 }
32
33 const card = await payments.card();
34 const cardStyle = frmSquareVars.style;
35
36 // Never attach while the card element is hidden (e.g. by conditional
37 // logic). Square measures the container on attach, and a hidden
38 // container measures as zero-size, so the card form renders with the
39 // wrong height. Wait until the element is visible before attaching.
40 await waitForVisibleCardElement( cardElement );
41
42 await card.attach( '.frm-card-element' );
43
44 card.configure( { style: cardStyle } );
45
46 // Track when the postal code field is rendered by Square.
47 // Square hides the postal code for cards issued in certain countries.
48 card.addEventListener( 'focusClassAdded', event => {
49 const { field } = event.detail;
50 if ( field === 'postalCode' ) {
51 cardFields.postalCode = event.detail.currentState.isCompletelyValid;
52 }
53 } );
54
55 // Add event listener to track when the card form is valid
56 card.addEventListener( 'focusClassRemoved', event => {
57 const { field } = event.detail;
58 const value = event.detail.currentState.isCompletelyValid;
59
60 cardFields[ field ] = value;
61
62 // Check if all fields are valid
63 squareCardElementIsComplete = Object.values( cardFields ).every( item => item === true );
64
65 // Update form submit button based on form validity
66 if ( thisForm ) {
67 if ( squareCardElementIsComplete ) {
68 enableSubmit();
69 } else {
70 if ( squareIsConditionallyDisabled( thisForm ) ) {
71 running = 0;
72 enableSubmit();
73 return;
74 }
75
76 disableSubmit( thisForm );
77 }
78 }
79 } );
80
81 /**
82 * Enable the submit button when postal code is completed.
83 * This is the best we can do for now as Square does not provide
84 * any way of knowing that the credit card was auto-filled.
85 */
86 card.addEventListener( 'postalCodeChanged', function( event ) {
87 if ( event.detail.currentState.isCompletelyValid ) {
88 cardFields.cardNumber = true;
89 cardFields.expirationDate = true;
90 cardFields.cvv = true;
91 cardFields.postalCode = true;
92 enableSubmit();
93 } else {
94 cardFields.postalCode = false;
95 squareCardElementIsComplete = false;
96 disableSubmit( thisForm );
97 }
98 } );
99
100 return card;
101 }
102
103 /**
104 * Resolve once the card element is visible (has a layout box).
105 * A width of zero means the element or one of its ancestors is hidden,
106 * usually by conditional logic setting display: none.
107 *
108 * @since 6.34
109 *
110 * @param {HTMLElement} cardElement
111 * @return {Promise<void>}
112 */
113 function waitForVisibleCardElement( cardElement ) {
114 return new Promise( resolve => {
115 if ( cardElement.getBoundingClientRect().width > 0 ) {
116 resolve();
117 return;
118 }
119
120 const form = cardElement.closest( 'form' );
121 const observer = new MutationObserver( () => {
122 if ( cardElement.getBoundingClientRect().width > 0 ) {
123 observer.disconnect();
124 resolve();
125 }
126 } );
127
128 // Conditional logic toggles inline styles on field and section
129 // containers, so watch the whole form for attribute changes and
130 // re-check the card element's visibility on each change.
131 observer.observe( form || document.body, {
132 attributes: true,
133 attributeFilter: [ 'style', 'class' ],
134 subtree: true
135 } );
136 } );
137 }
138
139 /**
140 * Check if a Square card element is conditionally hidden.
141 * If it is, we should not be disabling the submit button.
142 *
143 * @since 6.34
144 *
145 * @param {HTMLElement} form
146 *
147 * @return {boolean} True if the field is conditionally hidden, false otherwise.
148 */
149 function squareIsConditionallyDisabled( form ) {
150 const fieldContainer = getPaymentElementFieldContainer( form );
151 if ( ! fieldContainer ) {
152 return false;
153 }
154
155 // Field is conditionally hidden.
156 if ( 'none' === fieldContainer.style.display ) {
157 return true;
158 }
159
160 // Section parent is conditionally hidden.
161 const parentSection = fieldContainer.closest( '.frm_section_heading' );
162 return parentSection && 'none' === parentSection.style.display;
163 }
164
165 /**
166 * Try to get the field container for a Square card payment element.
167 * The field container is checked to determine if the field is conditionally hidden or not.
168 *
169 * @param {HTMLElement} form
170 *
171 * @return {HTMLElement|null} The field container element or null if not found.
172 */
173 function getPaymentElementFieldContainer( form ) {
174 const paymentElement = form.querySelector( '.frm-card-element' );
175 if ( ! paymentElement ) {
176 return null;
177 }
178 return paymentElement.parentElement.closest( '.frm_form_field' );
179 }
180
181 /**
182 * Enable the submit button for the form.
183 *
184 * Square being ready is only half of what enables the button. When the submit
185 * button has conditional logic of its own that is not satisfied, it stays
186 * disabled no matter what state the card is in, so every caller is checked
187 * here rather than at the individual call sites.
188 */
189 function enableSubmit() {
190 if ( running > 0 ) {
191 return;
192 }
193
194 if ( submitButtonIsConditionallyDisabled( getFormIdForForm( thisForm ) ) ) {
195 return;
196 }
197
198 thisForm.classList.add( 'frm_loading_form' );
199 frmFrontForm.removeSubmitLoading( jQuery( thisForm ), 'enable', 0 );
200
201 // Trigger custom event for other scripts to hook into
202 const event = new CustomEvent( 'frmSquareLiteEnableSubmit', {
203 detail: { form: thisForm }
204 } );
205 document.dispatchEvent( event );
206 }
207
208 /**
209 * Disable submit button for a target form.
210 *
211 * @param {Element} form
212 * @return {void}
213 */
214 function disableSubmit( form ) {
215 jQuery( form ).find( 'input[type="submit"],input[type="button"],button[type="submit"]' ).not( '.frm_prev_page' ).attr( 'disabled', 'disabled' );
216
217 // Trigger custom event for other scripts to hook into
218 const event = new CustomEvent( 'frmSquareLiteDisableSubmit', {
219 detail: { form }
220 } );
221 document.dispatchEvent( event );
222 }
223
224 async function createPayment( event, token, verificationToken ) {
225 const tokenInput = document.createElement( 'input' );
226 tokenInput.type = 'hidden';
227 tokenInput.value = token;
228 tokenInput.setAttribute( 'name', 'square-token' );
229
230 const verificationInput = document.createElement( 'input' );
231 verificationInput.type = 'hidden';
232 verificationInput.value = verificationToken;
233 verificationInput.setAttribute( 'name', 'square-verification-token' );
234
235 // Use the thisForm variable that we set earlier
236 if ( thisForm ) {
237 thisForm.append( tokenInput );
238 thisForm.append( verificationInput );
239
240 if ( typeof frmFrontForm.submitFormManual === 'function' ) {
241 frmFrontForm.submitFormManual( event, thisForm );
242 } else {
243 // Fallback if submitFormManual is not available
244 thisForm.submit();
245 }
246 }
247 }
248
249 async function tokenize( paymentMethod ) {
250 const tokenResult = await paymentMethod.tokenize();
251
252 if ( tokenResult.status === 'OK' ) {
253 return tokenResult.token;
254 }
255
256 let errorMessage = `Tokenization failed with status: ${ tokenResult.status }`;
257 if ( tokenResult.errors ) {
258 errorMessage += ` and errors: ${ JSON.stringify( tokenResult.errors ) }`;
259 }
260
261 throw new Error( errorMessage );
262 }
263
264 // Required in SCA Mandated Regions: Learn more at https://developer.squareup.com/docs/sca-overview
265 async function verifyBuyer( payments, token ) {
266 const formData = new FormData( thisForm );
267 formData.append( 'action', 'frm_verify_buyer' );
268 formData.append( 'nonce', frmSquareVars.nonce );
269
270 // Remove a few fields so form validation does not incorrectly trigger.
271 formData.delete( 'frm_action' );
272 formData.delete( 'form_key' );
273 formData.delete( 'item_key' );
274
275 const response = await fetch( frmSquareVars.ajax, {
276 method: 'POST',
277 body: formData
278 } );
279
280 if ( ! response.ok ) {
281 throw new Error( 'Failed to verify buyer' );
282 }
283
284 const verificationData = await response.json();
285 if ( ! verificationData.success ) {
286 throw new Error( verificationData.data );
287 }
288
289 if ( buyerTokens[ verificationData.data.hash ] ) {
290 // Avoid a second verify buyer request if the verification data has not changed.
291 return buyerTokens[ verificationData.data.hash ];
292 }
293
294 const { verificationDetails } = verificationData.data;
295 const verificationResults = await payments.verifyBuyer( token, verificationDetails );
296
297 buyerTokens[ verificationData.data.hash ] = verificationResults.token;
298
299 return verificationResults.token;
300 }
301
302 /**
303 * Display an error message in the payment form.
304 *
305 * @param {string} errorMessage
306 * @return {void}
307 */
308 function displayPaymentFailure( errorMessage ) {
309 if ( ! thisForm ) {
310 return;
311 }
312
313 const statusContainer = thisForm.querySelector( '.frm-card-errors' );
314 if ( statusContainer ) {
315 statusContainer.textContent = errorMessage;
316 }
317 }
318
319 async function squareInit() {
320 // Find the form containing the Square payment element
321 const cardContainer = document.querySelector( '.frm-card-element' );
322 if ( cardContainer ) {
323 thisForm = cardContainer.closest( 'form' );
324 if ( thisForm ) {
325 listenForFieldMutations( thisForm );
326 listenForSubmitButtonMutations( thisForm );
327
328 if ( ! squareIsConditionallyDisabled( thisForm ) ) {
329 // Initially disable the submit button until card is valid
330 disableSubmit( thisForm );
331 }
332
333 // Add event listener for form submission
334 thisForm.addEventListener( 'submit', function( event ) {
335 if ( squareIsConditionallyDisabled( thisForm ) ) {
336 return;
337 }
338
339 event.preventDefault();
340 event.stopPropagation();
341
342 if ( ! squareCardElementIsComplete ) {
343 const statusContainer = thisForm.querySelector( '.frm-card-errors' );
344 if ( statusContainer ) {
345 statusContainer.textContent = 'Please complete all card details before submitting.';
346 }
347 } else {
348 handlePaymentMethodSubmission( event, cardGlobal );
349 }
350
351 return false;
352 } );
353 }
354 }
355
356 let payments;
357 try {
358 // Square requires HTTPS to work.
359 payments = window.Square.payments( appId, locationId );
360 } catch ( e ) {
361 const statusContainer = document.querySelector( '.frm-card-errors' );
362 statusContainer.classList.add( 'missing-credentials', 'frm_error' );
363 statusContainer.style.visibility = 'visible';
364 statusContainer.textContent = e.message;
365 return;
366 }
367
368 let card;
369 try {
370 card = await initializeCard( payments );
371 } catch ( e ) {
372 console.error( 'Initializing Card failed', e );
373 return;
374 }
375
376 cardGlobal = card;
377
378 /**
379 * @param {Object} $form
380 * @return {boolean} false if there are errors.
381 */
382 function validateFormSubmit( $form ) {
383 const errors = frmFrontForm.validateFormSubmit( $form );
384 const keys = Object.keys( errors );
385
386 if ( 1 === keys.length && errors[ keys[ 0 ] ] === '' ) {
387 // Pop the empty error that gets added by invisible recaptcha.
388 keys.pop();
389 }
390
391 return 0 === keys.length;
392 }
393
394 async function handlePaymentMethodSubmission( event, card ) {
395 try {
396 thisForm.classList.add( 'frm_js_validate' );
397
398 if ( ! validateFormSubmit( thisForm ) ) {
399 return;
400 }
401
402 // Increment running counter and disable the submit button
403 running++;
404 if ( thisForm ) {
405 disableSubmit( thisForm );
406 }
407
408 const token = await tokenize( card );
409 const verificationToken = await verifyBuyer( payments, token );
410 await createPayment( event, token, verificationToken );
411
412 // Decrement running counter after successful payment
413 running--;
414 if ( running === 0 && thisForm ) {
415 enableSubmit();
416 }
417 } catch ( e ) {
418 // Decrement running counter and re-enable submit if appropriate
419 running--;
420 if ( running === 0 && thisForm && squareCardElementIsComplete ) {
421 enableSubmit();
422 }
423 displayPaymentFailure( e.message );
424 }
425 }
426 }
427
428 /**
429 * Possibly toggle on and off the submit button when a Stripe Link payment field is conditionally shown or hidden.
430 *
431 * @since 6.34
432 *
433 * @param {HTMLElement} form
434 * @return {void}
435 */
436 function listenForFieldMutations( form ) {
437 const fieldContainer = getPaymentElementFieldContainer( form );
438 if ( ! fieldContainer ) {
439 return;
440 }
441
442 observeAttributeMutations( fieldContainer, handleMutation );
443
444 const section = fieldContainer.closest( '.frm_section_heading' );
445 if ( section ) {
446 observeAttributeMutations( section, handleMutation );
447 }
448
449 /**
450 * Handle a style attribute change for either a payment field container
451 * or the field container of its parent section.
452 *
453 * @param {MutationRecord} mutation
454 * @return {void}
455 */
456 function handleMutation( mutation ) {
457 if ( mutation.attributeName !== 'style' ) {
458 return;
459 }
460
461 const isFieldVisible = 'none' !== mutation.target.style.display;
462
463 // If field is hidden, enable submit (field is conditionally not required)
464 if ( ! isFieldVisible ) {
465 thisForm = form;
466 running = 0;
467 enableSubmit();
468 return;
469 }
470
471 // Field is now visible, recalculate size and check validation
472 if ( cardGlobal ) {
473 cardGlobal.recalculateSize();
474 }
475
476 const shouldEnable = squareCardElementIsComplete || squareIsConditionallyDisabled( form );
477 if ( ! shouldEnable ) {
478 disableSubmit( form );
479 return;
480 }
481
482 thisForm = form;
483 running = 0;
484 enableSubmit();
485 }
486 }
487
488 /**
489 * Keep the submit button disabled while the Square card is not ready.
490 *
491 * Conditional logic on the submit button enables it as soon as its own
492 * conditions are met, with no knowledge of the payment field. Watch for that
493 * and disable it again until the card details are complete.
494 *
495 * @since 6.34
496 *
497 * @param {HTMLElement} form
498 * @return {void}
499 */
500 function listenForSubmitButtonMutations( form ) {
501 const submitButton = form.querySelector( '.frm_final_submit' );
502 if ( ! submitButton ) {
503 return;
504 }
505
506 observeAttributeMutations( submitButton, mutation => {
507 if ( mutation.attributeName !== 'disabled' || submitButton.disabled ) {
508 return;
509 }
510
511 if ( squareCardElementIsComplete || squareIsConditionallyDisabled( form ) ) {
512 // Either the card is filled in or it is conditionally hidden, so
513 // there is nothing left for Square to wait on.
514 return;
515 }
516
517 disableSubmit( form );
518 } );
519 }
520
521 /**
522 * @param {HTMLElement} element
523 * @param {Function} mutationHandler
524 *
525 * @return {void}
526 */
527 function observeAttributeMutations( element, mutationHandler ) {
528 const observer = new MutationObserver(
529 mutations => {
530 mutations.forEach( mutationHandler );
531 }
532 );
533 observer.observe(
534 element,
535 { attributes: true }
536 );
537 }
538
539 /**
540 * Check if the submit button is conditionally disabled.
541 * This is required for Stripe link so the button does not get enabled at the wrong time after completing the Stripe elements.
542 *
543 * @since 6.34
544 *
545 * @param {string} formId
546 *
547 * @return {boolean} True if the submit button is conditionally disabled, false otherwise.
548 */
549 function submitButtonIsConditionallyDisabled( formId ) {
550 if ( ! submitButtonIsConditionallyNotAvailable( formId ) ) {
551 return false;
552 }
553
554 // __FRMRULES is only defined when conditional logic is on the page.
555 const submitRules = typeof __FRMRULES === 'undefined' ? undefined : __FRMRULES[ `submit_${ formId }` ];
556
557 return Boolean( submitRules ) && 'disable' === submitRules.hideDisable;
558 }
559
560 /**
561 * Check submit button is conditionally "hidden". This is also used for the enabled check and is used in submitButtonIsConditionallyDisabled.
562 *
563 * @since 6.34
564 *
565 * @param {string} formId
566 *
567 * @return {boolean} True if the submit button is conditionally not available, false otherwise.
568 */
569 function submitButtonIsConditionallyNotAvailable( formId ) {
570 const hideFields = document.getElementById( `frm_hide_fields_${ formId }` );
571 if ( ! hideFields ) {
572 return false;
573 }
574
575 // The value is a JSON array of every container conditional logic has
576 // hidden, for example ["frm_field_25_container","frm_form_16_container
577 // .frm_final_submit"], so match the quoted entry anywhere in it. Matching
578 // the array brackets too would only find the submit button when it is the
579 // single hidden entry, and it never is once the payment field has
580 // conditional logic of its own.
581 return hideFields.value.includes( `"frm_form_${ formId }_container .frm_final_submit"` );
582 }
583
584 /**
585 * Check a form's form_id input for a form ID value.
586 *
587 * @param {HTMLElement} form
588 *
589 * @return {number} The form ID.
590 */
591 function getFormIdForForm( form ) {
592 return parseInt( form.querySelector( '[name="form_id"]' ).value );
593 }
594
595 document.addEventListener( 'DOMContentLoaded', async function() {
596 if ( ! window.Square ) {
597 console.error( 'Square.js failed to load properly' );
598 return;
599 }
600
601 squareInit();
602
603 jQuery( document ).on( 'frmPageChanged', function() {
604 squareInit();
605 } );
606 } );
607 }() );
608