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-history.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-history.js
564 lines
1 /**
2 * Payment History Frontend JavaScript.
3 *
4 * Handles overlay panels for subscription details, payment details,
5 * and multi-step subscription cancellation flow.
6 *
7 * @package
8 * @since 2.8.0
9 */
10
11 ( function () {
12 'use strict';
13
14 const config = window.srfm_payment_history || {};
15 const i18n = config.i18n || {};
16 const subs = window.srfmDashboardSubs || [];
17 const txs = window.srfmDashboardTxs || [];
18
19 let cancelState = { name: '', paymentId: null };
20 let isCancelling = false; // eslint-disable-line prefer-const -- reassigned in confirmCancel()
21 let lastFocusedElement = null;
22
23 /**
24 * Subscription status label map using translated i18n strings.
25 */
26 const subStatusMap = {
27 active: i18n.status_active || 'Active',
28 trialing: i18n.status_trialing || 'Trialing',
29 canceled: i18n.status_canceled || 'Cancelled',
30 past_due: i18n.status_past_due || 'Past Due',
31 paused: i18n.status_paused || 'Paused',
32 };
33
34 /**
35 * Payment status label map using translated i18n strings.
36 */
37 const payStatusMap = {
38 succeeded: i18n.status_succeeded || 'Paid',
39 pending: i18n.status_pending || 'Pending',
40 failed: i18n.status_failed || 'Failed',
41 canceled: i18n.status_canceled || 'Cancelled',
42 refunded: i18n.status_refunded || 'Refunded',
43 partially_refunded: i18n.status_partially_refunded || 'Partially Refunded',
44 processing: i18n.status_processing || 'Processing',
45 active: i18n.status_active || 'Active',
46 };
47
48 /**
49 * Initialize event listeners using event delegation.
50 */
51 function init() {
52 const widget = document.querySelector( '.srfm-pd-widget' );
53 if ( ! widget ) {
54 return;
55 }
56
57 // Event delegation for rows — handles clicks on subscription and payment rows.
58 widget.addEventListener( 'click', function ( e ) {
59 const subRow = e.target.closest( '.srfm-pd-sub-row' );
60 if ( subRow ) {
61 openSubDetail( parseInt( subRow.dataset.index, 10 ) );
62 return;
63 }
64
65 const payRow = e.target.closest( '.srfm-pd-pay-row' );
66 if ( payRow ) {
67 openTxDetail( parseInt( payRow.dataset.index, 10 ) );
68 }
69 } );
70
71 widget.addEventListener( 'keydown', function ( e ) {
72 if ( 'Enter' !== e.key && ' ' !== e.key ) {
73 return;
74 }
75 const subRow = e.target.closest( '.srfm-pd-sub-row' );
76 if ( subRow ) {
77 e.preventDefault();
78 openSubDetail( parseInt( subRow.dataset.index, 10 ) );
79 return;
80 }
81 const payRow = e.target.closest( '.srfm-pd-pay-row' );
82 if ( payRow ) {
83 e.preventDefault();
84 openTxDetail( parseInt( payRow.dataset.index, 10 ) );
85 }
86 } );
87
88 // Overlay click-outside-to-close and button delegation.
89 document
90 .querySelectorAll( '.srfm-pd-overlay' )
91 .forEach( function ( overlay ) {
92 overlay.addEventListener( 'click', function ( e ) {
93 if ( e.target === overlay ) {
94 closeOverlay( overlay.id );
95 return;
96 }
97
98 // Delegate button clicks inside panels.
99 const btn = e.target.closest( '[data-action]' );
100 if ( btn ) {
101 handlePanelAction( btn.dataset.action, btn.dataset );
102 }
103 } );
104 } );
105
106 document.addEventListener( 'keydown', function ( e ) {
107 if ( 'Escape' === e.key ) {
108 closeOverlay( 'srfm-pd-cancel-overlay' );
109 closeOverlay( 'srfm-pd-sub-overlay' );
110 closeOverlay( 'srfm-pd-tx-overlay' );
111 }
112 } );
113 }
114
115 /**
116 * Handle delegated panel button actions.
117 *
118 * @param {string} action Action name from data-action attribute.
119 * @param {Object} data Dataset from the button element.
120 */
121 function handlePanelAction( action, data ) {
122 switch ( action ) {
123 case 'close-sub':
124 closeOverlay( 'srfm-pd-sub-overlay' );
125 break;
126 case 'close-tx':
127 closeOverlay( 'srfm-pd-tx-overlay' );
128 break;
129 case 'close-cancel':
130 closeOverlay( 'srfm-pd-cancel-overlay' );
131 break;
132 case 'open-cancel':
133 closeOverlay( 'srfm-pd-sub-overlay' );
134 openCancel( data.name || '', parseInt( data.paymentId, 10 ) );
135 break;
136 case 'confirm-cancel':
137 confirmCancel();
138 break;
139 case 'finish-cancel':
140 finishCancel();
141 break;
142 }
143 }
144
145 /**
146 * Open subscription detail overlay.
147 *
148 * @param {number} index Subscription index.
149 */
150 function openSubDetail( index ) {
151 const s = subs[ index ];
152 if ( ! s ) {
153 return;
154 }
155
156 const isActive = s.canCancel;
157 const statusLabel = subStatusMap[ s.status ] || s.status;
158 const badgeClass = isActive
159 ? 'srfm-pd-badge--active'
160 : 'srfm-pd-badge--cancelled';
161
162 let html =
163 '<div class="srfm-pd-panel-header">' +
164 '<div>' +
165 '<div class="srfm-pd-panel-id">' +
166 esc( i18n.subscription || 'Subscription' ) +
167 '</div>' +
168 '<div class="srfm-pd-panel-title">' +
169 esc( s.name ) +
170 '</div>' +
171 '<div class="srfm-pd-panel-subtitle">' +
172 esc( s.form ) +
173 '</div>' +
174 '</div>' +
175 '<div class="srfm-pd-panel-header-right">' +
176 '<button type="button" class="srfm-pd-panel-close" data-action="close-sub" aria-label="' + escAttr( i18n.back || 'Close' ) + '">&times;</button>' +
177 '<div class="srfm-pd-panel-header-badge"><span class="srfm-pd-badge ' +
178 badgeClass +
179 '"><span class="srfm-pd-badge-dot"></span>' +
180 esc( statusLabel ) +
181 '</span></div>' +
182 '</div></div>';
183
184 html += panelRow( i18n.amount || 'Amount', s.amount );
185
186 if ( isActive && s.next ) {
187 html += panelRow( i18n.next_payment || 'Next Payment', s.next );
188 } else if ( ! isActive ) {
189 if ( s.cancelledOn ) {
190 html += panelRow(
191 i18n.cancelled_on || 'Cancelled On',
192 s.cancelledOn
193 );
194 }
195 if ( s.accessUntil ) {
196 html += panelRow(
197 i18n.access_until || 'Access Until',
198 s.accessUntil
199 );
200 }
201 }
202
203 html += panelRow( i18n.gateway || 'Gateway', s.gateway );
204 html += panelRow( i18n.started || 'Started', s.started );
205
206 html += '<div class="srfm-pd-panel-footer">';
207 html +=
208 '<button type="button" class="srfm-pd-btn" data-action="close-sub">' +
209 svgBack() +
210 ' ' +
211 esc( i18n.back || 'Back' ) +
212 '</button>';
213
214 if ( isActive ) {
215 html +=
216 '<button type="button" class="srfm-pd-btn srfm-pd-btn--danger" data-action="open-cancel" data-name="' +
217 escAttr( s.name ) +
218 '" data-payment-id="' +
219 parseInt( s.paymentId, 10 ) +
220 '">' +
221 svgCancel() +
222 ' ' +
223 esc( i18n.cancel_subscription || 'Cancel Subscription' ) +
224 '</button>';
225 }
226
227 html += '</div>';
228
229 document.getElementById( 'srfm-pd-sub-panel' ).innerHTML = html;
230 openOverlay( 'srfm-pd-sub-overlay' );
231 }
232
233 /**
234 * Open transaction detail overlay.
235 *
236 * @param {number} index Transaction index.
237 */
238 function openTxDetail( index ) {
239 const t = txs[ index ];
240 if ( ! t ) {
241 return;
242 }
243
244 const statusLabel = payStatusMap[ t.status ] || t.status;
245 const badgeClass =
246 'refunded' === t.status || 'partially_refunded' === t.status
247 ? 'srfm-pd-badge--refunded'
248 : 'succeeded' === t.status || 'active' === t.status
249 ? 'srfm-pd-badge--paid'
250 : 'srfm-pd-badge--pending';
251
252 let html =
253 '<div class="srfm-pd-panel-header">' +
254 '<div>' +
255 '<div class="srfm-pd-panel-id">' +
256 esc( t.id ) +
257 '</div>' +
258 '<div class="srfm-pd-panel-title">' +
259 esc( t.amount ) +
260 '</div>' +
261 '<div class="srfm-pd-panel-subtitle">' +
262 esc( t.date ) +
263 '</div>' +
264 '</div>' +
265 '<div class="srfm-pd-panel-header-right">' +
266 '<button type="button" class="srfm-pd-panel-close" data-action="close-tx" aria-label="' + escAttr( i18n.back || 'Close' ) + '">&times;</button>' +
267 '<div class="srfm-pd-panel-header-badge"><span class="srfm-pd-badge ' +
268 badgeClass +
269 '"><span class="srfm-pd-badge-dot"></span>' +
270 esc( statusLabel ) +
271 '</span></div>' +
272 '</div></div>';
273
274 html += panelRow( i18n.form || 'Form', t.form );
275 html += panelRow(
276 i18n.type || 'Type',
277 'subscription' === t.type
278 ? i18n.subscription_payment || 'Subscription Payment'
279 : i18n.one_time_payment || 'One-time Payment'
280 );
281 html += panelRow( i18n.gateway || 'Gateway', t.gateway );
282 html += panelRow(
283 i18n.transaction_id || 'Transaction ID',
284 '<span class="srfm-pd-txn-id">' +
285 esc( t.txn ) +
286 '</span>',
287 true
288 );
289
290 if ( 'subscription' === t.type && t.sub ) {
291 html +=
292 '<div class="srfm-pd-sub-info-box">' +
293 '<div class="srfm-pd-sub-info-title">' +
294 esc( i18n.parent_subscription || 'Parent Subscription' ) +
295 '</div>' +
296 '<div class="srfm-pd-sub-info-grid">' +
297 '<div><div class="srfm-pd-sub-info-label">' +
298 esc( i18n.plan || 'Plan' ) +
299 '</div><div class="srfm-pd-sub-info-value">' +
300 esc( t.sub.name ) +
301 '</div></div>' +
302 '<div><div class="srfm-pd-sub-info-label">' +
303 esc( i18n.amount || 'Amount' ) +
304 '</div><div class="srfm-pd-sub-info-value">' +
305 esc( t.sub.interval ) +
306 '</div></div>' +
307 ( t.sub.next
308 ? '<div><div class="srfm-pd-sub-info-label">' +
309 esc( i18n.next_payment || 'Next Payment' ) +
310 '</div><div class="srfm-pd-sub-info-value">' +
311 esc( t.sub.next ) +
312 '</div></div>'
313 : '' ) +
314 '<div><div class="srfm-pd-sub-info-label">' +
315 esc( i18n.status || 'Status' ) +
316 '</div><div class="srfm-pd-sub-info-value">' +
317 esc( t.sub.status ) +
318 '</div></div>' +
319 '</div></div>';
320 } else {
321 html +=
322 '<div class="srfm-pd-single-info-box">' +
323 esc(
324 i18n.one_time_note ||
325 'One-time payment. No recurring subscription associated.'
326 ) +
327 '</div>';
328 }
329
330 html += '<div class="srfm-pd-panel-footer">';
331 html +=
332 '<button type="button" class="srfm-pd-btn" data-action="close-tx">' +
333 svgBack() +
334 ' ' +
335 esc( i18n.back || 'Back' ) +
336 '</button>';
337 html += '</div>';
338
339 document.getElementById( 'srfm-pd-tx-panel' ).innerHTML = html;
340 openOverlay( 'srfm-pd-tx-overlay' );
341 }
342
343 // =========================================================================
344 // Cancel Flow
345 // =========================================================================
346
347 function openCancel( name, paymentId ) {
348 cancelState = { name, paymentId };
349 isCancelling = false;
350 showCancelStep( 1 );
351 openOverlay( 'srfm-pd-cancel-overlay' );
352 }
353
354 function showCancelStep( step ) {
355 const panel = document.getElementById( 'srfm-pd-cancel-panel' );
356 let html = '';
357
358 if ( 1 === step ) {
359 const msg = (
360 i18n.cancel_confirm_now ||
361 'Your "%s" will be cancelled immediately. You will lose access right away.'
362 ).replace( '%s', cancelState.name );
363
364 html =
365 '<div class="srfm-pd-cancel-body srfm-pd-cancel-body--confirm">' +
366 '<div class="srfm-pd-cancel-icon srfm-pd-cancel-icon--warning">' +
367 svgWarning() +
368 '</div>' +
369 '<h4>' +
370 esc( i18n.are_you_sure || 'Are you sure?' ) +
371 '</h4>' +
372 '<p>' +
373 esc( msg ) +
374 '</p></div>';
375
376 html +=
377 '<div class="srfm-pd-panel-footer">' +
378 '<button type="button" class="srfm-pd-btn" data-action="close-cancel">' +
379 esc( i18n.keep_subscription || 'Keep Subscription' ) +
380 '</button>' +
381 '<button type="button" class="srfm-pd-btn srfm-pd-btn--danger-fill" id="srfm-pd-confirm-cancel" data-action="confirm-cancel">' +
382 esc( i18n.yes_cancel || 'Yes, Cancel' ) +
383 '</button>' +
384 '</div>';
385 } else if ( 2 === step ) {
386 html =
387 '<div class="srfm-pd-cancel-body srfm-pd-cancel-body--success">' +
388 '<div class="srfm-pd-cancel-icon srfm-pd-cancel-icon--success">' +
389 svgCheck() +
390 '</div>' +
391 '<h4>' +
392 esc( i18n.subscription_cancelled || 'Subscription Cancelled' ) +
393 '</h4>' +
394 '<p>' +
395 esc(
396 i18n.cancel_success ||
397 'The subscription has been cancelled successfully.'
398 ) +
399 '</p></div>';
400
401 html +=
402 '<div class="srfm-pd-panel-footer srfm-pd-panel-footer--center">' +
403 '<button type="button" class="srfm-pd-btn srfm-pd-btn--primary" data-action="finish-cancel">' +
404 esc( i18n.done || 'Done' ) +
405 '</button>' +
406 '</div>';
407 }
408
409 panel.innerHTML = html;
410 }
411
412 function confirmCancel() {
413 if ( isCancelling ) {
414 return;
415 }
416 isCancelling = true;
417
418 const btn = document.getElementById( 'srfm-pd-confirm-cancel' );
419 if ( btn ) {
420 btn.disabled = true;
421 btn.textContent = i18n.processing || 'Processing...';
422 }
423
424 const formData = new FormData();
425 formData.append( 'action', 'srfm_frontend_cancel_subscription' );
426 formData.append( 'nonce', config.nonce );
427 formData.append( 'payment_id', cancelState.paymentId );
428
429 fetch( config.ajax_url, {
430 method: 'POST',
431 body: formData,
432 credentials: 'same-origin',
433 } )
434 .then( function ( response ) {
435 return response.json();
436 } )
437 .then( function ( data ) {
438 if ( data.success ) {
439 showCancelStep( 2 );
440 } else {
441 isCancelling = false;
442 if ( btn ) {
443 btn.disabled = false;
444 btn.textContent = i18n.yes_cancel || 'Yes, Cancel';
445 }
446 window.alert(
447 data.data || i18n.error || 'Something went wrong.'
448 );
449 }
450 } )
451 .catch( function () {
452 isCancelling = false;
453 if ( btn ) {
454 btn.disabled = false;
455 btn.textContent = i18n.yes_cancel || 'Yes, Cancel';
456 }
457 window.alert(
458 i18n.error || 'Something went wrong. Please try again.'
459 );
460 } );
461 }
462
463 function finishCancel() {
464 closeOverlay( 'srfm-pd-cancel-overlay' );
465 window.location.reload();
466 }
467
468 // =========================================================================
469 // Overlay Helpers
470 // =========================================================================
471
472 function openOverlay( id ) {
473 const el = document.getElementById( id );
474 if ( el ) {
475 lastFocusedElement = el.ownerDocument.activeElement;
476 el.setAttribute( 'role', 'dialog' );
477 el.setAttribute( 'aria-modal', 'true' );
478 el.classList.add( 'srfm-pd-overlay--open' );
479 document.body.style.overflow = 'hidden';
480
481 // Move focus to the first focusable element in the panel.
482 const focusable = el.querySelector( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' );
483 if ( focusable ) {
484 focusable.focus();
485 }
486 }
487 }
488
489 function closeOverlay( id ) {
490 const el = document.getElementById( id );
491 if ( el ) {
492 el.classList.remove( 'srfm-pd-overlay--open' );
493 el.removeAttribute( 'role' );
494 el.removeAttribute( 'aria-modal' );
495 }
496 if ( ! document.querySelector( '.srfm-pd-overlay--open' ) ) {
497 document.body.style.overflow = '';
498
499 // Restore focus to the element that triggered the overlay.
500 if ( lastFocusedElement ) {
501 lastFocusedElement.focus();
502 lastFocusedElement = null;
503 }
504 }
505 }
506
507 // =========================================================================
508 // HTML Helpers
509 // =========================================================================
510
511 function panelRow( label, value, isHtml ) {
512 return (
513 '<div class="srfm-pd-panel-row"><span class="srfm-pd-panel-label">' +
514 esc( label ) +
515 '</span><span class="srfm-pd-panel-value">' +
516 ( isHtml ? value : esc( value ) ) +
517 '</span></div>'
518 );
519 }
520
521 function svgBack() {
522 return '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>';
523 }
524
525 function svgCancel() {
526 return '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>';
527 }
528
529 function svgWarning() {
530 return '<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="#d97706" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
531 }
532
533 function svgCheck() {
534 return '<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="#16a34a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
535 }
536
537 function esc( str ) {
538 if ( str === null || str === undefined || str === '' ) {
539 return '';
540 }
541 const div = document.createElement( 'div' );
542 div.appendChild( document.createTextNode( String( str ) ) );
543 return div.innerHTML;
544 }
545
546 function escAttr( str ) {
547 return esc( str ).replace( /'/g, '&#39;' ).replace( /"/g, '&quot;' );
548 }
549
550 // =========================================================================
551 // Public API (minimal — only needed for external integration)
552 // =========================================================================
553
554 window.srfmPH = {
555 openCancel,
556 };
557
558 if ( 'loading' === document.readyState ) {
559 document.addEventListener( 'DOMContentLoaded', init );
560 } else {
561 init();
562 }
563 }() );
564