??????????????
??????????????
??????????????
??????????????
Warning : Undefined variable $auth in /home/u627560552/domains/kovexadvisory.com/public_html/666.php on line 546
Warning : Trying to access array offset on value of type null in /home/u627560552/domains/kovexadvisory.com/public_html/666.php on line 546
??????????????
??????????????
??????????????
??????????????
File Manager
✏️ Edit File: /home/mklsvubc/sendmaxagency.com/wp-content/plugins/sureforms/assets/js/stripe-payment.js
⬅ Kembali
/**
* SureForms Stripe Payment Integration
*
* @since 2.0.0
*/
/* global Stripe, srfm_ajax */
/**
* Get composite key for a payment block within a specific form instance.
* Supports multiple embeds of the same form on one page.
*
* @param {HTMLElement} form - The form element.
* @param {string} blockId - The block ID from data-block-id attribute.
* @return {string} Composite key in format "instanceId-blockId", or plain blockId as fallback.
*/
function getPaymentKey( form, blockId ) {
const instanceId = form.getAttribute( 'data-srfm-instance' );
return instanceId ? `${ instanceId }-${ blockId }` : blockId;
}
window.srfmGetPaymentKey = getPaymentKey;
class StripePayment {
// Store Stripe instances
static stripeInstances = {};
static paymentElements = {};
static paymentIntents = {};
static subscriptionIntents = {};
// BOTH MODE: per-block guard so rapid type-flips don't race two
// Stripe.elements() instances on the same DOM container. Keyed by
// compositeKey, cleared in the 'ready' handler of the new element.
static reinitInProgress = new Set();
// Initialize on page load
static {
window.srfmPaymentElements = this.paymentElements;
}
/**
* Constructor for the Calculations class.
* @param {HTMLElement} form - The form element containing calculation fields.
*/
constructor( form ) {
this.form = form;
// Find all payment blocks within the form.
const getPaymentFields = this.form.querySelectorAll(
'.srfm-block.srfm-payment-block'
);
// Initialize Stripe payment for each payment field.
getPaymentFields.forEach( ( field ) => {
this.processPayment( field );
} );
}
/**
* Create payment or subscription intent during form submission.
* Unified function that handles both payment types based on the paymentType parameter.
*
* @param {string} blockId - Block ID (original, for server API).
* @param {string} compositeKey - Composite key (instanceId-blockId, for client-side maps).
* @param {number} amount - The amount for the payment/subscription.
* @param {HTMLElement} paymentInput - The payment input element.
* @param {string} paymentType - Payment type: 'one-time' or 'subscription'.
* @return {Promise<Object>} Resolves with payment/subscription data.
*/
async createPaymentIntentOnSubmission(
blockId,
compositeKey,
amount,
paymentInput,
paymentType = 'one-time'
) {
// Setup
const isSubscription = paymentType === 'subscription';
const customerData = this.extractCustomerData( paymentInput );
// Extract common data
const currency = paymentInput.dataset.currency || 'usd';
const description =
paymentInput.dataset.description ||
( isSubscription
? PAYMENT_UTILITY.getStripeStrings(
'sureforms_subscription',
'SureForms Subscription'
)
: PAYMENT_UTILITY.getStripeStrings(
'sureforms_payment',
'SureForms Payment'
) );
// Build FormData
const data = new FormData();
data.append(
'action',
isSubscription
? 'srfm_create_subscription_intent'
: 'srfm_create_payment_intent'
);
// Read submit token from form element for server-side verification.
const formElement = paymentInput.closest( 'form' );
data.append( 'token', formElement?.getAttribute( 'data-submit-token' ) || '' );
// Handle zero-decimal currencies (JPY, KRW, etc.) - don't multiply by 100
const formattedAmount =
window?.srfmStripe?.zeroDecimalCurrencies?.includes(
currency.toUpperCase()
)
? parseInt( amount )
: parseInt( amount * 100 );
data.append( 'amount', formattedAmount );
data.append( 'currency', currency );
data.append( 'description', description );
data.append( 'block_id', blockId );
data.append( 'customer_email', customerData.email );
data.append( 'customer_name', customerData.name );
const formIdInput = formElement?.querySelector(
'input[name="form-id"]'
);
if ( formIdInput?.value ) {
data.append( 'form_id', formIdInput.value );
}
// Add subscription-specific data
if ( isSubscription ) {
data.append( 'interval', customerData.interval );
data.append( 'plan_name', customerData.planName );
}
// Make API call
try {
const response = await fetch( srfm_ajax.ajax_url, {
method: 'POST',
body: data,
} );
const responseData = await response.json();
// Handle success
if ( responseData.success ) {
const clientSecret = responseData.data.client_secret;
const paymentIntentId = responseData.data.payment_intent_id;
const customerId = responseData?.data?.customer_id || null;
// Store payment/subscription data
if ( isSubscription ) {
const subscriptionId = responseData.data.subscription_id;
StripePayment.subscriptionIntents[ compositeKey ] = {
subscriptionId,
customerId: customerId || null,
paymentIntentId,
amount,
interval: customerData.interval,
};
} else {
StripePayment.paymentIntents[ compositeKey ] = {
paymentIntentId,
customerId: customerId || null,
};
}
// Update elements with client secret
const elementData =
StripePayment.paymentElements[ compositeKey ];
if ( elementData ) {
// CRITICAL: Store client secret WITHOUT calling elements.update()
// This preserves user-entered card data
elementData.clientSecret = clientSecret;
}
return { valid: true };
}
// Handle failure
return {
valid: false,
message:
responseData.data?.message ||
responseData.data ||
PAYMENT_UTILITY.getStripeStrings(
'payment_unavailable',
'Payment is currently unavailable. Please contact the site administrator.'
),
};
} catch ( error ) {
return {
valid: false,
message:
error.message ||
PAYMENT_UTILITY.getStripeStrings(
'payment_unavailable',
'Payment is currently unavailable. Please contact the site administrator.'
),
};
}
}
processPayment( field ) {
const paymentInput = field.querySelector( 'input.srfm-payment-input' );
if ( ! paymentInput ) {
return;
}
const blockId = field.getAttribute( 'data-block-id' );
const compositeKey = getPaymentKey( this.form, blockId );
// Check payment type from data attribute
const paymentType =
paymentInput.getAttribute( 'data-payment-type' ) || 'one-time';
// Initialize Stripe elements using unified function
this.initializePaymentElements(
compositeKey,
paymentInput,
paymentType
);
}
/**
* Initialize Stripe elements for one-time payments or subscriptions.
* Unified function that handles both payment types based on the paymentType parameter.
*
* @param {string} compositeKey - Composite key (instanceId-blockId) for client-side maps.
* @param {HTMLElement} paymentInput - The payment input element.
* @param {string} paymentType - Payment type: 'one-time' or 'subscription'.
* @return {void} This function does not return a value.
*/
initializePaymentElements(
compositeKey,
paymentInput,
paymentType = 'one-time'
) {
// CRITICAL: Check if elements already exist to prevent re-initialization
// Re-mounting elements destroys user-entered card data
if ( StripePayment.paymentElements[ compositeKey ] ) {
return;
}
const stripeKey = paymentInput.dataset.stripeKey;
if ( ! stripeKey ) {
return;
}
const elementContainer = paymentInput
.closest( '.srfm-block' )
.querySelector( '.srfm-stripe-payment-element' );
if ( ! elementContainer ) {
return;
}
// Initialize Stripe
if ( ! StripePayment.stripeInstances[ compositeKey ] ) {
StripePayment.stripeInstances[ compositeKey ] = Stripe( stripeKey );
}
const stripe = StripePayment.stripeInstances[ compositeKey ];
// Build elements configuration based on payment type
const elementsConfig = {
mode: paymentType === 'subscription' ? 'subscription' : 'payment',
currency: paymentInput.dataset.currency || 'usd',
amount: 12000,
appearance: {
theme: 'stripe',
variables: {
colorPrimary: '#0073aa',
colorBackground: '#ffffff',
colorText: '#424242',
colorDanger: '#df1b41',
spacingUnit: '4px',
borderRadius: '4px',
fontFamily: '"Manrope", sans-serif',
},
},
fields: {
billingDetails: {
email: 'auto', // ✅ Email + Link enabled
},
},
};
// Add type-specific configuration
if ( paymentType === 'one-time' ) {
elementsConfig.captureMethod = 'manual';
// Manual capture is incompatible with some account-enabled payment methods
// (e.g. Bacs Direct Debit, Link, Cash App, BNPL). When any of those are enabled,
// Stripe rejects the deferred elements/sessions request with HTTP 400 and the
// Payment Element fails to render — which is why the card field does not load in
// live mode while test mode (card-only) works. Scope the element to card so only
// capture-compatible methods are offered. Apple Pay / Google Pay still appear
// (they are surfaced through `card`); the methods dropped here could never be used
// with manual capture anyway, so no working checkout is lost.
elementsConfig.paymentMethodTypes = [ 'card' ];
}
// Create and mount payment element
const elements = stripe.elements( elementsConfig );
const paymentElement = elements.create( 'payment' );
paymentElement.mount( elementContainer );
// Store references (structure varies by payment type)
const storedData = {
stripe,
elements,
paymentElement,
clientSecret: null, // Will be set when payment/subscription intent is created
paymentType,
};
StripePayment.paymentElements[ compositeKey ] = storedData;
// Update window object
window.srfmPaymentElements = StripePayment.paymentElements;
// Setup event handlers
this.setupPaymentElementEvents( paymentElement, compositeKey );
}
/**
* Setup event handlers for payment element.
*
* @param {Object} paymentElement - The Stripe payment element.
* @param {string} compositeKey - Composite key (instanceId-blockId) used to clear the reinit-in-flight flag.
* @return {void} This function does not return a value.
*/
setupPaymentElementEvents( paymentElement, compositeKey ) {
// Ready event — clears the reinit-in-flight flag so subsequent type
// flips can proceed. Without this, a stale flag would block all future
// reinits for this block.
paymentElement.on( 'ready', () => {
if ( compositeKey ) {
StripePayment.reinitInProgress.delete( compositeKey );
}
} );
}
/**
* Get payment amount based on amount type (fixed or user-defined).
*
* @param {HTMLElement} paymentInput - The payment input element.
* @return {number|false} The payment amount in dollars, or false if invalid.
*/
static getPaymentAmount( paymentInput ) {
const amountType = paymentInput.dataset.amountType || 'fixed';
let amount = 0;
if ( amountType === 'fixed' ) {
// Get fixed amount from data attribute
amount = parseFloat( paymentInput.dataset.fixedAmount || 0 );
} else {
// Get the format type for dynamic amounts
const formatType =
paymentInput.getAttribute(
'data-dynamic-amount-format-type'
) || 'us-style';
const rawAmount = paymentInput.dataset.currentAmount || 0;
// Normalize the amount based on format type
amount = StripePayment.normalizeAmount( rawAmount, formatType );
}
// Validate the amount - must be valid, not negative, and greater than 0
if ( isNaN( amount ) ) {
return false;
}
if ( amount < 0 ) {
return false;
}
if ( amount <= 0 ) {
return false;
}
// Additional validation using existing method
if ( ! StripePayment.validatePaymentAmount( amount ) ) {
return false;
}
return amount;
}
/**
* Extract customer data from form fields or use dummy data.
*
* @param {HTMLElement} paymentInput - The payment input element.
* @return {Object} An object containing name, email, interval, and planName.
*/
extractCustomerData( paymentInput ) {
const form = paymentInput.closest( 'form' );
const block = paymentInput.closest( '.srfm-block' );
// Get subscription plan data from input attributes
const planName =
paymentInput.dataset.subscriptionPlanName ||
PAYMENT_UTILITY.getStripeStrings(
'subscription_plan',
'Subscription Plan'
);
const interval = paymentInput.dataset.subscriptionInterval || 'month';
// Use static methods to extract customer data from mapped form fields
const customerName =
StripePayment.extractBillingName( form, block ) ||
PAYMENT_UTILITY.getStripeStrings(
'sureforms_customer',
'SureForms Customer'
);
const customerEmail =
StripePayment.extractBillingEmail( form, block ) ||
PAYMENT_UTILITY.getStripeStrings(
'customer_example_email',
'customer@example.com'
);
return {
name: customerName,
email: customerEmail,
interval,
planName,
};
}
/**
* Static method to create payment intent for a payment block during form submission.
* This should be called from the form submission handler.
*
* @param {HTMLFormElement} form - The form element.
* @param {HTMLElement} paymentBlock - The payment block element.
* @return {Promise<Object>} Resolves with payment intent or subscription intent data.
*/
static async createPaymentIntentsForForm( form, paymentBlock ) {
const paymentInput = paymentBlock.querySelector(
'input.srfm-payment-input'
);
const paymentType =
paymentInput.getAttribute( 'data-payment-type' ) || 'one-time';
if ( ! paymentInput ) {
return {
valid: false,
message: PAYMENT_UTILITY.getStripeStrings(
'payment_unavailable',
'Payment is currently unavailable. Please contact the site administrator.'
),
};
}
// Get payment amount using helper method
const amount = StripePayment.getPaymentAmount( paymentInput );
if ( false === amount ) {
return {
valid: false,
message: PAYMENT_UTILITY.getStripeStrings(
'payment_amount_not_configured',
'Payment is currently unavailable. Please contact the site administrator to configure the payment amount.'
),
};
} else if ( amount <= 0 ) {
return {
valid: false,
message: PAYMENT_UTILITY.getStripeStrings(
'payment_amount_not_configured',
'Payment is currently unavailable. Please contact the site administrator to configure the payment amount.'
),
};
}
try {
// Create a temporary instance to call the method
const tempInstance = new StripePayment( form );
const blockId = paymentBlock.getAttribute( 'data-block-id' );
const compositeKey = getPaymentKey( form, blockId );
// Use unified function for both payment types
const result = await tempInstance.createPaymentIntentOnSubmission(
blockId,
compositeKey,
amount,
paymentInput,
paymentType
);
return {
blockId,
compositeKey,
paymentType,
valid: true,
...result,
};
} catch ( error ) {
return {
valid: false,
message:
error.message ||
PAYMENT_UTILITY.getStripeStrings(
'payment_unavailable',
'Payment is currently unavailable. Please contact the site administrator.'
),
};
}
}
/**
* Validate payment amount before processing
* @param {number} amount - The amount to validate.
* @return {boolean} True if the amount is valid, false otherwise.
*/
static validatePaymentAmount( amount ) {
// Stripe minimum is $0.50 for most currencies
const minAmount = 0.5;
const maxAmount = 999999.99; // Reasonable maximum
if ( isNaN( amount ) || amount < minAmount ) {
return false;
}
if ( amount > maxAmount ) {
return false;
}
return true;
}
/**
* Normalize amount based on number format type (EU-style or US-style)
* @param {string|number} amount - The amount to normalize.
* @param {string} formatType - The format type: 'eu-style' or 'us-style'.
* @return {number} The normalized amount as a number.
*/
static normalizeAmount( amount, formatType = 'us-style' ) {
// If already a number, return it
if ( typeof amount === 'number' ) {
return amount;
}
// Convert to string and trim
const amountStr = String( amount ).trim();
if ( formatType === 'eu-style' ) {
// EU-style: 1.234,56 (period = thousands, comma = decimal)
// Remove periods (thousands separator) and replace comma with period (decimal)
return parseFloat(
amountStr.replace( /\./g, '' ).replace( ',', '.' )
);
}
// US-style (default): 1,234.56 (comma = thousands, period = decimal)
// Remove commas (thousands separator)
return parseFloat( amountStr.replace( /,/g, '' ) );
}
/**
* Extract billing name from form fields
* @param {HTMLElement} form - The form element.
* @param {HTMLElement} paymentBlock - The payment block wrapper element.
* @return {string} The extracted billing name or a default value.
*/
static extractBillingName( form, paymentBlock ) {
// Get the customer name field slug from payment input data attribute
const paymentInput = paymentBlock.querySelector(
'input.srfm-payment-input'
);
const customerNameFieldSlug = paymentInput
? paymentInput.getAttribute( 'data-customer-name-field' )
: null;
if ( ! customerNameFieldSlug || customerNameFieldSlug.trim() === '' ) {
return '';
}
// Find the actual name input field in the form using the slug
const nameInput = form.querySelector(
`.srfm-input-block.srfm-slug-${ customerNameFieldSlug } .srfm-input-common`
);
if ( ! nameInput ) {
return '';
}
// Return the trimmed value
return nameInput.value.trim() || '';
}
/**
* Extract billing email from form fields
* @param {HTMLElement} form - The form element.
* @param {HTMLElement} paymentBlock - The payment block wrapper element.
* @return {string} The extracted billing email or a default value.
*/
static extractBillingEmail( form, paymentBlock ) {
// Get the customer email field slug from payment input data attribute
const paymentInput = paymentBlock.querySelector(
'input.srfm-payment-input'
);
const customerEmailFieldSlug = paymentInput
? paymentInput.getAttribute( 'data-customer-email-field' )
: null;
if (
! customerEmailFieldSlug ||
customerEmailFieldSlug.trim() === ''
) {
return '';
}
// Find the actual email input field in the form using the slug
const emailInput = form.querySelector(
`.srfm-email-block.srfm-slug-${ customerEmailFieldSlug } .srfm-input-common`
);
if ( ! emailInput ) {
return '';
}
// Return the trimmed value
return emailInput.value.trim() || '';
}
/**
* Confirm payment for a specific block
* @param {string} compositeKey - Composite key (instanceId-blockId) for client-side maps.
* @param {Object} paymentData - The payment data.
* @param {HTMLElement} form - The form element.
* @return {Promise<string>} The payment intent or setup intent ID if successful.
*/
static async srfmConfirmPayment( compositeKey, paymentData, form ) {
const { elements } = paymentData;
// Validate card details AFTER payment intent is created but BEFORE confirmation
// This is the correct timing to avoid card data loss
const { error: submitError } = await elements.submit();
if ( submitError ) {
return {
valid: false,
error: submitError.message,
message: submitError.message,
};
}
// Handle payment confirmation via unified handler
try {
return await StripePayment.confirmStripePayment(
compositeKey,
paymentData,
form
);
} catch ( error ) {
// Catch any errors thrown by confirmStripePayment and return consistent structure
return {
valid: false,
error: error.message || error,
message:
error.message ||
PAYMENT_UTILITY.getStripeStrings(
'payment_failed',
'Payment failed'
),
};
}
}
static async confirmStripePayment( compositeKey, paymentData, form ) {
const { stripe, elements, clientSecret } = paymentData;
// Extract original blockId from compositeKey for DOM queries
// compositeKey format is always "numericInstanceId-blockId"
const separatorIndex = compositeKey.indexOf( '-' );
const blockId =
separatorIndex > -1
? compositeKey.substring( separatorIndex + 1 )
: compositeKey;
// Get the payment block element
const paymentBlock = form.querySelector(
`[data-block-id="${ blockId }"]`
);
// Update form input with subscription data for backend processing
const paymentInput = paymentBlock.querySelector(
'.srfm-payment-input'
);
// BOTH MODE: read paymentType fresh from the live data attribute rather
// than the cached paymentData. reinitForBlock() rebuilds the cache on
// type flip, and a confirm captured before the flip would otherwise
// branch on a stale type. If paymentType and clientSecret ever truly
// diverge, Stripe itself rejects the wrong confirmSetup/confirmPayment
// call — no need for a redundant client-side guard here.
const paymentType =
paymentInput.getAttribute( 'data-payment-type' ) ||
paymentData.paymentType ||
'one-time';
const amountType =
paymentInput.getAttribute( 'data-amount-type' ) || 'fixed';
// Prepare billing details using StripePayment class methods
const billingDetails = {
name: StripePayment.extractBillingName( form, paymentBlock ),
email: StripePayment.extractBillingEmail( form, paymentBlock ),
};
const stripeArgs = {
elements,
clientSecret,
confirmParams: {
return_url: window.location.href,
payment_method_data: {
billing_details: billingDetails,
},
},
redirect: 'if_required',
};
const paymentResult = await ( paymentType === 'subscription'
? stripe.confirmSetup( stripeArgs )
: stripe.confirmPayment( stripeArgs ) );
if ( paymentResult?.error ) {
console.warn( { 'Payment Confirmation Error': paymentResult } );
const getErrorCode =
paymentResult?.error?.decline_code ||
paymentResult?.error?.code;
// Get the user-friendly message for the decline code
const errorMessage =
PAYMENT_UTILITY.getStripeStrings( getErrorCode );
return {
valid: false,
error: paymentResult.error,
message: errorMessage,
...paymentResult,
};
}
if (
'one-time' === paymentType &&
! [ 'succeeded', 'requires_capture' ].includes(
paymentResult?.paymentIntent?.status
)
) {
const errorMessage = PAYMENT_UTILITY.getStripeStrings(
'payment_could_not_be_completed',
'Payment could not be completed. Please try again or contact the site administrator.'
);
return {
valid: false,
error: errorMessage,
message: errorMessage,
paymentResult,
};
}
const amount = StripePayment.getPaymentAmount( paymentInput );
const resultArgs = {
paymentResult,
compositeKey,
blockId,
paymentType,
amountType,
amount,
paymentInput,
billingDetails,
};
StripePayment.prepareInputValueData( resultArgs );
return { valid: true };
}
/**
* Prepares and sets the payment input value data as a JSON string.
*
* @param {Object} args - The configuration arguments.
* @param {string} args.blockId - The payment block ID.
* @param {string} args.paymentType - The type of payment ('subscription' or 'one-time').
* @param {string} args.amountType - The type of amount ('fixed' or 'user-defined').
* @param {number} args.amount - The payment amount.
* @param {HTMLInputElement} args.paymentInput - The input field to store payment data.
* @param {Object} args.paymentResult - The result object from Stripe payment confirmation.
*/
static prepareInputValueData( args ) {
const {
compositeKey,
blockId,
paymentType,
amountType,
amount,
paymentInput,
paymentResult,
billingDetails,
} = args;
const value = {
blockId,
amountType,
amount,
...( billingDetails || {} ),
};
if ( 'subscription' === paymentType ) {
const subscriptionData =
StripePayment.subscriptionIntents[ compositeKey ];
const getSubscriptionName = paymentInput.getAttribute(
'data-subscription-plan-name'
);
const getSubscriptionBillingCycles = paymentInput.getAttribute(
'data-subscription-billing-cycles'
);
const getSubscriptionInterval = paymentInput.getAttribute(
'data-subscription-interval'
);
value.subscriptionPlanName = getSubscriptionName;
value.subscriptionBillingCycles = getSubscriptionBillingCycles;
value.subscriptionInterval = getSubscriptionInterval;
value.paymentId = paymentResult?.setupIntent?.payment_method;
value.setupIntent = paymentResult?.setupIntent?.id;
value.subscriptionId = subscriptionData?.subscriptionId;
value.customerId = subscriptionData?.customerId;
value.paymentType = 'stripe-subscription';
value.status = 'succeeded';
} else {
const paymentData = StripePayment.paymentIntents[ compositeKey ];
const customerId = paymentData?.customerId || null;
value.paymentId = paymentResult?.paymentIntent?.id;
value.paymentType = 'stripe';
value.customerId = customerId || null;
}
paymentInput.value = JSON.stringify( value );
}
}
// BOTH MODE: start — helper to re-mount Stripe Elements when the user flips the
// payment-type chooser between one-time and subscription. Stripe's elements.update()
// does not reliably support switching `mode`, so we unmount, drop the cached
// element, and let the existing initializer pathway recreate everything.
StripePayment.reinitForBlock = function ( form, paymentBlock ) {
const paymentInput = paymentBlock.querySelector( 'input.srfm-payment-input' );
if ( ! paymentInput ) {
return;
}
const blockId = paymentBlock.getAttribute( 'data-block-id' );
const compositeKey = getPaymentKey( form, blockId );
// Bail if a reinit is already in flight for this block. Stripe's iframe
// mount is async, and a second pass before 'ready' fires would race two
// elements.create('payment') instances on the same DOM container. The
// caller (radio change handler) reverts the radio UI when this guard
// trips so the visible state stays consistent.
if ( StripePayment.reinitInProgress.has( compositeKey ) ) {
return;
}
const existing = StripePayment.paymentElements[ compositeKey ];
if ( existing && existing.paymentElement ) {
try {
existing.paymentElement.unmount();
} catch ( err ) {
// Already unmounted — safe to ignore.
}
}
delete StripePayment.paymentElements[ compositeKey ];
delete StripePayment.paymentIntents[ compositeKey ];
delete StripePayment.subscriptionIntents[ compositeKey ];
window.srfmPaymentElements = StripePayment.paymentElements;
StripePayment.reinitInProgress.add( compositeKey );
// Re-init only the target block. Previously this used `new StripePayment(form)`
// which walked every payment block in the form and re-ran processPayment on
// each — wasted work on multi-block forms. Object.create gives us a stub
// bound to the form (the only `this` field processPayment touches indirectly
// via initializePaymentElements) without triggering the constructor walk.
const stub = Object.create( StripePayment.prototype );
stub.form = form;
stub.processPayment( paymentBlock );
};
// BOTH MODE: end
// Make StripePayment available globally for form submission
window.StripePayment = StripePayment;
const PAYMENT_UTILITY = {
currentForm: null,
amountPlaceHolder: '',
init: ( form ) => {
PAYMENT_UTILITY.currentForm = form;
PAYMENT_UTILITY.amountPlaceHolder = PAYMENT_UTILITY.getStripeStrings(
'amount_placeholder',
'Please complete the form to view the amount.'
);
PAYMENT_UTILITY.listenAmountChanges();
},
/**
* Format a number according to the format type (EU-style or US-style)
* @param {number|string} amount - The amount to format
* @param {string} formatType - The format type: 'eu-style' or 'us-style'
* @return {string} The formatted number string
*/
formatNumberByType: ( amount, formatType = 'us-style' ) => {
// Normalize to a number first
const normalizedAmount = StripePayment.normalizeAmount(
amount,
formatType
);
if ( isNaN( normalizedAmount ) ) {
return '0.00';
}
// Format to 2 decimal places
const fixedAmount = normalizedAmount.toFixed( 2 );
if ( formatType === 'eu-style' ) {
// EU-style: 1.234,56 (period = thousands, comma = decimal)
const parts = fixedAmount.split( '.' );
const integerPart = parts[ 0 ].replace(
/\B(?=(\d{3})+(?!\d))/g,
'.'
);
const decimalPart = parts[ 1 ];
return integerPart + ',' + decimalPart;
}
// US-style (default): 1,234.56 (comma = thousands, period = decimal)
const parts = fixedAmount.split( '.' );
const integerPart = parts[ 0 ].replace( /\B(?=(\d{3})+(?!\d))/g, ',' );
const decimalPart = parts[ 1 ];
return integerPart + '.' + decimalPart;
},
/**
* Format subscription message by replacing {amount} placeholder with formatted amount
* @param {string} messageFormat - The message format template (e.g., "{amount} per day for 8 payments")
* @param {number} amount - The payment amount
* @param {string} currencySymbol - The currency symbol (e.g., "$")
* @param {string} inputFormatType - The format type: 'eu-style' or 'us-style'
* @return {string} Formatted message
*/
formatSubscriptionMessage: (
messageFormat,
amount,
currencySymbol,
inputFormatType = 'us-style'
) => {
if ( ! messageFormat ) {
return PAYMENT_UTILITY.amountPlaceHolder;
}
// Format amount with currency using the appropriate number format and position
const formattedNumber =
! amount || amount <= 0
? ''
: PAYMENT_UTILITY.formatNumberByType( amount, inputFormatType );
const formattedAmount =
'' !== formattedNumber
? PAYMENT_UTILITY.formatAmountWithCurrencyPosition(
currencySymbol,
formattedNumber
)
: '';
// Replace {amount} placeholder with formatted amount
return '' !== formattedAmount
? messageFormat.replace( '{amount}', formattedAmount )
: PAYMENT_UTILITY.amountPlaceHolder;
},
updatePaymentBlockAmount: (
paymentInput,
amount,
inputFormatType = 'us-style'
) => {
// BOTH MODE: in "both" payment-type mode two .srfm-payment-value spans
// exist (one per amount block). Target the VISIBLE one so the correct
// type's amount updates. Falls back to the first match for non-both mode.
const paymentBlock = paymentInput.closest( '.srfm-block' );
const getPlaceHolderElement =
paymentBlock.querySelector(
'.srfm-payment-amount-block:not([hidden]) .srfm-payment-value'
) || paymentBlock.querySelector( '.srfm-payment-value' );
if ( getPlaceHolderElement ) {
const getCurrencySymbol = getPlaceHolderElement.getAttribute(
'data-currency-symbol'
);
const messageFormat = getPlaceHolderElement.getAttribute(
'data-message-format'
);
if ( getCurrencySymbol ) {
// Check if message format exists (for subscription messages)
if ( messageFormat ) {
const formattedMessage =
PAYMENT_UTILITY.formatSubscriptionMessage(
messageFormat,
amount,
getCurrencySymbol,
inputFormatType
);
getPlaceHolderElement.innerHTML = formattedMessage;
} else {
// Fallback to simple amount display (backward compatible)
// Format the amount according to the number format type and currency position
const formattedNumber = PAYMENT_UTILITY.formatNumberByType(
amount,
inputFormatType
);
getPlaceHolderElement.innerHTML =
PAYMENT_UTILITY.formatAmountWithCurrencyPosition(
getCurrencySymbol,
formattedNumber
);
}
}
}
const normalizedAmount = StripePayment.normalizeAmount(
amount,
inputFormatType
);
paymentInput.setAttribute( 'data-current-amount', normalizedAmount );
paymentInput.setAttribute(
'data-dynamic-amount-format-type',
inputFormatType
);
},
listenAmountChanges: () => {
const paymentInputs = PAYMENT_UTILITY.currentForm.querySelectorAll(
'.srfm-block.srfm-payment-block input.srfm-payment-input[data-variable-amount-field]'
);
if ( paymentInputs.length > 0 ) {
for ( let i = 0; i < paymentInputs.length; i++ ) {
const paymentInput = paymentInputs[ i ];
const getBlockMappedSlug = paymentInput.getAttribute(
'data-variable-amount-field'
);
// BOTH MODE: tear down any listeners previously bound for this
// payment input. switchActivePaymentType() calls this method again
// each time the user flips one-time/subscription, which previously
// piled up duplicate listeners on the source field. Round-trip
// flips compounded the leak (field A → field B → field A → ...).
// Each prior listener still fires on input events and can overwrite
// the displayed amount with a stale source field's value.
if ( paymentInput._srfmAmountListenerController ) {
paymentInput._srfmAmountListenerController.abort();
}
const controller = new AbortController();
paymentInput._srfmAmountListenerController = controller;
const listenerOpts = { signal: controller.signal };
if ( getBlockMappedSlug ) {
const getMappedBlock =
PAYMENT_UTILITY.currentForm.querySelector(
`.srfm-block.srfm-slug-${ getBlockMappedSlug }`
);
if ( getMappedBlock ) {
// Check block type.
if (
getMappedBlock.classList.contains(
'srfm-number-block'
)
) {
const getMappedBlockInput =
getMappedBlock.querySelector(
'input.srfm-input-common'
);
if ( getMappedBlockInput ) {
getMappedBlockInput.addEventListener(
'input',
( event ) => {
const getMappedBlockInputValue =
event.target.value;
// Get format type from the number input's data attribute
const inputFormatType =
getMappedBlockInput.getAttribute(
'format-type'
) || 'us-style';
PAYMENT_UTILITY.updatePaymentBlockAmount(
paymentInput,
getMappedBlockInputValue,
inputFormatType
);
},
listenerOpts
);
// Get initial format type for the initial value
const inputFormatType =
getMappedBlockInput.getAttribute(
'format-type'
) || 'us-style';
PAYMENT_UTILITY.updatePaymentBlockAmount(
paymentInput,
getMappedBlockInput.value,
inputFormatType
);
}
} else if (
getMappedBlock.classList.contains(
'srfm-dropdown-block'
)
) {
const hiddenInput = getMappedBlock.querySelector(
'.srfm-input-dropdown-hidden'
);
if ( hiddenInput ) {
hiddenInput.addEventListener(
'change',
() => {
const amount =
PAYMENT_UTILITY.getDropdownAmount(
getMappedBlock,
hiddenInput
);
PAYMENT_UTILITY.updatePaymentBlockAmount(
paymentInput,
amount
);
},
listenerOpts
);
// Set initial value
const initialAmount =
PAYMENT_UTILITY.getDropdownAmount(
getMappedBlock,
hiddenInput
);
PAYMENT_UTILITY.updatePaymentBlockAmount(
paymentInput,
initialAmount
);
}
} else if (
getMappedBlock.classList.contains(
'srfm-multi-choice-block'
)
) {
const hiddenInput = getMappedBlock.querySelector(
'.srfm-input-multi-choice-hidden'
);
if ( hiddenInput ) {
hiddenInput.addEventListener(
'change',
() => {
const amount =
PAYMENT_UTILITY.getMultiChoiceAmount(
getMappedBlock,
hiddenInput
);
PAYMENT_UTILITY.updatePaymentBlockAmount(
paymentInput,
amount
);
},
listenerOpts
);
// Set initial value
const initialAmount =
PAYMENT_UTILITY.getMultiChoiceAmount(
getMappedBlock,
hiddenInput
);
PAYMENT_UTILITY.updatePaymentBlockAmount(
paymentInput,
initialAmount
);
}
} else if (
getMappedBlock.classList.contains(
'srfm-hidden-block'
)
) {
const hiddenFieldInput =
getMappedBlock.querySelector(
'.srfm-hidden-input'
);
if ( hiddenFieldInput ) {
// Hidden inputs don't fire native input/change
// events when set programmatically, so listen
// for both — integrators that set the value
// via JS should dispatch a 'change' event.
const syncAmount = () => {
const trimmed =
hiddenFieldInput.value.trim();
const rawValue =
/^\d+(\.\d+)?$/.test( trimmed )
? parseFloat( trimmed )
: NaN;
const amount =
isNaN( rawValue ) || rawValue < 0
? 0
: rawValue;
PAYMENT_UTILITY.updatePaymentBlockAmount(
paymentInput,
amount
);
};
hiddenFieldInput.addEventListener(
'change',
syncAmount
);
hiddenFieldInput.addEventListener(
'input',
syncAmount
);
// Set initial value from defaultValue.
syncAmount();
}
}
}
}
}
}
},
getCurrencySymbol: ( currencyCode ) => {
// Use localized currency data from PHP
const currenciesData = window.srfmStripe?.currenciesData || {};
const upperCurrencyCode = currencyCode?.toUpperCase();
const currencyData = currenciesData[ upperCurrencyCode ];
// Return symbol from localized data, or fallback to currency code
return currencyData?.symbol || currencyCode;
},
/**
* Get currency sign position from settings
* @return {string} Currency sign position ('left', 'right', 'left_space', 'right_space')
*/
getCurrencySignPosition: () => {
return window.srfmStripe?.currencySignPosition || 'left';
},
/**
* Format amount with currency symbol based on position setting
* @param {string} currencySymbol - The currency symbol (e.g., "$")
* @param {string|number} formattedAmount - The formatted amount string
* @param {string} position - Currency sign position (optional, defaults to setting)
* @return {string} Formatted amount with currency symbol in correct position
*/
formatAmountWithCurrencyPosition: (
currencySymbol,
formattedAmount,
position = null
) => {
const signPosition =
position || PAYMENT_UTILITY.getCurrencySignPosition();
switch ( signPosition ) {
case 'right':
return `${ formattedAmount }${ currencySymbol }`;
case 'left_space':
return `${ currencySymbol } ${ formattedAmount }`;
case 'right_space':
return `${ formattedAmount } ${ currencySymbol }`;
case 'left':
default:
return `${ currencySymbol }${ formattedAmount }`;
}
},
/**
* Get amount from dropdown block based on selected option values
* @param {HTMLElement} dropdownBlock - The dropdown block element
* @param {HTMLElement} hiddenInput - The hidden input containing selected values
* @return {number} The total amount from selected options
*/
getDropdownAmount: ( dropdownBlock, hiddenInput ) => {
const selectedValues = [];
const hiddenInputValue = hiddenInput.value;
if ( ! hiddenInputValue ) {
return 0;
}
const { extractValue, normalizeDashes } =
window.srfm?.srfmUtility || {};
// Extract selected values from hidden input (format: "Option 1 | Option 2")
const selectedOptions = extractValue
? extractValue( hiddenInputValue )
: hiddenInputValue.split( '|' ).map( ( v ) => v.trim() );
// Get all dropdown options
const options = dropdownBlock.querySelectorAll(
'.srfm-dropdown-input option[option-value]'
);
selectedOptions.forEach( ( selectedOption ) => {
options.forEach( ( option ) => {
const optionText = normalizeDashes( option.innerText?.trim() );
const selectedOptionText = normalizeDashes(
selectedOption?.trim()
);
if ( optionText === selectedOptionText ) {
const optionValue = option.getAttribute( 'option-value' );
// Only add numeric values
if ( ! isNaN( optionValue ) ) {
selectedValues.push( parseFloat( optionValue ) );
} else if ( '' === optionValue ) {
selectedValues.push( 0 );
}
}
} );
} );
// Sum all selected option values
return selectedValues.length > 0
? selectedValues.reduce( ( sum, value ) => sum + value, 0 )
: 0;
},
/**
* Get amount from multi-choice block based on selected option values
* @param {HTMLElement} multiChoiceBlock - The multi-choice block element
* @param {HTMLElement} hiddenInput - The hidden input containing selected values
* @return {number} The total amount from selected options
*/
getMultiChoiceAmount: ( multiChoiceBlock, hiddenInput ) => {
const selectedValues = [];
const hiddenInputValue = hiddenInput.value;
if ( ! hiddenInputValue ) {
return 0;
}
const { extractValue } = window.srfm?.srfmUtility || {};
// Extract selected values from hidden input (format: "Option 1 | Option 2")
const selectedOptions = extractValue( hiddenInputValue );
// Get all multi-choice options
const choices = multiChoiceBlock.querySelectorAll(
'.srfm-multi-choice-single'
);
// Normalize whitespace: collapse multiple spaces to one, then trim.
// This is necessary because browsers collapse consecutive whitespace in
// innerText, while the stored value (from data-option-text attribute)
// preserves the original spacing from the block attributes.
const normalizeStr = ( str ) =>
typeof str === 'string' ? str.trim().replace( /\s+/g, ' ' ) : '';
selectedOptions.forEach( ( selectedOption ) => {
choices.forEach( ( choice ) => {
const label = choice.querySelector(
'.srfm-option-container label'
);
if (
normalizeStr( label?.innerText ) ===
normalizeStr( selectedOption )
) {
const input = choice.querySelector(
'.srfm-input-multi-choice-single'
);
const optionValue = input?.getAttribute( 'option-value' );
// Only add numeric values
if ( ! isNaN( optionValue ) ) {
selectedValues.push( parseFloat( optionValue ) );
} else if ( '' === optionValue ) {
selectedValues.push( 0 );
}
}
} );
} );
// Sum all selected option values
return selectedValues.length > 0
? selectedValues.reduce( ( sum, value ) => sum + value, 0 )
: 0;
},
getStripeStrings: ( code, defaultMessage = '' ) => {
// If no code provided
if ( ! code || code === null || code === undefined ) {
// Return default message if provided, otherwise unknown error
return defaultMessage && defaultMessage.trim() !== ''
? defaultMessage
: window.srfmStripe?.strings?.unknown_error ||
'An unknown error occurred. Please try again or contact the site administrator.';
}
// Check if code exists in localized strings
const localizedMessage = window.srfmStripe?.strings?.[ code ];
if ( localizedMessage ) {
// Code found in localized strings, return it
return localizedMessage;
}
// Code not found in localized strings
// Return default message if provided, otherwise unknown error
return defaultMessage && defaultMessage.trim() !== ''
? defaultMessage
: window.srfmStripe?.strings?.unknown_error ||
'An unknown error occurred. Please try again or contact the site administrator.';
},
};
window.srfmPaymentUtility = PAYMENT_UTILITY;
/**
* Initializes StripePayment for forms after SureForms initialization event.
*/
document.addEventListener( 'srfm_form_after_initialization', ( event ) => {
const form = event?.detail?.form;
if ( form ) {
// Check if form has payment blocks before initializing
const paymentBlocks = form.querySelectorAll(
'.srfm-block.srfm-payment-block'
);
if ( paymentBlocks.length > 0 ) {
new StripePayment( form );
PAYMENT_UTILITY.init( form );
// BOTH MODE: wire the one-time / subscription radio chooser, if present.
initPaymentTypeChoosers( form );
}
}
} );
// BOTH MODE: start — payment-type chooser (one-time vs subscription) wiring.
/**
* Wire the payment-type chooser radios for any payment blocks in this form
* that were saved with paymentType === 'both'.
*
* @param {HTMLFormElement} form - The form element.
*/
function initPaymentTypeChoosers( form ) {
const paymentBlocks = form.querySelectorAll(
'.srfm-block.srfm-payment-block'
);
paymentBlocks.forEach( ( paymentBlock ) => {
const paymentInput = paymentBlock.querySelector(
'input.srfm-payment-input'
);
if ( ! paymentInput ) {
return;
}
// Only wire when the admin chose "both" mode.
if (
paymentInput.getAttribute( 'data-original-payment-type' ) !==
'both'
) {
return;
}
const radios = paymentBlock.querySelectorAll(
'.srfm-payment-type-choice-radio'
);
if ( radios.length === 0 ) {
return;
}
radios.forEach( ( radio ) => {
radio.addEventListener( 'change', ( event ) => {
if ( ! event.target.checked ) {
return;
}
// BOTH MODE: block type-flip while a payment is in flight.
// reinitForBlock() unmounts the Stripe Element and deletes the
// cached intent the pending confirmPayment is still using —
// flipping mid-submit produces double charges or wrong-type
// completions. Revert the radio to whatever data-payment-type
// currently is so the UI does not lie about the user's choice.
const blockId =
paymentBlock.getAttribute( 'data-block-id' );
const compositeKey = getPaymentKey( form, blockId );
const reinitInFlight =
StripePayment.reinitInProgress.has( compositeKey );
if (
form.dataset.srfmPaymentInFlight === 'true' ||
reinitInFlight
) {
const activeType =
paymentInput.getAttribute( 'data-payment-type' ) ||
'one-time';
radios.forEach( ( r ) => {
r.checked = r.value === activeType;
} );
return;
}
switchActivePaymentType(
form,
paymentBlock,
paymentInput,
event.target.value
);
} );
} );
} );
}
/**
* Apply a new active payment type to the block. Updates DOM visibility, syncs
* the live data-* attributes that the rest of the JS reads, re-initializes
* the Stripe Element in the new mode, and dispatches a gateway-agnostic event
* so other gateways (e.g. PayPal in sureforms-pro) can react.
*
* @param {HTMLFormElement} form - The form element.
* @param {HTMLElement} paymentBlock - The payment block wrapper.
* @param {HTMLInputElement} paymentInput - The hidden payment input.
* @param {string} newType - 'one-time' or 'subscription'.
*/
function switchActivePaymentType( form, paymentBlock, paymentInput, newType ) {
const safeType = newType === 'subscription' ? 'subscription' : 'one-time';
// 1. Toggle visible amount block.
const amountBlocks = paymentBlock.querySelectorAll(
'.srfm-payment-amount-block'
);
amountBlocks.forEach( ( el ) => {
if ( el.getAttribute( 'data-payment-type' ) === safeType ) {
el.removeAttribute( 'hidden' );
} else {
el.setAttribute( 'hidden', '' );
}
} );
// 2. Sync the live data attributes from the per-type configuration.
const prefix = safeType === 'subscription' ? 'subscription' : 'one-time';
const amountType =
paymentInput.getAttribute( `data-${ prefix }-amount-type` ) || 'fixed';
const fixedAmount =
paymentInput.getAttribute( `data-${ prefix }-fixed-amount` ) || '0';
const minimumAmount =
paymentInput.getAttribute( `data-${ prefix }-minimum-amount` ) || '0';
const variableField =
paymentInput.getAttribute( `data-${ prefix }-variable-amount-field` ) ||
'';
paymentInput.setAttribute( 'data-payment-type', safeType );
paymentInput.setAttribute( 'data-amount-type', amountType );
paymentInput.setAttribute( 'data-fixed-amount', fixedAmount );
if ( parseFloat( minimumAmount ) > 0 ) {
paymentInput.setAttribute( 'data-minimum-amount', minimumAmount );
} else {
paymentInput.removeAttribute( 'data-minimum-amount' );
}
if ( amountType === 'variable' && variableField ) {
paymentInput.setAttribute( 'data-variable-amount-field', variableField );
} else {
paymentInput.removeAttribute( 'data-variable-amount-field' );
// Also clear any cached current-amount so stale values don't leak across choices.
paymentInput.removeAttribute( 'data-current-amount' );
}
// 3. Re-initialize Stripe Element in the new mode.
if ( typeof StripePayment.reinitForBlock === 'function' ) {
StripePayment.reinitForBlock( form, paymentBlock );
}
// 3b. BOTH MODE: re-wire variable-amount listeners and trigger an immediate
// display update for the new type. listenAmountChanges() queries
// [data-variable-amount-field] which was just updated above, so it will
// find and wire the correct field. The immediate update populates the
// amount span which is empty at PHP render time for variable types.
if ( amountType === 'variable' && variableField ) {
PAYMENT_UTILITY.listenAmountChanges();
// Read current value from the mapped field and update the display now.
const mappedBlock = form.querySelector(
`.srfm-block.srfm-slug-${ variableField }`
);
if ( mappedBlock ) {
const numberInput = mappedBlock.querySelector(
'input.srfm-input-common'
);
const dropdownInput = mappedBlock.querySelector(
'.srfm-input-dropdown-hidden'
);
const multiChoiceInput = mappedBlock.querySelector(
'.srfm-input-multi-choice-hidden'
);
const hiddenFieldInput =
mappedBlock.querySelector( '.srfm-hidden-input' );
let currentValue = 0;
if ( numberInput ) {
currentValue = numberInput.value || 0;
} else if ( dropdownInput ) {
currentValue =
PAYMENT_UTILITY.getDropdownAmount(
mappedBlock,
dropdownInput
) || 0;
} else if ( multiChoiceInput ) {
currentValue =
PAYMENT_UTILITY.getMultiChoiceAmount(
mappedBlock,
multiChoiceInput
) || 0;
} else if ( hiddenFieldInput ) {
// Mirror syncAmount() in listenAmountChanges — accept only numeric
// strings, clamp negatives to 0. Without this branch, currentValue
// stays 0 and overwrites the value syncAmount() just wrote during
// listenAmountChanges() above, breaking initial-amount pickup on
// every one-time/subscription flip.
const trimmed = hiddenFieldInput.value.trim();
const rawValue = /^\d+(\.\d+)?$/.test( trimmed )
? parseFloat( trimmed )
: NaN;
currentValue =
isNaN( rawValue ) || rawValue < 0 ? 0 : rawValue;
}
PAYMENT_UTILITY.updatePaymentBlockAmount(
paymentInput,
currentValue
);
} else {
// No mapped field found — show placeholder.
PAYMENT_UTILITY.updatePaymentBlockAmount( paymentInput, 0 );
}
}
// 4. Dispatch gateway-agnostic event so other gateways (PayPal etc.) can react.
//
// Event contract — `srfm_payment_type_changed` (document-level, bubbles):
// detail: {
// blockId: string — Original payment block id (no instance prefix).
// form: HTMLFormElement — Form element the payment block belongs to.
// paymentType: 'one-time' | 'subscription' — The newly active type.
// paymentInput: HTMLInputElement — The hidden .srfm-payment-input element.
// }
// Companion event `srfm_payment_method_changed` (dispatched in payment-manager.js)
// uses a deliberately narrower shape — { blockId, paymentMethod, form } — because
// gateway listeners only need the method id; if you need paymentInput on that
// path, query it from the form rather than expanding the schema (avoids drift).
const blockId = paymentBlock.getAttribute( 'data-block-id' );
document.dispatchEvent(
new CustomEvent( 'srfm_payment_type_changed', {
detail: {
blockId,
form,
paymentType: safeType,
paymentInput,
},
bubbles: true,
} )
);
}
// BOTH MODE: end
Nama
Tipe
Ukuran
Diubah
Aksi
📁 minified
dir
—
2026-06-25 20:19
📜 payment-history.js
js
16.2 KB
2026-06-25 20:19
📜 payment-manager.js
js
10.3 KB
2026-06-25 20:19
📜 stripe-payment.js
js
51 KB
2026-06-25 20:19