namespace WPForms\Integrations\Stripe;
use Stripe\Exception\ApiErrorException;
use WPForms\Helpers\Transient;
use WPForms\Vendor\Stripe\SubscriptionSchedule;
* Stripe payment processing.
* Form Stripe payment settings.
* Sanitized submitted field values and data.
* Form data and settings.
* Whether the payment has been processed.
protected $is_payment_processed = false;
* Save matched subscription settings.
private $subscription_settings = [];
* Save the matched plan id.
* @param Api\ApiInterface $api Api interface.
public function init( $api ) {
private function hooks() {
add_action( 'wpforms_process', [ $this, 'process_entry' ], 10, 3 );
add_action( 'wpforms_process_payment_saved', [ $this, 'process_payment_saved' ], 10, 3 );
add_action( 'wpformsstripe_api_common_set_error_from_exception', [ $this, 'process_card_error' ] );
add_filter( 'wpforms_forms_submission_prepare_payment_data', [ $this, 'prepare_payment_data' ] );
add_filter( 'wpforms_forms_submission_prepare_payment_meta', [ $this, 'prepare_payment_meta' ], 10, 3 );
add_action( 'wpforms_process_entry_saved', [ $this, 'process_entry_data' ], 10, 4 );
* Check if a payment exists with an entry, if so validate and process.
* @param array $fields Final/sanitized submitted field data.
* @param array $entry Copy of original $_POST.
* @param array $form_data Form data and settings.
public function process_entry( $fields, $entry, $form_data ) {
// Check if payment method exists and is enabled.
if ( ! Helpers::has_stripe_enabled( [ $form_data ] ) ) {
$this->form_id = (int) $form_data['id'];
$this->form_data = $form_data;
$this->settings = $form_data['payments']['stripe'];
$this->amount = wpforms_get_total_payment( $this->fields );
$this->rate_limit = new RateLimit();
$this->rate_limit->init();
if ( $this->is_process_entry_error() ) {
if ( $this->is_submitted_payment_data_corrupted( $entry ) ) {
$this->api->set_payment_tokens( $entry );
$error = $this->get_entry_errors();
// Before proceeding, check if any basic errors were detected.
$this->log_error( $error );
$this->display_error( $error );
$this->process_payment();
* Bypass captcha if payment has been processed.
* @param bool $bypass_captcha Whether to bypass captcha.
public function bypass_captcha( $bypass_captcha ) {
_deprecated_function( __METHOD__, '1.9.6 of the WPForms plugin' );
return $this->is_payment_processed;
* Check on process entry errors.
protected function is_process_entry_error() {
// Check for processing errors.
if ( ! empty( wpforms()->obj( 'process' )->errors[ $this->form_id ] ) || ! $this->is_card_field_visibility_ok() ) {
if ( ! $this->is_rate_limit_ok() ) {
wpforms()->obj( 'process' )->errors[ $this->form_id ]['footer'] = esc_html__( 'Unable to process payment, please try again later.', 'wpforms-lite' );
* Add meta for a successful payment.
* @param array $payment_meta Payment meta.
* @param array $fields Final/sanitized submitted field data.
* @param array $form_data Form data and settings.
* @noinspection PhpMissingParamTypeInspection
* @noinspection PhpUnusedParameterInspection
public function prepare_payment_meta( $payment_meta, $fields, $form_data ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
$payment = $this->api->get_payment();
if ( empty( $payment->id ) ) {
$charge_details = $this->api->get_charge_details( [ 'type', 'name', 'last4', 'brand', 'exp_month', 'exp_year' ] );
$payment_meta['method_type'] = $this->get_payment_type( $charge_details );
$payment_meta['customer_name'] = $this->get_customer_name();
$payment_meta['customer_email'] = $this->get_customer_email();
$subscription = $this->api->get_subscription();
if ( ! empty( $subscription->id ) ) {
$payment_meta['subscription_period'] = sanitize_text_field( $this->subscription_settings['period'] );
if ( ! empty( $charge_details['brand'] ) ) {
$payment_meta['credit_card_method'] = $charge_details['brand'];
if ( ! empty( $charge_details['name'] ) ) {
$payment_meta['credit_card_name'] = $charge_details['name'];
if ( ! empty( $charge_details['last4'] ) ) {
$payment_meta['credit_card_last4'] = $charge_details['last4'];
if ( ! empty( $charge_details['exp_month'] ) && ! empty( $charge_details['exp_year'] ) ) {
$payment_meta['credit_card_expires'] = sprintf( '%s/%s', $charge_details['exp_month'], $charge_details['exp_year'] );
'value' => $payment->object === 'payment_intent' ? sprintf( 'Stripe payment intent created. (Payment Intent ID: %s)', $payment->id ) : 'Stripe payment was created.',
'date' => gmdate( 'Y-m-d H:i:s' ),
$payment_meta['log'] = wp_json_encode( $log );
* Get payment method type.
* @param array $charge_details Get details from a saved Charge object.
private function get_payment_type( $charge_details ) {
if ( empty( $charge_details['last4'] ) ) {
if ( ! empty( $charge_details['type'] ) ) {
return sanitize_text_field( $charge_details['type'] );
* Add payment info for successful payment.
* @param int $payment_id Payment ID.
* @param array $fields Final/sanitized submitted field data.
* @param array $form_data Form data and settings.
public function process_payment_saved( $payment_id, $fields, $form_data ) {
$payment = $this->api->get_payment();
if ( empty( $payment->id ) ) {
$payment_url = add_query_arg(
'page' => 'wpforms-payments',
'payment_id' => $payment_id,
// Update the Stripe charge metadata to include the Payment ID.
$payment->metadata['payment_id'] = $payment_id;
$payment->metadata['payment_url'] = esc_url_raw( $payment_url );
// Clean up spam reason in case it was set before.
$payment->metadata['spam_reason'] = null;
$custom_metadata = $this->get_mapped_custom_metadata( 'payment' );
static function ( &$value, $key ) use ( $payment ) {
$payment->metadata[ $key ] = $value;
* Allow to add additional payment metadata to the Stripe payment.
* @param array $additional_meta Additional metadata.
* @param int $payment_id Payment ID.
* @param array $fields Final/sanitized submitted field data.
* @param array $form_data Form data and settings.
$additional_meta = (array) apply_filters( 'wpforms_integrations_stripe_process_additional_metadata', [], $payment_id, $fields, $form_data );
static function ( $meta, $key ) use ( &$payment ) {
$payment->metadata[ $key ] = $meta;
$payment->update( $payment->id, $payment->serializeParameters(), Helpers::get_auth_opts() );
$subscription = $this->api->get_subscription();
// Update the Stripe subscription metadata to include the Payment ID.
if ( ! empty( $subscription->id ) ) {
$subscription->metadata['payment_id'] = $payment_id;
$subscription->metadata['payment_url'] = esc_url_raw( $payment_url );
$this->maybe_set_subscription_schedule( $subscription );
// Clean up cycles value.
$subscription->metadata['cycles'] = null;
$subscription->update( $subscription->id, $subscription->serializeParameters(), Helpers::get_auth_opts() );
wpforms()->obj( 'payment_meta' )->add_log(
'Stripe charge processed. (Charge ID: %1$s)',
isset( $payment->latest_charge ) ? $payment->latest_charge : $payment->id
* Fire when processing is complete.
* @param array $fields Final/sanitized submitted field data.
* @param array $form_data Form data and settings.
* @param int $payment_id Payment ID.
* @param mixed $payment Stripe payment object.
* @param mixed $subscription Stripe subscription object.
* @param mixed $customer Stripe customer object.
do_action( 'wpforms_stripe_process_complete', $fields, $form_data, $payment_id, $payment, $subscription, $this->api->get_customer() ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
* Get mapped custom metadata.
* @param string $type Object type ( e.g 'customer', 'payment' ).
private function get_mapped_custom_metadata( string $type ): array {
$settings_key = ! is_null( $this->plan_id ) ? 'recurring_custom_metadata_' . $this->plan_id : 'custom_metadata';
if ( empty( $this->form_data['payments']['stripe'][ $settings_key ] ) ) {
foreach ( $this->form_data['payments']['stripe'][ $settings_key ] as $data ) {
// Skip if the field type not set or the meta-key is empty.
if ( $data['object_type'] !== $type || empty( $data['meta_key'] ) ) {
// Skip if either the key or value is empty.
if ( ! $data['meta_key'] || ! $data['meta_value'] ) {
$field_id = $data['meta_value'];
if ( ! isset( $this->fields[ $field_id ]['value'] ) || wpforms_is_empty_string( $this->fields[ $field_id ]['value'] ) ) {
// Add quantity for the field value.
if ( wpforms_payment_has_quantity( $this->fields[ $field_id ], $this->form_data ) ) {
$field_value = wpforms_payment_format_quantity( $this->fields[ $field_id ] );
$field_value = $this->fields[ $field_id ]['value'];
// Key length limited to 40 characters long by Stripe API.
$key = wp_html_excerpt( sanitize_text_field( $data['meta_key'] ), 40 );
// Check whether the meta-key is empty once again after sanitization.
// Value length limited to 500 characters long by Stripe API.
$metadata[ $key ] = wp_html_excerpt( wpforms_decode_string( $field_value ), 500 );
* Add details to payment data.
* @param array $payment_data Payment data args.
public function prepare_payment_data( $payment_data ) {
$payment = $this->api->get_payment();
if ( empty( $payment->id ) ) {
$customer = $this->api->get_customer();
$subscription = $this->api->get_subscription();
$payment_data['status'] = 'processed';
$payment_data['gateway'] = 'stripe';
$payment_data['mode'] = Helpers::get_stripe_mode();
$payment_data['transaction_id'] = sanitize_text_field( $payment->id );
$payment_data['customer_id'] = ! empty( $customer->id ) ? sanitize_text_field( $customer->id ) : '';
$payment_data['title'] = $this->get_payment_title();
if ( ! empty( $subscription->id ) ) {
$payment_data['subscription_id'] = sanitize_text_field( $subscription->id );
$payment_data['subscription_status'] = 'not-synced';