PluginProbe ʕ •ᴥ•ʔ
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / trunk
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz vtrunk
2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8 0.0.9 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.1.0 1.1.1 1.1.2 1.10.0 1.10.1 1.11.0 1.12.0 1.12.1 1.12.2 1.12.3 1.13.0 1.13.1 1.13.2 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.8.0 1.9.0 1.9.1 2.0.0 2.0.1 2.0.2 2.1.0 2.1.1 2.2.0 2.2.1 2.2.2 2.3.0 2.4.0 2.5.0 2.5.2 2.6.0
sureforms / assets / js / payment-manager.js
sureforms / assets / js Last commit date
minified 5 days ago payment-history.js 3 weeks ago payment-manager.js 3 months ago stripe-payment.js 2 months ago
payment-manager.js
371 lines
1 /**
2 * Payment Manager - Minimal payment method switching
3 *
4 * Handles:
5 * - Detecting selected payment method
6 * - Toggling srfm-payment-active class
7 * - Providing unified interface for payment processing
8 *
9 * @package
10 * @since 2.4.0
11 */
12
13 class PaymentManager {
14 constructor( paymentBlock, form ) {
15 this.paymentBlock = paymentBlock;
16 this.blockId = paymentBlock.getAttribute( 'data-block-id' );
17 this.compositeKey = window.srfmGetPaymentKey
18 ? window.srfmGetPaymentKey( form, this.blockId )
19 : this.blockId;
20 this.paymentInput = paymentBlock.querySelector( '.srfm-payment-input' );
21 this.form = form;
22 // AbortController scopes the document-level srfm_payment_type_changed
23 // listener so it is removable on form re-initialization. Pattern mirrors
24 // PAYMENT_UTILITY.listenAmountChanges() in stripe-payment.js.
25 this.abortController = new AbortController();
26 this.init();
27 }
28
29 /**
30 * Tear down listeners owned by this instance. Called by
31 * initializePaymentManagers before replacing this manager so document-level
32 * listeners don't accumulate across form re-initializations.
33 */
34 destroy() {
35 if ( this.abortController ) {
36 this.abortController.abort();
37 }
38 }
39
40 init() {
41 // Set initial active method
42 this.updateActiveMethod();
43
44 // Dispatch initial payment method event
45 this.dispatchPaymentMethodEvent( this.getSelectedMethod(), this.form );
46
47 // Listen for payment method radio changes
48 const radioButtons = this.paymentBlock.querySelectorAll(
49 '.srfm-payment-method-radio'
50 );
51
52 if ( radioButtons.length === 0 ) {
53 console.warn(
54 'PaymentManager: No payment method radio buttons found in block',
55 this.blockId
56 );
57 return;
58 }
59
60 radioButtons.forEach( ( radio ) => {
61 radio.addEventListener( 'change', ( event ) => {
62 this.updateActiveMethod();
63 // Get the parent form from the radio button's position in the DOM
64 const form = event.target.closest( 'form' );
65 // Dispatch event when payment method changes, override this.form with the detected form
66 this.dispatchPaymentMethodEvent(
67 this.getSelectedMethod(),
68 form
69 );
70 } );
71 } );
72
73 // Listen for accordion header clicks
74 const accordionHeaders = this.paymentBlock.querySelectorAll(
75 '.srfm-accordion-header'
76 );
77
78 accordionHeaders.forEach( ( header ) => {
79 header.addEventListener( 'click', () => {
80 this.handleAccordionHeaderClick( header );
81 } );
82 } );
83
84 // BOTH MODE: forward the payment-type change so gateways can reinit if needed.
85 // stripe-payment.js dispatches `srfm_payment_type_changed` on the document.
86 // We keep a per-instance listener so multiple payment blocks on a page don't
87 // step on each other. Scoped to abortController so it can be torn down on
88 // form re-initialization (see destroy()).
89 document.addEventListener(
90 'srfm_payment_type_changed',
91 ( event ) => {
92 if ( event?.detail?.blockId !== this.blockId ) {
93 return;
94 }
95 if ( event?.detail?.form !== this.form ) {
96 return;
97 }
98 // Re-broadcast a payment-method event so accordion gateways re-render
99 // for the new mode (Stripe is already handled by reinitForBlock).
100 this.dispatchPaymentMethodEvent(
101 this.getSelectedMethod(),
102 this.form
103 );
104 },
105 { signal: this.abortController.signal }
106 );
107 }
108
109 /**
110 * Handle accordion header click
111 * Activates the accordion item and selects corresponding radio button
112 * @param {HTMLElement} header - The clicked header element
113 */
114 handleAccordionHeaderClick( header ) {
115 // Find the parent accordion item
116 const accordionItem = header.closest( '.srfm-accordion-item' );
117 if ( ! accordionItem ) {
118 return;
119 }
120
121 // Get the payment method for this accordion item
122 const method = accordionItem.getAttribute( 'data-method' );
123 if ( ! method ) {
124 return;
125 }
126
127 // Find and check the corresponding radio button
128 const radio = this.paymentBlock.querySelector(
129 `.srfm-payment-method-radio[data-method="${ method }"]`
130 );
131
132 if ( radio && ! radio.checked ) {
133 radio.checked = true;
134 // Trigger change event to update active state
135 radio.dispatchEvent( new Event( 'change', { bubbles: true } ) );
136 }
137 }
138
139 /**
140 * Get the currently selected payment method
141 * @return {string} Method ID ('stripe', 'paypal', etc.)
142 */
143 getSelectedMethod() {
144 // Check radio buttons first (user selection)
145 const checkedRadio = this.paymentBlock.querySelector(
146 '.srfm-payment-method-radio:checked'
147 );
148
149 if ( checkedRadio ) {
150 return checkedRadio.getAttribute( 'data-method' );
151 }
152
153 // Fallback to data-selected-method attribute (default)
154 return this.paymentInput.getAttribute( 'data-selected-method' );
155 }
156
157 /**
158 * Update active payment method and toggle classes
159 */
160 updateActiveMethod() {
161 const selectedMethod = this.getSelectedMethod();
162
163 // Update data-selected-method attribute
164 this.paymentInput.setAttribute(
165 'data-selected-method',
166 selectedMethod
167 );
168
169 // Toggle srfm-payment-active class on accordion items
170 const allAccordionItems = this.paymentBlock.querySelectorAll(
171 '.srfm-accordion-item'
172 );
173
174 allAccordionItems.forEach( ( item ) => {
175 const itemMethod = item.getAttribute( 'data-method' );
176
177 if ( itemMethod === selectedMethod ) {
178 item.classList.add( 'srfm-payment-active' );
179 // Update aria-expanded attribute
180 const header = item.querySelector( '.srfm-accordion-header' );
181 if ( header ) {
182 header.setAttribute( 'aria-expanded', 'true' );
183 }
184 } else {
185 item.classList.remove( 'srfm-payment-active' );
186 // Update aria-expanded attribute
187 const header = item.querySelector( '.srfm-accordion-header' );
188 if ( header ) {
189 header.setAttribute( 'aria-expanded', 'false' );
190 }
191 }
192 } );
193 }
194
195 /**
196 * Dispatch payment method change event
197 * Allows other components to react to payment method changes
198 * @param {string} paymentMethod - The selected payment method (stripe, paypal, etc.)
199 * @param {HTMLElement} form - The form element
200 */
201 dispatchPaymentMethodEvent( paymentMethod, form ) {
202 const event = new CustomEvent( 'srfm_payment_method_changed', {
203 detail: {
204 blockId: this.blockId,
205 paymentMethod,
206 form,
207 },
208 bubbles: true,
209 } );
210
211 document.dispatchEvent( event );
212 }
213
214 /**
215 * Process payment for the selected method
216 * Routes to appropriate payment gateway
217 * @param {HTMLElement} form - The form element
218 * @return {Promise<Object>} Payment result
219 */
220 async processPayment( form ) {
221 const selectedMethod = this.getSelectedMethod();
222
223 switch ( selectedMethod ) {
224 case 'stripe':
225 return await this.processStripePayment( form );
226
227 case 'paypal':
228 return await this.processPayPalPayment( form );
229
230 default:
231 return {
232 valid: false,
233 message: `Payment method "${ selectedMethod }" is not supported.`,
234 };
235 }
236 }
237
238 /**
239 * Process Stripe payment
240 * @param {HTMLElement} form - The form element
241 */
242 async processStripePayment( form ) {
243 const paymentResultOnCreateIntent =
244 await window.StripePayment.createPaymentIntentsForForm(
245 form,
246 this.paymentBlock
247 );
248
249 if ( ! paymentResultOnCreateIntent?.valid ) {
250 return {
251 valid: false,
252 message: paymentResultOnCreateIntent.message,
253 };
254 }
255
256 const paymentData = window.srfmPaymentElements?.[ this.compositeKey ];
257
258 if ( paymentData && paymentData.clientSecret ) {
259 const paymentResult = await window.StripePayment.srfmConfirmPayment(
260 this.compositeKey,
261 paymentData,
262 form
263 ).catch( () => {
264 return null;
265 } );
266
267 if ( ! paymentResult?.valid ) {
268 return {
269 valid: false,
270 message: paymentResult.message,
271 paymentResult: null,
272 };
273 }
274
275 return {
276 valid: true,
277 message: window.srfmPaymentUtility?.getStripeStrings(
278 'payment_successful',
279 'Payment successful'
280 ),
281 paymentResult,
282 };
283 }
284
285 return {
286 valid: false,
287 message: 'Payment data not found',
288 };
289 }
290
291 /**
292 * Process PayPal payment
293 */
294 async processPayPalPayment() {
295 // PayPal uses a different flow - it's handled by PayPal buttons
296 // which call the backend directly and store completion status
297
298 // Check if PayPal payment completion data exists for this block.
299 // No blockId fallback needed here — sureforms-pro (which writes to srfmPayPalPayments)
300 // is always updated alongside sureforms (core) since we enforce minimum core version
301 // via SRFM_PRO_CORE_RQD_VER, so both plugins will use compositeKey simultaneously.
302 const paypalPaymentData =
303 window.srfmPayPalPayments?.[ this.compositeKey ];
304
305 // Verify that PayPal payment was completed
306 if ( paypalPaymentData && paypalPaymentData.completed === true ) {
307 // Store the payment result in srfmPaymentElements for form submission
308 if ( ! window.srfmPaymentElements ) {
309 window.srfmPaymentElements = {};
310 }
311
312 window.srfmPaymentElements[ this.compositeKey ] = {
313 paymentMethod: 'paypal',
314 paypalOrderId: paypalPaymentData.orderID,
315 paypalSubscriptionId: paypalPaymentData.subscriptionID,
316 paypalPayerEmail: paypalPaymentData.payerEmail || '',
317 paypalPayerName: paypalPaymentData.payerName || '',
318 completed: true,
319 };
320
321 return {
322 valid: true,
323 message: window.srfmPaymentUtility?.getStripeStrings(
324 'payment_successful',
325 'Payment successful'
326 ),
327 paymentResult: window.srfmPaymentElements[ this.compositeKey ],
328 };
329 }
330
331 return {
332 valid: false,
333 message: window.srfmPaymentUtility?.getStripeStrings(
334 'paypal_payment_incomplete',
335 'Please complete your PayPal payment before submitting the form.'
336 ),
337 };
338 }
339 }
340
341 // Initialize payment managers for all payment blocks
342 function initializePaymentManagers() {
343 document.addEventListener( 'srfm_form_after_initialization', ( event ) => {
344 const form = event.detail.form;
345 const paymentBlocks = form.querySelectorAll( '.srfm-payment-block' );
346
347 paymentBlocks.forEach( ( paymentBlock ) => {
348 const blockId = paymentBlock.getAttribute( 'data-block-id' );
349 const compositeKey = window.srfmGetPaymentKey
350 ? window.srfmGetPaymentKey( form, blockId )
351 : blockId;
352
353 // Store payment manager instance
354 if ( ! window.srfmPaymentManagers ) {
355 window.srfmPaymentManagers = {};
356 }
357
358 // Tear down the previous manager (if any) so its document-level
359 // srfm_payment_type_changed listener is removed before we replace it.
360 window.srfmPaymentManagers[ compositeKey ]?.destroy?.();
361
362 window.srfmPaymentManagers[ compositeKey ] = new PaymentManager(
363 paymentBlock,
364 form
365 );
366 } );
367 } );
368 }
369
370 initializePaymentManagers();
371