??????????????
??????????????
??????????????
??????????????
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/netecfwd.co.uk/wp-content/plugins/sureforms/inc/payments/front-end.php
⬅ Kembali
<?php
/**
* SureForms Payments Frontend Class.
*
* @package sureforms
* @since 2.0.0
*/
namespace SRFM\Inc\Payments;
use SRFM\Inc\Database\Tables\Payments;
use SRFM\Inc\Field_Validation;
use SRFM\Inc\Payments\Stripe\Stripe_Helper;
use SRFM\Inc\Submit_Token;
use SRFM\Inc\Traits\Get_Instance;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* SureForms Payments Frontend Class.
*
* @since 2.0.0
*/
class Front_End {
use Get_Instance;
/**
* Stores payment entries for later linking with form submissions.
*
* @var array
* @since 2.0.0
*/
private $stripe_payment_entries = [];
/**
* Constructor.
*
* @since 2.0.0
*/
public function __construct() {
add_action( 'wp_ajax_srfm_create_payment_intent', [ $this, 'create_payment_intent' ] );
add_action( 'wp_ajax_nopriv_srfm_create_payment_intent', [ $this, 'create_payment_intent' ] );
add_action( 'wp_ajax_srfm_create_subscription_intent', [ $this, 'create_subscription_intent' ] );
add_action( 'wp_ajax_nopriv_srfm_create_subscription_intent', [ $this, 'create_subscription_intent' ] ); // For non-logged-in users.
add_filter( 'srfm_form_submit_data', [ $this, 'validate_payment_fields' ], 5, 1 );
add_action( 'srfm_form_submit', [ $this, 'update_payment_entry_id_form_submit' ], 10, 1 );
add_filter( 'srfm_show_options_values', [ $this, 'show_options_values' ], 10, 2 );
add_filter( 'srfm_all_data_field_row', [ $this, 'skip_payment_fields_from_all_data' ], 10, 2 );
add_filter( 'srfm_map_slug_to_submission_data_should_skip', [ $this, 'skip_payment_fields_from_submission_data' ], 10, 2 );
add_filter( 'srfm_should_skip_field_from_sample_data', [ $this, 'skip_payment_fields_from_sample_data' ], 10, 2 );
}
/**
* Show options values
*
* @param bool $default_value Default value.
* @param bool $value Value.
* @since 2.0.0
* @return bool
*/
public function show_options_values( $default_value, $value ) {
return $value ? true : $default_value;
}
/**
* Create payment intent
*
* @throws \Exception When Stripe configuration is invalid.
* @since 2.0.0
* @return void
*/
public function create_payment_intent() {
// Verify submit token.
$token = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- HMAC token verification replaces nonce.
$form_id = isset( $_POST['form_id'] ) && is_numeric( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( ! Submit_Token::verify( $token, $form_id ) ) {
wp_send_json_error( __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) );
}
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified via Submit_Token::verify() above.
$amount = intval( $_POST['amount'] ?? 0 );
$currency = sanitize_text_field( wp_unslash( $_POST['currency'] ?? 'usd' ) );
$description = sanitize_text_field( wp_unslash( $_POST['description'] ?? 'SureForms Payment' ) );
$block_id = sanitize_text_field( wp_unslash( $_POST['block_id'] ?? '' ) );
$customer_email = sanitize_email( wp_unslash( $_POST['customer_email'] ?? '' ) );
$customer_name = sanitize_text_field( wp_unslash( $_POST['customer_name'] ?? '' ) );
$form_id = isset( $_POST['form_id'] ) && is_numeric( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
// phpcs:enable WordPress.Security.NonceVerification.Missing
if ( $amount <= 0 ) {
wp_send_json_error( __( 'Invalid payment amount.', 'sureforms' ) );
}
$amount_processed_with_currency = Stripe_Helper::amount_from_stripe_format( $amount, $currency );
// Validate payment amount against stored form configuration.
if ( $form_id <= 0 || empty( $block_id ) ) {
wp_send_json_error( __( 'Invalid form configuration.', 'sureforms' ) );
}
// BOTH MODE: pass 'one-time' so the validator uses the correct per-type amount config.
$validation_result = Payment_Helper::validate_payment_amount( $amount_processed_with_currency, $currency, $form_id, $block_id, 'one-time' );
if ( ! $validation_result['valid'] ) {
wp_send_json_error( $validation_result['message'] );
}
// Validate customer email (required for one-time payments).
if ( empty( $customer_email ) || ! is_email( $customer_email ) ) {
wp_send_json_error( __( 'Valid customer email is required for payments.', 'sureforms' ) );
}
try {
// Validate Stripe connection.
if ( ! Stripe_Helper::is_stripe_connected() ) {
throw new \Exception( __( 'Stripe is not connected.', 'sureforms' ) );
}
$secret_key = Stripe_Helper::get_stripe_secret_key();
if ( empty( $secret_key ) ) {
throw new \Exception( __( 'Stripe secret key not found.', 'sureforms' ) );
}
// Create or get customer ID for logged-in users.
$customer_id = null;
if ( is_user_logged_in() ) {
$customer_id = $this->get_or_create_stripe_customer(
[
'email' => $customer_email,
'name' => $customer_name,
]
);
}
$license_key = Stripe_Helper::get_license_key();
// Create payment intent with confirm: true for immediate processing.
$payment_intent_data = [
'secret_key' => $secret_key,
'amount' => $amount,
'currency' => strtolower( $currency ),
'description' => $description,
'confirm' => false, // Will be confirmed by frontend.
'receipt_email' => $customer_email,
'license_key' => $license_key,
'automatic_payment_methods' => [
'enabled' => true,
'allow_redirects' => 'never',
],
'metadata' => [
'source' => 'SureForms',
'block_id' => $block_id,
'original_amount' => $amount,
'receipt_email' => $customer_email,
'customer_name' => $customer_name,
],
];
// Add customer ID to payment intent data if user is logged in.
if ( ! empty( $customer_id ) ) {
$payment_intent_data['customer'] = $customer_id;
}
$payment_intent_data = apply_filters(
'srfm_create_payment_intent_data',
$payment_intent_data,
$customer_id
);
$payment_intent_data = wp_json_encode( $payment_intent_data );
$payment_intent_data = is_string( $payment_intent_data ) ? $payment_intent_data : '';
$payment_intent_data = base64_encode( $payment_intent_data ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
$payment_intent = wp_remote_post(
Stripe_Helper::middle_ware_base_url() . 'payment-intent/create',
[
'body' => $payment_intent_data,
'headers' => [
'Content-Type' => 'application/json',
],
]
);
if ( is_wp_error( $payment_intent ) ) {
throw new \Exception( Payment_Helper::get_error_message_by_key( 'failed_to_create_payment' ) );
}
$payment_intent = json_decode( wp_remote_retrieve_body( $payment_intent ), true );
$payment_intent = is_array( $payment_intent ) ? $payment_intent : [];
// Check if we have an error from Stripe API (verify both status and code).
if ( isset( $payment_intent['status'] ) && 'error' === $payment_intent['status'] && isset( $payment_intent['code'] ) && ! empty( $payment_intent['code'] ) ) {
// Handle amount_too_small error with custom message.
if ( 'amount_too_small' === $payment_intent['code'] ) {
// Format the amount for display.
$currency_symbol = Stripe_Helper::get_currency_symbol( $currency );
$display_amount = $amount_processed_with_currency;
$formatted_amount = $currency_symbol . number_format( $display_amount, 2 );
throw new \Exception(
sprintf(
/* translators: %s: formatted payment amount */
__( 'The payment amount (%s) is below the minimum allowed. Stripe only processes amounts above 50¢.', 'sureforms' ),
$formatted_amount
)
);
}
// For other error codes, use the message from Stripe API if available.
if ( isset( $payment_intent['message'] ) && ! empty( $payment_intent['message'] ) ) {
throw new \Exception( $payment_intent['message'] );
}
// Fallback if we have error code but no message.
throw new \Exception( Payment_Helper::get_error_message_by_key( 'failed_to_create_payment' ) );
}
if ( ! isset( $payment_intent['client_secret'] ) || empty( $payment_intent['client_secret'] ) || ! isset( $payment_intent['id'] ) || empty( $payment_intent['id'] ) ) {
throw new \Exception( Payment_Helper::get_error_message_by_key( 'failed_to_create_payment' ) );
}
// Store payment intent metadata in transient for verification.
// active_type binds this intent to the one-time flow so a tampered
// submission cannot replay it through the subscription submit path.
Payment_Helper::store_payment_intent_metadata(
$block_id,
$payment_intent['id'],
[
'form_id' => $form_id,
'block_id' => $block_id,
'amount' => $amount_processed_with_currency,
'currency' => strtolower( $currency ),
'active_type' => 'one-time',
]
);
wp_send_json_success(
[
'client_secret' => $payment_intent['client_secret'],
'payment_intent_id' => $payment_intent['id'],
'customer_id' => $customer_id,
]
);
} catch ( \Exception $e ) {
$error_message = $e->getMessage();
$error_message = empty( $error_message ) ? Payment_Helper::get_error_message_by_key( 'failed_to_create_payment' ) : $error_message;
wp_send_json_error( $error_message );
}
}
/**
* Create subscription intent with improved error handling from simple-stripe-subscriptions
*
* @throws \Exception When Stripe configuration is invalid.
* @since 2.0.0
* @return void
*/
public function create_subscription_intent() {
// Verify submit token.
$token = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- HMAC token verification replaces nonce.
$form_id = isset( $_POST['form_id'] ) && is_numeric( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( ! Submit_Token::verify( $token, $form_id ) ) {
wp_send_json_error( __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) );
}
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified via Submit_Token::verify() above.
// Validate required fields like simple-stripe-subscriptions.
$required_fields = [ 'amount', 'currency', 'description', 'block_id', 'interval', 'plan_name' ];
foreach ( $required_fields as $field ) {
if ( empty( $_POST[ $field ] ) ) {
/* translators: %s: Field name */
wp_send_json_error( sprintf( __( 'Missing required field: %s', 'sureforms' ), $field ) );
}
}
$amount = intval( $_POST['amount'] ?? 0 );
$currency = sanitize_text_field( wp_unslash( $_POST['currency'] ?? 'usd' ) );
$description = sanitize_text_field( wp_unslash( $_POST['description'] ?? 'SureForms Subscription' ) );
$block_id = sanitize_text_field( wp_unslash( $_POST['block_id'] ?? '' ) );
$subscription_interval = sanitize_text_field( wp_unslash( $_POST['interval'] ?? 'month' ) );
$plan_name = sanitize_text_field( wp_unslash( $_POST['plan_name'] ?? 'Subscription Plan' ) );
$customer_email = sanitize_email( wp_unslash( $_POST['customer_email'] ?? '' ) );
$customer_name = sanitize_text_field( wp_unslash( $_POST['customer_name'] ?? '' ) );
$form_id = isset( $_POST['form_id'] ) && is_numeric( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
// phpcs:enable WordPress.Security.NonceVerification.Missing
// Validate customer email (required for all subscriptions).
if ( empty( $customer_email ) || ! is_email( $customer_email ) ) {
wp_send_json_error( __( 'Valid customer email is required for subscriptions.', 'sureforms' ) );
}
// Validate customer name (required for subscriptions).
if ( empty( $customer_name ) ) {
wp_send_json_error( __( 'Customer name is required for subscriptions.', 'sureforms' ) );
}
$amount_processed_with_currency = Stripe_Helper::amount_from_stripe_format( $amount, $currency );
// Validate payment amount against stored form configuration.
if ( $form_id <= 0 || empty( $block_id ) ) {
wp_send_json_error( __( 'Invalid form configuration.', 'sureforms' ) );
}
// BOTH MODE: pass 'subscription' so the validator uses the correct per-type amount config.
$validation_result = Payment_Helper::validate_payment_amount( $amount_processed_with_currency, $currency, $form_id, $block_id, 'subscription' );
if ( ! $validation_result['valid'] ) {
wp_send_json_error( $validation_result['message'] );
}
// Validate amount like simple-stripe-subscriptions.
if ( $amount <= 0 ) {
wp_send_json_error( __( 'Amount must be greater than 0', 'sureforms' ) );
}
// Validate interval like simple-stripe-subscriptions.
// BOTH MODE: 'quarter' is a valid editor option but was missing from the allow-list,
// causing Quarterly subscriptions to be rejected at submit time.
$valid_intervals = [ 'day', 'week', 'month', 'quarter', 'year' ];
if ( ! in_array( $subscription_interval, $valid_intervals, true ) ) {
wp_send_json_error( __( 'Invalid billing interval', 'sureforms' ) );
}
// Reject when the submitted interval does not match what the admin saved in
// the form's stored block config. Admin picks a single interval in the editor;
// the end user has no chooser. So a divergence here is always tampering — the
// data attribute the server itself rendered has been altered before submit.
$stored_block_config = Field_Validation::get_or_migrate_block_config_for_legacy_form( $form_id );
if ( is_array( $stored_block_config ) && isset( $stored_block_config[ $block_id ] ) && is_array( $stored_block_config[ $block_id ] ) ) {
$stored_interval = $stored_block_config[ $block_id ]['subscription_interval'] ?? '';
if ( ! empty( $stored_interval ) && $stored_interval !== $subscription_interval ) {
wp_send_json_error( __( 'Billing interval does not match the form configuration.', 'sureforms' ) );
}
}
try {
// Validate Stripe connection.
if ( ! Stripe_Helper::is_stripe_connected() ) {
throw new \Exception( __( 'Stripe is not connected.', 'sureforms' ) );
}
$secret_key = Stripe_Helper::get_stripe_secret_key();
if ( empty( $secret_key ) ) {
throw new \Exception( __( 'Stripe secret key not found.', 'sureforms' ) );
}
// Get or create Stripe customer for subscriptions.
$customer_id = $this->get_or_create_stripe_customer(
[
'email' => $customer_email,
'name' => $customer_name,
]
);
if ( ! $customer_id ) {
throw new \Exception( __( 'Failed to create customer for subscription.', 'sureforms' ) );
}
$license_key = Stripe_Helper::get_license_key();
// Prepare subscription data for middleware.
$subscription_data = apply_filters(
'srfm_create_subscription_data',
[
'secret_key' => $secret_key,
'customer_id' => $customer_id,
'amount' => $amount,
'currency' => strtolower( $currency ),
'description' => $description,
'interval' => $subscription_interval,
'license_key' => $license_key,
'block_id' => $block_id,
'plan_name' => $plan_name,
'metadata' => [
'source' => 'SureForms',
'block_id' => $block_id,
'original_amount' => $amount,
'billing_interval' => $subscription_interval,
],
]
);
$endpoint = Stripe_Helper::middle_ware_base_url() . 'subscription/create';
$subscription_data_body = wp_json_encode( $subscription_data );
$subscription_data_body = is_string( $subscription_data_body ) ? $subscription_data_body : '';
$subscription_data_body = base64_encode( $subscription_data_body ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
if ( empty( $subscription_data_body ) ) {
throw new \Exception( __( 'Failed to create subscription through middleware.', 'sureforms' ) );
}
// Call middleware subscription creation endpoint.
$subscription_response = wp_remote_post(
$endpoint,
[
'body' => $subscription_data_body,
'headers' => [
'Content-Type' => 'application/json',
],
'timeout' => 60, // Subscription creation can take longer.
]
);
if ( is_wp_error( $subscription_response ) ) {
throw new \Exception( __( 'Failed to create subscription through middleware.', 'sureforms' ) );
}
$response_body = wp_remote_retrieve_body( $subscription_response );
if ( empty( $response_body ) ) {
throw new \Exception( __( 'Empty response from subscription creation.', 'sureforms' ) );
}
$subscription = json_decode( $response_body, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
throw new \Exception( __( 'Invalid JSON response from subscription creation.', 'sureforms' ) );
}
if ( ! is_array( $subscription ) ) {
wp_send_json_error( __( 'Invalid subscription data.', 'sureforms' ) );
}
if ( 'error' === $subscription['status'] ) {
wp_send_json_error( isset( $subscription['message'] ) && ! empty( $subscription['message'] ) ? $subscription['message'] : __( 'Invalid subscription data.', 'sureforms' ) );
}
$payment_intent_id = isset( $subscription['setup_intent']['id'] ) && ! empty( $subscription['setup_intent']['id'] ) ? $subscription['setup_intent']['id'] : '';
$subscription_id = isset( $subscription['subscription_data']['id'] ) && ! empty( $subscription['subscription_data']['id'] ) ? $subscription['subscription_data']['id'] : '';
$client_secret = isset( $subscription['client_secret'] ) && ! empty( $subscription['client_secret'] ) ? $subscription['client_secret'] : '';
if ( empty( $client_secret ) || empty( $subscription_id ) || empty( $payment_intent_id ) ) {
throw new \Exception( __( 'Failed to create subscription.', 'sureforms' ) );
}
// Store subscription metadata in transient for verification.
// active_type binds this intent to the subscription flow so a tampered
// submission cannot replay it through the one-time submit path.
Payment_Helper::store_payment_intent_metadata(
$block_id,
$payment_intent_id,
[
'form_id' => $form_id,
'block_id' => $block_id,
'amount' => $amount_processed_with_currency,
'currency' => strtolower( $currency ),
'subscription_id' => $subscription_id,
'active_type' => 'subscription',
]
);
$response = [
'type' => 'subscription',
'client_secret' => $client_secret,
'subscription_id' => $subscription_id,
'customer_id' => $customer_id,
'payment_intent_id' => $payment_intent_id,
'amount' => Stripe_Helper::amount_from_stripe_format( $amount, $currency ),
'interval' => $subscription_interval,
];
wp_send_json_success( $response );
} catch ( \Exception $e ) {
/* translators: %s: Error message */
wp_send_json_error( sprintf( __( 'Unexpected error: %s', 'sureforms' ), $e->getMessage() ) );
}
}
/**
* Validate payment fields before form submission
*
* @param array<mixed> $form_data Form data.
* @since 2.0.0
* @return array<mixed>
*/
public function validate_payment_fields( $form_data ) {
// Check if form data is valid.
if ( empty( $form_data ) || ! is_array( $form_data ) ) {
return $form_data;
}
$payment_response = [];
// Loop through form data to find payment fields.
foreach ( $form_data as $field_name => $field_value ) {
// Check if field name contains "-lbl-" pattern.
if ( strpos( $field_name, '-lbl-' ) === false ) {
continue;
}
// Split field name by "-lbl-" delimiter.
$name_parts = explode( '-lbl-', $field_name );
// Check if we have the expected parts.
if ( count( $name_parts ) < 2 ) {
continue;
}
// Check if the first part starts with "srfm-payment-".
if ( ! ( strpos( $name_parts[0], 'srfm-payment-' ) === 0 ) ) {
continue;
}
// Value will be in the form of the json string.
$payment_value = json_decode( $field_value, true );
if ( empty( $payment_value ) || ! is_array( $payment_value ) ) {
continue;
}
// Extract payment ID - this will be the payment intent ID for one-time payments,
// or the payment method ID (result.setupIntent.payment_method) for subscriptions.
$payment_id = ! empty( $payment_value['paymentId'] ) ? $payment_value['paymentId'] : '';
$setup_intent = ! empty( $payment_value['setupIntent'] ) ? $payment_value['setupIntent'] : '';
// introduced during the paypal implementation and in the other payment methods, we use the transactionId to verify the payment.
$transaction_id = ! empty( $payment_value['transactionId'] ) ? $payment_value['transactionId'] : '';
if ( empty( $payment_id ) && empty( $setup_intent ) && empty( $transaction_id ) ) {
continue;
}
$block_id = ! empty( $payment_value['blockId'] ) ? $payment_value['blockId'] : '';
$payment_type = ! empty( $payment_value['paymentType'] ) ? $payment_value['paymentType'] : '';
$payment_method = ! empty( $payment_value['paymentMethod'] ) ? $payment_value['paymentMethod'] : 'stripe';
if ( empty( $block_id ) || empty( $payment_type ) ) {
continue;
}
if ( 'stripe' === $payment_method ) {
$payment_response = $this->verify_stripe_payment( $payment_value, $payment_id, $block_id, $form_data, $payment_type );
} else {
$payment_response = apply_filters(
'srfm_verify_payment_value',
[
'payment_value' => $payment_value,
'class' => $this,
'block_id' => $block_id,
'form_data' => $form_data,
]
);
}
if ( ! empty( $payment_response ) && isset( $payment_response['payment_id'] ) ) {
// Modify the form data with the payment ID.
$form_data[ $field_name ] = $payment_response['payment_id'];
}
}
if ( ! empty( $payment_response ) && isset( $payment_response['error'] ) ) {
$form_data = array_merge( $form_data, $payment_response );
}
return $form_data;
}
/**
* Verify Stripe payment
*
* @param array<mixed> $payment_value Payment value.
* @param string $payment_id Payment ID.
* @param string $block_id Block ID.
* @param array<mixed> $form_data Form data.
* @param string $payment_type Payment type.
* @since 2.0.0
* @return array<mixed> Payment response.
*/
public function verify_stripe_payment( $payment_value, $payment_id, $block_id, $form_data, $payment_type ) {
if ( 'stripe-subscription' === $payment_type ) {
/**
* For subscription payments, we receive the following data structure:
* - paymentMethod: Stripe payment method ID (e.g., "pm_1S82ZkHqS7N4oFQhruGV67u1")
* - setupIntent: Stripe setup intent ID (e.g., "seti_1S82ZkHqS7N4oFQhPa4LYPYg")
* - subscriptionId: Stripe subscription ID (e.g., "sub_1S82ZiHqS7N4oFQhPGhm2eNR")
* - customerId: Stripe customer ID (e.g., "cus_T4Apjla33GlYAk")
* - blockId: Form block identifier (e.g., "be920796")
* - paymentType: Payment type identifier ("stripe-subscription")
* - status: Payment status ("succeeded")
*/
$payment_response = $this->verify_stripe_subscription_intent_and_save( $payment_value, $block_id, $form_data );
} else {
$payment_response = $this->verify_stripe_payment_intent_and_save( $payment_value, $payment_id, $block_id, $form_data );
}
return ! empty( $payment_response ) && is_array( $payment_response ) ? $payment_response : [];
}
/**
* Simplified subscription verification using simple-stripe-subscriptions approach
*
* @param array<mixed> $subscription_value Subscription data from frontend.
* @param string $block_id Block ID.
* @param array<mixed> $form_data Form data.
* @since 2.0.0
* @return void|array<mixed> True if subscription is verified and saved successfully.
*/
public function verify_stripe_subscription_intent_and_save( $subscription_value, $block_id, $form_data ) {
$subscription_id = ! empty( $subscription_value['subscriptionId'] ) && is_string( $subscription_value['subscriptionId'] ) ? $subscription_value['subscriptionId'] : '';
if ( empty( $subscription_id ) ) {
return [
'error' => __( 'Subscription ID not found.', 'sureforms' ),
];
}
$customer_id = ! empty( $subscription_value['customerId'] ) ? $subscription_value['customerId'] : '';
$setup_intent_id = ! empty( $subscription_value['setupIntent'] ) && is_string( $subscription_value['setupIntent'] ) ? $subscription_value['setupIntent'] : '';
// Verify payment intent with comprehensive validation including form data.
// BOTH MODE: pass 'subscription' so per-type amount config is used for verification.
$verification_result = Payment_Helper::verify_payment_intent( $block_id, $setup_intent_id, $form_data, 'subscription' );
if ( false === $verification_result['valid'] ) {
return [
'error' => $verification_result['message'],
];
}
if ( empty( $customer_id ) ) {
return [
'error' => __( 'Customer ID not found for the payment.', 'sureforms' ),
];
}
try {
// Get payment mode and secret key.
$payment_mode = Stripe_Helper::get_stripe_mode();
$secret_key = Stripe_Helper::get_stripe_secret_key();
if ( empty( $secret_key ) ) {
return [
'error' => __( 'Stripe secret key not found.', 'sureforms' ),
];
}
// Update subscription with payment method from setup intent if available.
$paid_invoice = [];
if ( ! empty( $setup_intent_id ) ) {
try {
$setup_intent_response = Stripe_Helper::stripe_api_request(
'setup_intents',
'GET',
[],
$setup_intent_id
);
if ( ! $setup_intent_response['success'] ) {
return [
'error' => $setup_intent_response['error']['message'] ?? __( 'Failed to retrieve setup intent.', 'sureforms' ),
];
}
$setup_intent = $setup_intent_response['data'];
if ( ( isset( $setup_intent['payment_method'] ) && ! empty( $setup_intent['payment_method'] ) && is_string( $setup_intent['payment_method'] ) ) ) {
// Prepare subscription update data.
$subscription_update_data = [
'default_payment_method' => $setup_intent['payment_method'],
'collection_method' => 'charge_automatically',
];
// Override interval + billing cycles with the values stored in the
// form's block config. These come from the data attributes the
// server itself rendered, so they cannot legitimately diverge from
// the admin's saved subscriptionPlan. Trusting the submitted values
// would let an attacker DevTools-flip cancel_at to 'ongoing'.
$form_id_for_config = isset( $form_data['form-id'] ) && is_numeric( $form_data['form-id'] ) ? intval( $form_data['form-id'] ) : 0;
if ( $form_id_for_config > 0 && ! empty( $block_id ) ) {
$stored_block_config = Field_Validation::get_or_migrate_block_config_for_legacy_form( $form_id_for_config );
if ( is_array( $stored_block_config ) && isset( $stored_block_config[ $block_id ] ) && is_array( $stored_block_config[ $block_id ] ) ) {
$stored_payment_config = $stored_block_config[ $block_id ];
if ( isset( $stored_payment_config['subscription_interval'] ) ) {
$subscription_value['subscriptionInterval'] = $stored_payment_config['subscription_interval'];
}
if ( isset( $stored_payment_config['subscription_billing_cycles'] ) ) {
$subscription_value['subscriptionBillingCycles'] = $stored_payment_config['subscription_billing_cycles'];
}
}
}
// Calculate cancel_at timestamp based on billing cycles and interval.
$cancel_at = $this->prepare_cancel_at( $subscription_value );
if ( ! empty( $cancel_at ) ) {
$subscription_update_data['cancel_at'] = $cancel_at;
}
$subscription_update_response = Stripe_Helper::stripe_api_request(
'subscriptions',
'POST',
$subscription_update_data,
$subscription_id
);
if ( ! $subscription_update_response['success'] ) {
return [
'error' => $subscription_update_response['error']['message'] ?? __( 'Failed to update subscription.', 'sureforms' ),
];
}
$subscription_update = $subscription_update_response['data'];
if ( empty( $subscription_update['latest_invoice'] ) ) {
return [
'error' => __( 'Latest invoice not found on subscription.', 'sureforms' ),
];
}
$invoice_response = Stripe_Helper::stripe_api_request(
'invoices',
'GET',
[],
$subscription_update['latest_invoice']
);
if ( ! $invoice_response['success'] ) {
return [
'error' => $invoice_response['error']['message'] ?? __( 'Failed to retrieve invoice.', 'sureforms' ),
];
}
$invoice = $invoice_response['data'];
// Ensure invoice auto-advance is enabled for recurring payments.
// This tells Stripe to automatically finalize and charge future invoices.
if ( empty( $invoice['auto_advance'] ) && ! empty( $invoice['id'] ) && is_string( $invoice['id'] ) ) {
Stripe_Helper::stripe_api_request(
'invoices',
'POST',
[ 'auto_advance' => true ],
$invoice['id']
);
}
// Extract payment intent from the invoice.
$payment_intent_id = isset( $invoice['payment_intent'] ) && ! empty( $invoice['payment_intent'] ) && is_string( $invoice['payment_intent'] ) ? $invoice['payment_intent'] : '';
if ( empty( $payment_intent_id ) ) {
return [
'error' => __( 'Payment intent not found on invoice.', 'sureforms' ),
];
}
// Confirm the payment intent with payment method.
// This completes the payment and activates the subscription.
$paid_invoice_response = Stripe_Helper::stripe_api_request(
'payment_intents',
'POST',
[ 'payment_method' => $setup_intent['payment_method'] ],
$payment_intent_id . '/confirm'
);
if ( ! $paid_invoice_response['success'] ) {
return [
'error' => $paid_invoice_response['error']['message'] ?? __( 'Failed to confirm payment.', 'sureforms' ),
];
}
$paid_invoice = $paid_invoice_response['data'];
// Get the subscription.
$subscription_response = Stripe_Helper::stripe_api_request(
'subscriptions',
'GET',
[],
$subscription_id
);
if ( ! $subscription_response['success'] ) {
return [
'error' => $subscription_response['error']['message'] ?? __( 'Failed to retrieve subscription.', 'sureforms' ),
];
}
$subscription = $subscription_response['data'];
}
} catch ( \Exception $e ) {
return [
'error' => $e->getMessage(),
];
}
}
if ( empty( $subscription ) ) {
return [
'error' => __( 'Subscription not found for the payment.', 'sureforms' ),
];
}
// Use simple-stripe-subscriptions validation logic - check if subscription is in good state.
$is_subscription_active = in_array( $subscription['status'], [ 'active', 'trialing' ], true );
$final_status = $is_subscription_active ? 'active' : 'failed';
$amount = isset( $paid_invoice['amount'] ) && ! empty( $paid_invoice['amount'] ) ? $paid_invoice['amount'] : 0;
$currency = isset( $paid_invoice['currency'] ) && ! empty( $paid_invoice['currency'] ) ? $paid_invoice['currency'] : 'usd';
$form_id = isset( $form_data['form-id'] ) && ! empty( $form_data['form-id'] ) ? $form_data['form-id'] : 0;
$subscription_status = isset( $subscription['status'] ) && ! empty( $subscription['status'] ) && is_string( $subscription['status'] ) ? $subscription['status'] : '';
// Defense-in-depth: re-validate the amount Stripe actually invoiced against the form's
// server-side configuration. The recurring price is the invoiced amount, so an
// underpayment here would otherwise repeat every billing cycle.
$charged_amount = Stripe_Helper::amount_from_stripe_format( is_numeric( $amount ) ? (int) $amount : 0, is_string( $currency ) ? $currency : 'usd' );
$charge_validation = Payment_Helper::validate_amount_against_config( $block_id, is_numeric( $form_id ) ? (int) $form_id : 0, $form_data, $charged_amount, 'subscription' );
if ( false === $charge_validation['valid'] ) {
return [
'error' => $charge_validation['message'],
];
}
$invoice_status = isset( $paid_invoice['status'] ) && ! empty( $paid_invoice['status'] ) && is_string( $paid_invoice['status'] ) ? $paid_invoice['status'] : '';
// Extract customer data.
$customer_data = $this->extract_customer_data( $subscription_value );
// Extract charge ID from the first payment intent for refund purposes.
// For subscriptions, we store the charge ID in transaction_id so refunds can be processed.
$charge_id = '';
if ( ! empty( $paid_invoice['latest_charge'] ) && is_string( $paid_invoice['latest_charge'] ) ) {
$charge_id = $paid_invoice['latest_charge'];
} elseif ( ! empty( $paid_invoice['charges']['data'][0]['id'] ) && is_string( $paid_invoice['charges']['data'][0]['id'] ) ) {
$charge_id = $paid_invoice['charges']['data'][0]['id'];
}
// Use charge ID as transaction_id if available, otherwise fall back to subscription ID.
$transaction_id = ! empty( $charge_id ) ? $charge_id : $subscription_id;
// Send payment data to middleware for analytics.
if ( ! empty( $charge_id ) ) {
Stripe_Helper::intersect_payment( $charge_id, $secret_key, '', 'SureForms' );
}
// Prepare minimal subscription data for database.
$entry_data = [
'form_id' => $form_id,
'block_id' => $block_id,
'status' => $final_status,
'total_amount' => Stripe_Helper::amount_from_stripe_format( $amount, $currency ),
'currency' => $currency,
'entry_id' => 0,
'gateway' => 'stripe',
'type' => 'subscription',
'mode' => $payment_mode,
'transaction_id' => $transaction_id,
'customer_id' => $customer_id,
'subscription_id' => $subscription_id,
'subscription_status' => $subscription_status,
'srfm_txn_id' => '', // Will be updated after getting payment entry ID.
'customer_email' => $customer_data['email'],
'customer_name' => $customer_data['name'],
'payment_data' => [
'initial_invoice' => $paid_invoice,
'subscription' => $subscription,
'payment_value' => $subscription_value,
],
];
// Get user ID if logged in.
$user_id = get_current_user_id();
$user_info = $user_id > 0
/* translators: %d: User ID */
? sprintf( __( 'User ID: %d', 'sureforms' ), $user_id )
/* translators: Message for guest user in payment logs */
: __( 'Guest User', 'sureforms' );
// If invoice is not paid then we need to set the status in the subscription log and return error.
$paid_invoice_log = '';
if ( 'paid' !== $invoice_status ) {
/* translators: %s: Invoice status */
$paid_invoice_log = sprintf( __( 'Invoice Status: %s', 'sureforms' ), $invoice_status );
}
// Add simple log entry.
$entry_data['log'] = [
[
/* translators: Title for subscription verification log */
'title' => __( 'Subscription Verification', 'sureforms' ),
'created_at' => current_time( 'mysql' ),
'messages' => [
/* translators: %s: Subscription ID */
sprintf( __( 'Subscription ID: %s', 'sureforms' ), $subscription_id ),
/* translators: %s: Payment Gateway */
sprintf( __( 'Payment Gateway: %s', 'sureforms' ), 'Stripe' ),
/* translators: %s: Payment Intent ID */
sprintf( __( 'Payment Intent ID: %s', 'sureforms' ), $setup_intent_id ),
/* translators: %s: Charge ID */
sprintf( __( 'Charge ID: %s', 'sureforms' ), ! empty( $charge_id ) ? $charge_id : 'N/A' ),
/* translators: %s: Subscription Status */
sprintf( __( 'Subscription Status: %s', 'sureforms' ), $subscription_status ),
/* translators: %s: Customer ID */
sprintf( __( 'Customer ID: %s', 'sureforms' ), $customer_id ),
/* translators: 1: Amount, 2: Currency */
sprintf( __( 'Amount: %1$s %2$s', 'sureforms' ), number_format( Stripe_Helper::amount_from_stripe_format( $amount, $currency ), 2 ), strtoupper( $currency ) ),
$user_info,
/* translators: %s: Payment mode (e.g. Live or Test) */
sprintf( __( 'Mode: %s', 'sureforms' ), ucfirst( $payment_mode ) ),
$paid_invoice_log,
],
],
];
// Save to database.
$payment_entry_id = Payments::add( $entry_data );
if ( $payment_entry_id ) {
// Generate unique payment ID using the auto-increment ID and update the entry.
$unique_payment_id = Stripe_Helper::generate_unique_payment_id( $payment_entry_id );
// For initial subscription, set parent_subscription_id to itself (it's the parent).
Payments::update(
$payment_entry_id,
[
'srfm_txn_id' => $unique_payment_id,
'parent_subscription_id' => $payment_entry_id,
]
);
// Store in static array for later entry linking.
$this->stripe_payment_entries[] = [
'payment_id' => $transaction_id,
'block_id' => $block_id,
'form_id' => $form_id,
];
return [
'payment_id' => $payment_entry_id,
];
}
} catch ( \Exception $e ) {
return [
'error' => empty( $e->getMessage() ) ? __( 'Failed to verify subscription.', 'sureforms' ) : $e->getMessage(),
];
}
}
/**
* Prepare cancel_at timestamp for subscription based on billing cycles and interval.
*
* @param array<string,mixed> $input_value Array containing subscriptionBillingCycles and subscriptionInterval.
* @since 2.0.0
* @return int|false|null Unix timestamp for cancel_at, or null if not applicable.
*/
public function prepare_cancel_at( $input_value ) {
$subscription_billing_cycles = ! empty( $input_value['subscriptionBillingCycles'] ) ? $input_value['subscriptionBillingCycles'] : 0;
$subscription_interval = ! empty( $input_value['subscriptionInterval'] ) ? $input_value['subscriptionInterval'] : '';
// Return null if billing cycles is 0, empty, or equals 'ongoing'.
if ( empty( $subscription_billing_cycles ) || 'ongoing' === $subscription_billing_cycles || ! is_numeric( $subscription_billing_cycles ) ) {
return null;
}
// Convert billing cycles to integer.
$billing_cycles = (int) $subscription_billing_cycles;
// Return null if billing cycles is less than or equal to 0.
if ( $billing_cycles <= 0 ) {
return null;
}
// Calculate cancel_at timestamp based on interval.
$current_time = time();
$cancel_at = null;
switch ( $subscription_interval ) {
case 'day':
// Add days: cycles * 1 day.
$cancel_at = strtotime( "+{$billing_cycles} days", $current_time );
break;
case 'week':
// Add weeks: cycles * 7 days.
$cancel_at = strtotime( "+{$billing_cycles} weeks", $current_time );
break;
case 'month':
// Add months: cycles * 1 month.
$cancel_at = strtotime( "+{$billing_cycles} months", $current_time );
break;
case 'quarter':
// Add quarters: cycles * 3 months.
$total_months = $billing_cycles * 3;
$cancel_at = strtotime( "+{$total_months} months", $current_time );
break;
case 'year':
// Add years: cycles * 1 year.
$cancel_at = strtotime( "+{$billing_cycles} years", $current_time );
break;
default:
// Invalid interval, return null.
return null;
}
return $cancel_at;
}
/**
* Handle form submit and update payment entries with entry_id
*
* This function is called after a form submission to link the created entry
* with any associated Stripe payment records. It matches payment entries
* by form_id and updates them with the newly created entry_id.
*
* @param array<string,mixed> $form_submit_response The form submission response containing entry_id and form_id.
* @since 2.0.0
* @return void
*/
public function update_payment_entry_id_form_submit( $form_submit_response ) {
// Check if entry_id exists in the form_submit_response.
if ( ! empty( $form_submit_response['entry_id'] ) && ! empty( $this->stripe_payment_entries ) ) {
$entry_id = is_numeric( $form_submit_response['entry_id'] ) ? intval( $form_submit_response['entry_id'] ) : 0;
// Loop through stored payment entries to update with entry_id.
foreach ( $this->stripe_payment_entries as $stripe_payment_entry ) {
if ( ! empty( $stripe_payment_entry['payment_id'] ) && ! empty( $stripe_payment_entry['form_id'] ) ) {
// Check if form_id matches.
$stored_form_id = isset( $stripe_payment_entry['form_id'] ) && ! empty( $stripe_payment_entry['form_id'] ) && is_numeric( $stripe_payment_entry['form_id'] ) ? intval( $stripe_payment_entry['form_id'] ) : 0;
$response_form_id = isset( $form_submit_response['form_id'] ) && ! empty( $form_submit_response['form_id'] ) && is_numeric( $form_submit_response['form_id'] ) ? intval( $form_submit_response['form_id'] ) : 0;
$payment_id = is_string( $stripe_payment_entry['payment_id'] ) ? sanitize_text_field( $stripe_payment_entry['payment_id'] ) : '';
if ( ! empty( $stored_form_id ) && $stored_form_id === $response_form_id ) {
// Update the payment entry with the entry_id.
$this->update_payment_entry_id( $payment_id, $entry_id );
}
} elseif ( ! empty( $stripe_payment_entry['subscription_id'] ) && ! empty( $stripe_payment_entry['form_id'] ) ) {
// Check if form_id matches for subscription-based payment.
$stored_form_id = isset( $stripe_payment_entry['form_id'] ) && ! empty( $stripe_payment_entry['form_id'] ) && is_numeric( $stripe_payment_entry['form_id'] ) ? intval( $stripe_payment_entry['form_id'] ) : 0;
$response_form_id = isset( $form_submit_response['form_id'] ) && ! empty( $form_submit_response['form_id'] ) && is_numeric( $form_submit_response['form_id'] ) ? intval( $form_submit_response['form_id'] ) : 0;
$subscription_id = is_string( $stripe_payment_entry['subscription_id'] ) ? sanitize_text_field( $stripe_payment_entry['subscription_id'] ) : '';
if ( ! empty( $stored_form_id ) && $stored_form_id === $response_form_id ) {
// Update the payment entry with the entry_id using subscription_id.
$this->update_payment_entry_id_by_subscription_id( $subscription_id, $entry_id );
}
}
}
}
}
/**
* Add payment entry for later linking with form submission.
*
* Allows payment gateways (Stripe, PayPal, etc.) to register their entries
* for linking with form submissions. The entries are stored in memory and
* linked when the form is successfully submitted.
*
* @param array<string,mixed> $entry Payment entry containing payment_id, block_id, and form_id.
* @since 2.0.0
* @return void
*/
public function add_payment_entry_for_linking( $entry ) {
if ( ! empty( $entry ) && is_array( $entry ) ) {
$this->stripe_payment_entries[] = $entry;
}
}
/**
* Filter callback to determine if a payment field should be included in all data output.
*
* Excludes payment-related fields (like Stripe payment blocks) from being
* rendered in submission summaries, emails, exports, etc., as these fields
* serve as backend tracking data instead of user input.
*
* @since 2.0.0
*
* @param bool $should_add_field_row Whether this row should be output.
* @param array<string | mixed> $args Args describing the field row. Should contain 'block_name'.
* @return bool False for payment blocks; otherwise, original filter value.
*/
public function skip_payment_fields_from_all_data( $should_add_field_row, $args ) {
// Check if the block is a payment block by inspecting the block name.
$block_name = isset( $args['block_name'] ) && is_string( $args['block_name'] ) ? $args['block_name'] : '';
if ( 'srfm-payment' === $block_name ) {
return false;
}
return $should_add_field_row;
}
/**
* Skip payment fields from submission data.
*
* This function checks if a field is a payment field by validating its key prefix.
* Payment fields have keys that start with 'srfm-payment-' and should be skipped
* from certain data operations.
*
* @param bool $default_value The default skip value.
* @param array<mixed> $args Field arguments containing 'key', 'slug', and 'value'.
* @since 2.0.0
* @return bool True if the field should be skipped (is a payment field), false otherwise.
*/
public function skip_payment_fields_from_submission_data( $default_value, $args ) {
// Validate that args is an array and has the 'key' parameter.
if ( ! is_array( $args ) || ! isset( $args['key'] ) || ! is_string( $args['key'] ) ) {
return $default_value;
}
// Check if the key starts with 'srfm-payment-' to identify payment fields.
if ( 0 === strpos( $args['key'], 'srfm-payment-' ) ) {
return true;
}
return $default_value;
}
/**
* Skip payment fields from sample data.
*
* This function determines if a field associated with a "srfm/payment" block
* should be skipped when processing sample data. If the provided arguments
* specify a block with the name 'srfm/payment', the function returns true to
* indicate that the field should be skipped. Otherwise, it returns the given
* default value.
*
* @param bool $default_value The default skip value.
* @param array<mixed> $args Field arguments containing at least 'block_name'.
* @since 2.0.0
* @return bool True if the field should be skipped (is a payment block), false otherwise.
*/
public function skip_payment_fields_from_sample_data( $default_value, $args ) {
if ( ! is_array( $args ) || ! isset( $args['block_name'] ) || ! is_string( $args['block_name'] ) ) {
return $default_value;
}
if ( 'srfm/payment' === $args['block_name'] ) {
return true;
}
return $default_value;
}
/**
* Verify payment intent status
*
* @param array<mixed> $payment_value Payment value.
* @param string $payment_id Payment ID.
* @param string $block_id Block ID.
* @param array<mixed> $form_data Form data.
*
* @since 2.0.0
* @return void|array<mixed>
*/
private function verify_stripe_payment_intent_and_save( $payment_value, $payment_id, $block_id, $form_data ) {
try {
$payment_mode = Stripe_Helper::get_stripe_mode();
$secret_key = Stripe_Helper::get_stripe_secret_key();
if ( empty( $secret_key ) ) {
return [
'error' => __( 'Stripe secret key not found.', 'sureforms' ),
];
}
// Verify payment intent with comprehensive validation including form data.
// BOTH MODE: pass 'one-time' so per-type amount config is used for verification.
$verification_result = Payment_Helper::verify_payment_intent( $block_id, $payment_id, $form_data, 'one-time' );
if ( false === $verification_result['valid'] ) {
return [
'error' => $verification_result['message'],
];
}
$get_stripe_account_id = Stripe_Helper::get_stripe_account_id();
// Retrieve confirmed payment intent status.
$retrieve_body = apply_filters(
'srfm_retrieve_payment_intent_data',
[
'secret_key' => $secret_key,
'payment_intent_id' => $payment_id,
'stripe_account_id' => $get_stripe_account_id,
'plugin_name' => 'SureForms',
]
);
$retrieve_body = wp_json_encode( $retrieve_body );
$retrieve_body = is_string( $retrieve_body ) ? $retrieve_body : '';
$retrieve_body = base64_encode( $retrieve_body ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
if ( empty( $retrieve_body ) ) {
return [
'error' => __( 'Failed to retrieve payment intent.', 'sureforms' ),
];
}
// Call middleware retrieve endpoint to get confirmed payment intent.
$retrieve_response = wp_remote_post(
Stripe_Helper::middle_ware_base_url() . 'payment-intent/capture',
[
'timeout' => 60,
'body' => $retrieve_body,
'headers' => [
'Content-Type' => 'application/json',
],
]
);
if ( is_wp_error( $retrieve_response ) ) {
return [
'error' => __( 'Failed to retrieve payment intent.', 'sureforms' ),
];
}
$confirmed_payment_intent = json_decode( wp_remote_retrieve_body( $retrieve_response ), true );
if ( empty( $confirmed_payment_intent ) && ! is_array( $confirmed_payment_intent ) ) {
return [
'error' => __( 'Failed to retrieve payment intent.', 'sureforms' ),
];
}
// Strict type validation and array check to resolve phpstan errors.
if ( is_array( $confirmed_payment_intent ) && isset( $confirmed_payment_intent['status'] ) && 'error' === $confirmed_payment_intent['status'] ) {
return [
'error' => __( 'Failed to retrieve payment intent.', 'sureforms' ),
];
}
// Check if payment was actually confirmed successfully, safely.
$confirmed_status = is_array( $confirmed_payment_intent ) && isset( $confirmed_payment_intent['status'] ) ? (string) $confirmed_payment_intent['status'] : '';
if ( ! in_array( $confirmed_status, [ 'succeeded', 'requires_capture' ], true ) ) {
return [
'error' => __( 'Payment was not confirmed successfully.', 'sureforms' ),
];
}
$entry_data = [];
$form_id = isset( $form_data['form-id'] ) && ! empty( $form_data['form-id'] ) && is_numeric( $form_data['form-id'] ) ? intval( $form_data['form-id'] ) : 0;
$confirm_payment_status = is_array( $confirmed_payment_intent ) && isset( $confirmed_payment_intent['status'] ) && ! empty( $confirmed_payment_intent['status'] ) ? (string) $confirmed_payment_intent['status'] : '';
$confirm_payment_amount = is_array( $confirmed_payment_intent ) && isset( $confirmed_payment_intent['amount'] ) && ! empty( $confirmed_payment_intent['amount'] ) ? intval( $confirmed_payment_intent['amount'] ) : 0;
$confirm_payment_currency = is_array( $confirmed_payment_intent ) && isset( $confirmed_payment_intent['currency'] ) && ! empty( $confirmed_payment_intent['currency'] ) ? (string) $confirmed_payment_intent['currency'] : 'usd';
$confirm_payment_id = is_array( $confirmed_payment_intent ) && isset( $confirmed_payment_intent['id'] ) && ! empty( $confirmed_payment_intent['id'] ) ? (string) $confirmed_payment_intent['id'] : '';
// Defense-in-depth: re-validate the amount Stripe actually charged against the form's
// server-side configuration — not only the amount recorded when the intent was created.
$charged_amount = Stripe_Helper::amount_from_stripe_format( $confirm_payment_amount, $confirm_payment_currency );
$charge_validation = Payment_Helper::validate_amount_against_config( $block_id, $form_id, $form_data, $charged_amount, 'one-time' );
if ( false === $charge_validation['valid'] ) {
return [
'error' => $charge_validation['message'],
];
}
// Extract customer data.
$customer_data = $this->extract_customer_data( $payment_value );
// update payment status and save to the payment entries table.
$entry_data['form_id'] = $form_id;
$entry_data['block_id'] = $block_id;
$entry_data['status'] = $confirm_payment_status;
$entry_data['total_amount'] = Stripe_Helper::amount_from_stripe_format( $confirm_payment_amount, $confirm_payment_currency );
$entry_data['currency'] = $confirm_payment_currency;
$entry_data['entry_id'] = 0;
$entry_data['gateway'] = 'stripe';
$entry_data['type'] = 'payment';
$entry_data['mode'] = $payment_mode;
$entry_data['transaction_id'] = $confirm_payment_id;
$entry_data['srfm_txn_id'] = ''; // Will be updated after getting payment entry ID.
$entry_data['customer_email'] = $customer_data['email'];
$entry_data['customer_name'] = $customer_data['name'];
$entry_data['customer_id'] = $customer_data['customer_id'];
$entry_data['payment_data'] = [
'payment_value' => $payment_value,
];
// Get user ID if logged in.
$user_id = get_current_user_id();
/* translators: %d: User ID */
$user_info = $user_id > 0 ? sprintf( __( 'User ID: %d', 'sureforms' ), $user_id ) : __( 'Guest User', 'sureforms' );
// Add initial log entry for audit trail.
$entry_data['log'] = [
[
'title' => __( 'Payment Verification', 'sureforms' ),
'created_at' => current_time( 'mysql' ),
'messages' => [
/* translators: %s: Stripe transaction ID */
sprintf( __( 'Transaction ID: %s', 'sureforms' ), $confirm_payment_id ),
/* translators: %s: Payment gateway name. */
sprintf( __( 'Payment Gateway: %s', 'sureforms' ), 'Stripe' ),
/* translators: %1$s: amount, %2$s: currency. */
sprintf( __( 'Amount: %1$s %2$s', 'sureforms' ), number_format( Stripe_Helper::amount_from_stripe_format( $confirm_payment_amount, $confirm_payment_currency ), 2 ), strtoupper( $confirm_payment_currency ) ),
/* translators: %s: payment status */
sprintf( __( 'Status: %s', 'sureforms' ), ucfirst( str_replace( '_', ' ', $confirm_payment_status ) ) ),
$user_info,
/* translators: %s: payment mode */
sprintf( __( 'Mode: %s', 'sureforms' ), ucfirst( $payment_mode ) ),
],
],
];
$get_payment_entry_id = Payments::add( $entry_data );
if ( $get_payment_entry_id ) {
// Generate unique payment ID using the auto-increment ID and update the entry.
$unique_payment_id = Stripe_Helper::generate_unique_payment_id( $get_payment_entry_id );
Payments::update( $get_payment_entry_id, [ 'srfm_txn_id' => $unique_payment_id ] );
$add_in_static_value = [
'payment_id' => $confirm_payment_id,
'block_id' => $block_id,
'form_id' => $form_id,
];
$this->stripe_payment_entries[] = $add_in_static_value;
// Clean up transient after successful verification to prevent reuse.
Payment_Helper::delete_payment_intent_metadata( $block_id, $payment_id );
return [
'payment_id' => $get_payment_entry_id,
];
}
} catch ( \Exception $e ) {
return [
'error' => $e->getMessage(),
];
}
}
/**
* Get or create Stripe customer
*
* @param array<string,string> $customer_data Customer data containing 'email' and 'name' from POST.
* @since 2.0.0
* @return string|false Customer ID on success, false on failure.
*/
private function get_or_create_stripe_customer( $customer_data = [] ) {
$current_user = wp_get_current_user();
if ( $current_user->ID > 0 ) {
// Logged-in user - check for existing customer ID in user meta.
$customer_id = get_user_meta( $current_user->ID, 'srfm_stripe_customer_id', true );
if ( ! empty( $customer_id ) && is_string( $customer_id ) && $this->verify_stripe_customer( $customer_id ) ) {
return $customer_id;
}
// Create new customer for logged-in user.
return $this->create_stripe_customer_for_user( $current_user, $customer_data );
}
// Non-logged-in user - create temporary customer.
return $this->create_stripe_customer_for_guest( $customer_data );
}
/**
* Create Stripe customer for logged-in user
*
* @param \WP_User $user WordPress user object.
* @param array<string,string> $post_customer_data Customer data from POST containing 'email' and 'name'.
* @since 2.0.0
* @return string|false Customer ID on success, false on failure.
* @throws \Exception When Stripe API request fails.
*/
private function create_stripe_customer_for_user( $user, $post_customer_data = [] ) {
try {
// Use POST email if provided, else use logged-in user email.
$customer_email = ! empty( $post_customer_data['email'] ) ? $post_customer_data['email'] : $user->user_email;
// Use POST name if provided, else use logged-in user name.
$customer_name = ! empty( $post_customer_data['name'] ) ? $post_customer_data['name'] : ( trim( $user->first_name . ' ' . $user->last_name ) );
$customer_name = ! empty( $customer_name ) ? $customer_name : $user->display_name;
// Build description with provided email and name.
$description_parts = [];
if ( ! empty( $customer_email ) ) {
$description_parts[] = $customer_email;
}
if ( ! empty( $customer_name ) ) {
$description_parts[] = $customer_name;
}
$description = ! empty( $description_parts ) ? implode( ', ', $description_parts ) : sprintf( 'WordPress User ID: %d', $user->ID );
$customer_data = [
'email' => $customer_email,
'name' => $customer_name,
'description' => $description,
'metadata' => [
'source' => 'SureForms',
'wp_user_id' => $user->ID,
'wp_username' => $user->user_login,
'wp_user_email' => $user->user_email,
],
];
$customer_response = Stripe_Helper::stripe_api_request( 'customers', 'POST', $customer_data );
if ( ! $customer_response['success'] || empty( $customer_response['data']['id'] ) ) {
throw new \Exception( __( 'Failed to create Stripe customer.', 'sureforms' ) );
}
$customer = $customer_response['data'];
// Save customer ID to user meta for future use.
update_user_meta( $user->ID, 'srfm_stripe_customer_id', $customer['id'] );
return $customer['id'];
} catch ( \Exception $e ) {
return false;
}
}
/**
* Create Stripe customer for guest user
*
* @param array<string,string> $post_customer_data Customer data from POST containing 'email' and 'name'.
* @since 2.0.0
* @return string|false Customer ID on success, false on failure.
* @throws \Exception When Stripe API request fails.
*/
private function create_stripe_customer_for_guest( $post_customer_data = [] ) {
try {
// Use email and name from POST data.
$customer_email = ! empty( $post_customer_data['email'] ) ? sanitize_email( $post_customer_data['email'] ) : '';
$customer_name = ! empty( $post_customer_data['name'] ) ? sanitize_text_field( $post_customer_data['name'] ) : '';
// Build description with provided email and name.
$description_parts = [];
if ( ! empty( $customer_email ) ) {
$description_parts[] = $customer_email;
}
if ( ! empty( $customer_name ) ) {
$description_parts[] = $customer_name;
}
$description = ! empty( $description_parts ) ? implode( ', ', $description_parts ) : 'Guest User - SureForms Subscription';
$customer_data = [
'description' => $description,
'metadata' => [
'source' => 'SureForms',
'user_type' => 'guest',
'created_at' => current_time( 'mysql' ),
'ip_address' => $this->get_user_ip(),
],
];
// Add email if available from POST data.
if ( ! empty( $customer_email ) ) {
$customer_data['email'] = $customer_email;
$customer_data['metadata']['form_email'] = $customer_email;
}
// Add name if available from POST data.
if ( ! empty( $customer_name ) ) {
$customer_data['name'] = $customer_name;
$customer_data['metadata']['form_name'] = $customer_name;
}
$customer_response = Stripe_Helper::stripe_api_request( 'customers', 'POST', $customer_data );
if ( ! $customer_response['success'] || empty( $customer_response['data']['id'] ) ) {
throw new \Exception( __( 'Failed to create Stripe guest customer.', 'sureforms' ) );
}
$customer = $customer_response['data'];
return $customer['id'];
} catch ( \Exception $e ) {
return false;
}
}
/**
* Verify Stripe customer exists
*
* @param string $customer_id Stripe customer ID.
* @since 2.0.0
* @return bool True if customer exists, false otherwise.
*/
private function verify_stripe_customer( $customer_id ) {
try {
$customer_response = Stripe_Helper::stripe_api_request( 'customers', 'GET', [], $customer_id );
if ( ! $customer_response['success'] ) {
return false;
}
$customer = $customer_response['data'] ?? [];
/**
* Stripe API returns customer object with the following structure:
* {
* "id": "cus_Syq4hfWO9S5XC2",
* "object": "customer",
* "deleted": true // Present and true only if customer is deleted
* }
*
* When a customer is deleted, the 'deleted' property is set to true.
* Active customers do not have this property or it's set to false.
*/
$is_deleted_customer = isset( $customer['deleted'] ) && true === $customer['deleted'];
return ! empty( $customer['id'] ) && false === $is_deleted_customer;
} catch ( \Exception $e ) {
return false;
}
}
/**
* Get user IP address
*
* @since 2.0.0
* @return string User IP address.
*/
private function get_user_ip() {
// Check for various IP address headers.
$ip_keys = [ 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR' ];
foreach ( $ip_keys as $key ) {
if ( ! empty( $_SERVER[ $key ] ) ) {
$ip = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) );
// Handle comma-separated IPs (from proxies).
if ( strpos( $ip, ',' ) !== false ) {
$ip = trim( explode( ',', $ip )[0] );
}
if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
return $ip;
}
}
}
return '127.0.0.1'; // Fallback.
}
/**
* Update payment entry with entry_id
*
* @param string $payment_id Payment intent ID.
* @param int $entry_id Entry ID to update.
* @since 2.0.0
* @return bool True if payment entry updated, false otherwise.
*/
private function update_payment_entry_id( $payment_id, $entry_id ) {
// Find the payment entry by transaction_id.
$payment_entries = Payments::get_instance()->get_results(
[ 'transaction_id' => $payment_id ],
'id'
);
if ( ! empty( $payment_entries ) && is_array( $payment_entries ) && isset( $payment_entries[0] ) && is_array( $payment_entries[0] ) && isset( $payment_entries[0]['id'] ) ) {
$payment_entry_id = intval( $payment_entries[0]['id'] );
// Update the payment entry with entry_id using Payments class.
$updated = Payments::update( $payment_entry_id, [ 'entry_id' => $entry_id ] );
if ( $updated ) {
$this->maybe_fire_payment_completed( $payment_entry_id );
return true;
}
return false;
}
return false;
}
/**
* Fire the `srfm_payment_completed` action for a freshly linked payment.
*
* Called right after a payment row is linked to its form entry, so `entry_id`
* (and therefore the submitting user) is resolvable. Gated on the `succeeded`
* status so consumers never grant access for pending, failed or refunded
* payments.
*
* @param int $payment_entry_id Primary key of the linked `sureforms_payments` row.
* @since 2.12.0
* @return void
*/
private function maybe_fire_payment_completed( $payment_entry_id ) {
$payment = Payments::get( $payment_entry_id );
if ( ! is_array( $payment ) ) {
return;
}
$status = ! empty( $payment['status'] ) && is_string( $payment['status'] ) ? $payment['status'] : '';
if ( 'succeeded' !== $status ) {
return;
}
/**
* Fires when a SureForms payment reaches the `succeeded` state and has been
* linked to its form entry — a one-time payment, or the initial charge of a
* subscription.
*
* @param array<string, mixed> $payment Payment record (a `sureforms_payments` row).
* @param array<string, mixed> $context Resolved context: form_id, entry_id,
* user_id (0 for guests), customer_email,
* type, gateway, mode.
* @since 2.12.0
*/
do_action( 'srfm_payment_completed', $payment, Payment_Helper::build_payment_context( $payment ) );
}
/**
* Update payment entry with entry_id by subscription_id.
*
* Similar to update_payment_entry_id but looks up payment records by subscription_id
* instead of transaction_id. This is useful for subscription payments (PayPal, Stripe)
* where the subscription_id is available before the transaction_id.
*
* @param string $subscription_id The subscription ID from payment gateway.
* @param int $entry_id The form entry ID to link with payment.
* @since 2.4.0
* @return bool True if payment entry updated, false otherwise.
*/
private function update_payment_entry_id_by_subscription_id( $subscription_id, $entry_id ) {
// Find the payment entry by subscription_id.
$payment_entries = Payments::get_instance()->get_results(
[ 'subscription_id' => $subscription_id ],
'id'
);
if ( ! empty( $payment_entries ) && is_array( $payment_entries ) && isset( $payment_entries[0] ) && is_array( $payment_entries[0] ) && isset( $payment_entries[0]['id'] ) ) {
$payment_entry_id = intval( $payment_entries[0]['id'] );
// Update the payment entry with entry_id using Payments class.
$updated = Payments::update( $payment_entry_id, [ 'entry_id' => $entry_id ] );
if ( $updated ) {
$this->maybe_fire_payment_completed( $payment_entry_id );
return true;
}
return false;
}
return false;
}
/**
* Extract customer name and email from form data
*
* Uses the payment block's customerNameField and customerEmailField attributes
* to find the corresponding field slugs, then extracts the values from form data.
*
* @param array<string,mixed> $input_value Input value.
* @since 2.0.0
* @return array{name: string, email: string, customer_id: string} Customer data array.
*/
private function extract_customer_data( $input_value ) {
$email = ! empty( $input_value['email'] ) && is_string( $input_value['email'] ) ? sanitize_email( $input_value['email'] ) : '';
return [
'name' => ! empty( $input_value['name'] ) && is_string( $input_value['name'] ) ? sanitize_text_field( $input_value['name'] ) : '',
'email' => $email,
'customer_id' => ! empty( $input_value['customerId'] ) && is_string( $input_value['customerId'] ) ? sanitize_text_field( $input_value['customerId'] ) : '',
];
}
}
Nama
Tipe
Ukuran
Diubah
Aksi
📁 admin
dir
—
2026-06-25 16:24
📁 stripe
dir
—
2026-06-25 16:24
🐘 front-end.php
php
65.1 KB
2026-06-25 16:24
🐘 payment-helper.php
php
66.2 KB
2026-06-25 16:24
🐘 payment-history-shortcode.php
php
40 KB
2026-06-25 16:24
🐘 payments.php
php
1.6 KB
2026-06-25 16:24