/** * Payment History Frontend JavaScript. * * Handles overlay panels for subscription details, payment details, * and multi-step subscription cancellation flow. * * @package * @since 2.8.0 */ ( function () { 'use strict'; const config = window.srfm_payment_history || {}; const i18n = config.i18n || {}; const subs = window.srfmDashboardSubs || []; const txs = window.srfmDashboardTxs || []; let cancelState = { name: '', paymentId: null }; let isCancelling = false; // eslint-disable-line prefer-const -- reassigned in confirmCancel() let lastFocusedElement = null; /** * Subscription status label map using translated i18n strings. */ const subStatusMap = { active: i18n.status_active || 'Active', trialing: i18n.status_trialing || 'Trialing', canceled: i18n.status_canceled || 'Cancelled', past_due: i18n.status_past_due || 'Past Due', paused: i18n.status_paused || 'Paused', }; /** * Payment status label map using translated i18n strings. */ const payStatusMap = { succeeded: i18n.status_succeeded || 'Paid', pending: i18n.status_pending || 'Pending', failed: i18n.status_failed || 'Failed', canceled: i18n.status_canceled || 'Cancelled', refunded: i18n.status_refunded || 'Refunded', partially_refunded: i18n.status_partially_refunded || 'Partially Refunded', processing: i18n.status_processing || 'Processing', active: i18n.status_active || 'Active', }; /** * Initialize event listeners using event delegation. */ function init() { const widget = document.querySelector( '.srfm-pd-widget' ); if ( ! widget ) { return; } // Event delegation for rows — handles clicks on subscription and payment rows. widget.addEventListener( 'click', function ( e ) { const subRow = e.target.closest( '.srfm-pd-sub-row' ); if ( subRow ) { openSubDetail( parseInt( subRow.dataset.index, 10 ) ); return; } const payRow = e.target.closest( '.srfm-pd-pay-row' ); if ( payRow ) { openTxDetail( parseInt( payRow.dataset.index, 10 ) ); } } ); widget.addEventListener( 'keydown', function ( e ) { if ( 'Enter' !== e.key && ' ' !== e.key ) { return; } const subRow = e.target.closest( '.srfm-pd-sub-row' ); if ( subRow ) { e.preventDefault(); openSubDetail( parseInt( subRow.dataset.index, 10 ) ); return; } const payRow = e.target.closest( '.srfm-pd-pay-row' ); if ( payRow ) { e.preventDefault(); openTxDetail( parseInt( payRow.dataset.index, 10 ) ); } } ); // Overlay click-outside-to-close and button delegation. document .querySelectorAll( '.srfm-pd-overlay' ) .forEach( function ( overlay ) { overlay.addEventListener( 'click', function ( e ) { if ( e.target === overlay ) { closeOverlay( overlay.id ); return; } // Delegate button clicks inside panels. const btn = e.target.closest( '[data-action]' ); if ( btn ) { handlePanelAction( btn.dataset.action, btn.dataset ); } } ); } ); document.addEventListener( 'keydown', function ( e ) { if ( 'Escape' === e.key ) { closeOverlay( 'srfm-pd-cancel-overlay' ); closeOverlay( 'srfm-pd-sub-overlay' ); closeOverlay( 'srfm-pd-tx-overlay' ); } } ); } /** * Handle delegated panel button actions. * * @param {string} action Action name from data-action attribute. * @param {Object} data Dataset from the button element. */ function handlePanelAction( action, data ) { switch ( action ) { case 'close-sub': closeOverlay( 'srfm-pd-sub-overlay' ); break; case 'close-tx': closeOverlay( 'srfm-pd-tx-overlay' ); break; case 'close-cancel': closeOverlay( 'srfm-pd-cancel-overlay' ); break; case 'open-cancel': closeOverlay( 'srfm-pd-sub-overlay' ); openCancel( data.name || '', parseInt( data.paymentId, 10 ) ); break; case 'confirm-cancel': confirmCancel(); break; case 'finish-cancel': finishCancel(); break; } } /** * Open subscription detail overlay. * * @param {number} index Subscription index. */ function openSubDetail( index ) { const s = subs[ index ]; if ( ! s ) { return; } const isActive = s.canCancel; const statusLabel = subStatusMap[ s.status ] || s.status; const badgeClass = isActive ? 'srfm-pd-badge--active' : 'srfm-pd-badge--cancelled'; let html = '
' + '
' + '
' + esc( i18n.subscription || 'Subscription' ) + '
' + '
' + esc( s.name ) + '
' + '
' + esc( s.form ) + '
' + '
' + '
' + '' + '
' + esc( statusLabel ) + '
' + '
'; html += panelRow( i18n.amount || 'Amount', s.amount ); if ( isActive && s.next ) { html += panelRow( i18n.next_payment || 'Next Payment', s.next ); } else if ( ! isActive ) { if ( s.cancelledOn ) { html += panelRow( i18n.cancelled_on || 'Cancelled On', s.cancelledOn ); } if ( s.accessUntil ) { html += panelRow( i18n.access_until || 'Access Until', s.accessUntil ); } } html += panelRow( i18n.gateway || 'Gateway', s.gateway ); html += panelRow( i18n.started || 'Started', s.started ); html += ''; document.getElementById( 'srfm-pd-sub-panel' ).innerHTML = html; openOverlay( 'srfm-pd-sub-overlay' ); } /** * Open transaction detail overlay. * * @param {number} index Transaction index. */ function openTxDetail( index ) { const t = txs[ index ]; if ( ! t ) { return; } const statusLabel = payStatusMap[ t.status ] || t.status; const badgeClass = 'refunded' === t.status || 'partially_refunded' === t.status ? 'srfm-pd-badge--refunded' : 'succeeded' === t.status || 'active' === t.status ? 'srfm-pd-badge--paid' : 'srfm-pd-badge--pending'; let html = '
' + '
' + '
' + esc( t.id ) + '
' + '
' + esc( t.amount ) + '
' + '
' + esc( t.date ) + '
' + '
' + '
' + '' + '
' + esc( statusLabel ) + '
' + '
'; html += panelRow( i18n.form || 'Form', t.form ); html += panelRow( i18n.type || 'Type', 'subscription' === t.type ? i18n.subscription_payment || 'Subscription Payment' : i18n.one_time_payment || 'One-time Payment' ); html += panelRow( i18n.gateway || 'Gateway', t.gateway ); html += panelRow( i18n.transaction_id || 'Transaction ID', '' + esc( t.txn ) + '', true ); if ( 'subscription' === t.type && t.sub ) { html += '
' + '
' + esc( i18n.parent_subscription || 'Parent Subscription' ) + '
' + '
' + '
' + esc( i18n.plan || 'Plan' ) + '
' + esc( t.sub.name ) + '
' + '
' + esc( i18n.amount || 'Amount' ) + '
' + esc( t.sub.interval ) + '
' + ( t.sub.next ? '
' + esc( i18n.next_payment || 'Next Payment' ) + '
' + esc( t.sub.next ) + '
' : '' ) + '
' + esc( i18n.status || 'Status' ) + '
' + esc( t.sub.status ) + '
' + '
'; } else { html += '
' + esc( i18n.one_time_note || 'One-time payment. No recurring subscription associated.' ) + '
'; } html += ''; document.getElementById( 'srfm-pd-tx-panel' ).innerHTML = html; openOverlay( 'srfm-pd-tx-overlay' ); } // ========================================================================= // Cancel Flow // ========================================================================= function openCancel( name, paymentId ) { cancelState = { name, paymentId }; isCancelling = false; showCancelStep( 1 ); openOverlay( 'srfm-pd-cancel-overlay' ); } function showCancelStep( step ) { const panel = document.getElementById( 'srfm-pd-cancel-panel' ); let html = ''; if ( 1 === step ) { const msg = ( i18n.cancel_confirm_now || 'Your "%s" will be cancelled immediately. You will lose access right away.' ).replace( '%s', cancelState.name ); html = '
' + '
' + svgWarning() + '
' + '

' + esc( i18n.are_you_sure || 'Are you sure?' ) + '

' + '

' + esc( msg ) + '

'; html += ''; } else if ( 2 === step ) { html = '
' + '
' + svgCheck() + '
' + '

' + esc( i18n.subscription_cancelled || 'Subscription Cancelled' ) + '

' + '

' + esc( i18n.cancel_success || 'The subscription has been cancelled successfully.' ) + '

'; html += ''; } panel.innerHTML = html; } function confirmCancel() { if ( isCancelling ) { return; } isCancelling = true; const btn = document.getElementById( 'srfm-pd-confirm-cancel' ); if ( btn ) { btn.disabled = true; btn.textContent = i18n.processing || 'Processing...'; } const formData = new FormData(); formData.append( 'action', 'srfm_frontend_cancel_subscription' ); formData.append( 'nonce', config.nonce ); formData.append( 'payment_id', cancelState.paymentId ); fetch( config.ajax_url, { method: 'POST', body: formData, credentials: 'same-origin', } ) .then( function ( response ) { return response.json(); } ) .then( function ( data ) { if ( data.success ) { showCancelStep( 2 ); } else { isCancelling = false; if ( btn ) { btn.disabled = false; btn.textContent = i18n.yes_cancel || 'Yes, Cancel'; } window.alert( data.data || i18n.error || 'Something went wrong.' ); } } ) .catch( function () { isCancelling = false; if ( btn ) { btn.disabled = false; btn.textContent = i18n.yes_cancel || 'Yes, Cancel'; } window.alert( i18n.error || 'Something went wrong. Please try again.' ); } ); } function finishCancel() { closeOverlay( 'srfm-pd-cancel-overlay' ); window.location.reload(); } // ========================================================================= // Overlay Helpers // ========================================================================= function openOverlay( id ) { const el = document.getElementById( id ); if ( el ) { lastFocusedElement = el.ownerDocument.activeElement; el.setAttribute( 'role', 'dialog' ); el.setAttribute( 'aria-modal', 'true' ); el.classList.add( 'srfm-pd-overlay--open' ); document.body.style.overflow = 'hidden'; // Move focus to the first focusable element in the panel. const focusable = el.querySelector( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); if ( focusable ) { focusable.focus(); } } } function closeOverlay( id ) { const el = document.getElementById( id ); if ( el ) { el.classList.remove( 'srfm-pd-overlay--open' ); el.removeAttribute( 'role' ); el.removeAttribute( 'aria-modal' ); } if ( ! document.querySelector( '.srfm-pd-overlay--open' ) ) { document.body.style.overflow = ''; // Restore focus to the element that triggered the overlay. if ( lastFocusedElement ) { lastFocusedElement.focus(); lastFocusedElement = null; } } } // ========================================================================= // HTML Helpers // ========================================================================= function panelRow( label, value, isHtml ) { return ( '
' + esc( label ) + '' + ( isHtml ? value : esc( value ) ) + '
' ); } function svgBack() { return ''; } function svgCancel() { return ''; } function svgWarning() { return ''; } function svgCheck() { return ''; } function esc( str ) { if ( str === null || str === undefined || str === '' ) { return ''; } const div = document.createElement( 'div' ); div.appendChild( document.createTextNode( String( str ) ) ); return div.innerHTML; } function escAttr( str ) { return esc( str ).replace( /'/g, ''' ).replace( /"/g, '"' ); } // ========================================================================= // Public API (minimal — only needed for external integration) // ========================================================================= window.srfmPH = { openCancel, }; if ( 'loading' === document.readyState ) { document.addEventListener( 'DOMContentLoaded', init ); } else { init(); } }() );