name = esc_html__( 'Stripe', 'forminator' );
try {
$stripe = new Forminator_Gateway_Stripe();
if ( $stripe->is_test_ready() || $stripe->is_live_ready() ) {
$this->is_connected = true;
}
} catch ( Forminator_Gateway_Exception $e ) {
$this->is_connected = false;
}
}
/**
* Field defaults
*
* @return array
*/
public function defaults() {
$default_currency = 'USD';
try {
$stripe = new Forminator_Gateway_Stripe();
$default_currency = $stripe->get_default_currency();
} catch ( Forminator_Gateway_Exception $e ) {
forminator_maybe_log( __METHOD__, $e->getMessage() );
}
return array(
'field_label' => esc_html__( 'Credit / Debit Card', 'forminator' ),
'mode' => 'test',
'currency' => $default_currency,
'amount_type' => 'fixed',
'logo' => '',
'company_name' => '',
'product_description' => '',
'customer_email' => '',
'receipt' => 'false',
'billing' => 'false',
'payment_api' => 'checkout_session',
'verify_zip' => 'false',
'card_icon' => 'true',
'language' => 'auto',
'options' => array(),
'base_class' => 'StripeElement',
'complete_class' => 'StripeElement--complete',
'empty_class' => 'StripeElement--empty',
'focused_class' => 'StripeElement--focus',
'invalid_class' => 'StripeElement--invalid',
'autofilled_class' => 'StripeElement--webkit-autofill',
'subscription_amount_type' => 'fixed',
'quantity_type' => 'fixed',
'payments' => array(
array(
'plan_name' => esc_html__( 'Plan 1', 'forminator' ),
'payment_method' => 'single',
'amount_type' => 'fixed',
'amount' => '',
'subscription_amount_type' => 'fixed',
'quantity_type' => 'fixed',
'quantity' => '1',
'bill_input' => '1',
),
),
);
}
/**
* Field front-end markup
*
* @param array $field Field.
* @param Forminator_Render_Form $views_obj Forminator_Render_Form object.
*
* @return mixed
*/
public function markup( $field, $views_obj ) {
$settings = $views_obj->model->settings;
$this->field = $field;
$this->form_settings = $settings;
$is_ocs = 'stripe-ocs' === $field['type'];
// Don't render stripe field if there is stripe-ocs field in the form.
if ( ! $is_ocs && $views_obj->has_field_type( 'stripe-ocs' ) ) {
return '';
}
$id = self::get_property( 'element_id', $field );
$description = self::get_property( 'description', $field, '' );
$descr_position = self::get_description_position( $field, $settings );
$label = esc_html( self::get_property( 'field_label', $field, '' ) );
$mode = self::get_property( 'mode', $field, 'test' );
$currency = self::get_property( 'currency', $field, $this->get_default_currency() );
$card_icon = self::get_property( 'card_icon', $field, true );
$verify_zip = self::get_property( 'verify_zip', $field, false );
$zip_field = self::get_property( 'zip_field', $field, '' );
$language = self::get_property( 'language', $field, 'auto' );
$base_class = self::get_property( 'base_class', $field, 'StripeElement' );
$complete_class = self::get_property( 'complete_class', $field, 'StripeElement--complete' );
$empty_class = self::get_property( 'empty_class', $field, 'StripeElement--empty' );
$focused_class = self::get_property( 'focused_class', $field, 'StripeElement--focus' );
$invalid_class = self::get_property( 'invalid_class', $field, 'StripeElement--invalid' );
$autofilled_class = self::get_property( 'autofilled_class', $field, 'StripeElement--webkit-autofill' );
$billing = self::get_property( 'billing', $field, false );
$billing_name = self::get_property( 'billing_name', $field, '' );
$billing_email = self::get_property( 'billing_email', $field, '' );
$billing_address = self::get_property( 'billing_address', $field, '' );
$receipt = self::get_property( 'receipt', $field, false );
$customer_email = self::get_property( 'customer_email', $field, '' );
$checkout_email_enabled = self::get_property( 'checkout_email_enabled', $field, '' );
$checkout_email = self::get_property( 'checkout_email', $field, '' );
$checkout_phone_enabled = self::get_property( 'checkout_phone_enabled', $field, '' );
$checkout_phone = self::get_property( 'checkout_phone', $field, '' );
$uniqid = Forminator_CForm_Front::$uid;
$id_prefix = $is_ocs ? 'payment-element' : 'card-element';
$full_id = $id_prefix . '-' . $uniqid;
$prefix = 'basic' === $settings['form-style'] ? 'basic-' : '';
$customer_email = forminator_clear_field_id( $customer_email );
$checkout_email_enabled = filter_var( $checkout_email_enabled, FILTER_VALIDATE_BOOLEAN );
$checkout_phone_enabled = filter_var( $checkout_phone_enabled, FILTER_VALIDATE_BOOLEAN );
$current_checkout_map = $checkout_email;
if ( $this->is_checkout_session( $field ) ) {
$form_fields = $views_obj->model->get_fields();
$form_id = $views_obj->model->id;
// Saved Checkout mappings can outlive the underlying form field, so resolve only active mappings here.
$checkout_email = $checkout_email_enabled
? forminator_get_active_mapped_field_id( $checkout_email, $form_fields, $form_id )
: '';
$checkout_phone = $checkout_phone_enabled
? forminator_get_active_mapped_field_id( $checkout_phone, $form_fields, $form_id )
: '';
$checkout_billing_mappings = $this->get_checkout_session_billing_mappings( $field, $views_obj->model, $form_id );
$billing_name = $checkout_billing_mappings['billing_name'];
$billing_address = $checkout_billing_mappings['billing_address'];
} else {
$checkout_email = '';
$checkout_phone = '';
}
$should_render_contact = $this->is_checkout_session( $field ) && empty( $checkout_email );
forminator_maybe_log(
__METHOD__,
array(
'action' => 'checkout_contact_element_render_check',
'checkout_email' => $checkout_email,
'current_map' => $current_checkout_map,
'should_render_contact' => $should_render_contact,
)
);
$custom_fonts = false;
$payment_api = self::get_payment_api( $field );
// Generate payment intent object.
$this->mode = $mode;
if ( isset( $settings[ $prefix . 'form-font-family' ] ) && 'custom' === $settings[ $prefix . 'form-font-family' ] ) {
$custom_fonts = true;
}
if ( ! isset( $settings['form-substyle'] ) ) {
$settings['form-substyle'] = 'default';
}
$data_font_family = 'inherit';
$data_font_size = '16px';
$data_font_weight = '400';
if ( ! empty( $settings[ $prefix . 'form-font-family' ] ) ) {
$field_font_family = $this->get_form_setting( $prefix . 'cform-input-font-family', $settings, $data_font_family );
$data_font_size = $this->get_form_setting( $prefix . 'cform-input-font-size', $settings, '16' ) . 'px';
$data_font_weight = $this->get_form_setting( $prefix . 'cform-input-font-weight', $settings, $data_font_weight );
if ( 'custom' === $field_font_family ) {
$data_font_family = $this->get_form_setting( $prefix . 'cform-input-custom-family', $settings, $data_font_family );
} else {
$data_font_family = $field_font_family;
}
}
$data_placeholder = '#888888';
$data_font_color = '#000000';
$data_font_color_focus = '#000000';
$data_font_color_error = '#000000';
$data_icon_color = '#777771';
$data_icon_color_hover = '#097BAA';
$data_icon_color_focus = '#097BAA';
$data_icon_color_error = '#E51919';
if ( ! empty( $settings[ $prefix . 'cform-color-settings' ] ) ) {
$data_placeholder = $this->get_form_setting( $prefix . 'input-placeholder', $settings, $data_placeholder );
$data_font_color = $this->get_form_setting( $prefix . 'input-color', $settings, $data_font_color );
$data_font_color_focus = $this->get_form_setting( $prefix . 'input-color', $settings, $data_font_color_focus );
$data_font_color_error = $this->get_form_setting( $prefix . 'input-color', $settings, $data_font_color_error );
$data_icon_color = $this->get_form_setting( $prefix . 'input-icon', $settings, $data_icon_color );
$data_icon_color_hover = $this->get_form_setting( $prefix . 'input-icon-hover', $settings, $data_icon_color_hover );
$data_icon_color_focus = $this->get_form_setting( $prefix . 'input-icon-focus', $settings, $data_icon_color_focus );
$data_icon_color_error = $this->get_form_setting( $prefix . 'label-validation-color', $settings, $data_icon_color_error );
}
$attr = array_merge(
$this->build_common_element_attributes( $uniqid, $currency, $mode, $is_ocs ),
array(
'data-payment-api' => $payment_api,
'data-card-icon' => filter_var( $card_icon, FILTER_VALIDATE_BOOLEAN ),
'data-veify-zip' => filter_var( $verify_zip, FILTER_VALIDATE_BOOLEAN ),
'data-zip-field' => esc_html( $zip_field ),
'data-language' => esc_html( $language ),
'data-base-class' => esc_html( $base_class ),
'data-complete-class' => esc_html( $complete_class ),
'data-empty-class' => esc_html( $empty_class ),
'data-focused-class' => esc_html( $focused_class ),
'data-invalid-class' => esc_html( $invalid_class ),
'data-autofilled-class' => esc_html( $autofilled_class ),
'data-billing' => filter_var( $billing, FILTER_VALIDATE_BOOLEAN ),
'data-billing-name' => esc_html( $billing_name ),
'data-billing-email' => esc_html( $billing_email ),
'data-billing-address' => esc_html( $billing_address ),
'data-receipt' => filter_var( $receipt, FILTER_VALIDATE_BOOLEAN ),
'data-receipt-email' => esc_html( $customer_email ),
'data-custom-fonts' => $custom_fonts,
'data-placeholder' => $data_placeholder,
'data-font-color' => $data_font_color,
'data-font-color-focus' => $data_font_color_focus,
'data-font-color-error' => $data_font_color_error,
'data-font-size' => $data_font_size,
'data-font-family' => $data_font_family,
'data-font-weight' => $data_font_weight,
'data-icon-color' => $data_icon_color,
'data-icon-color-hover' => $data_icon_color_hover,
'data-icon-color-focus' => $data_icon_color_focus,
'data-icon-color-error' => $data_icon_color_error,
)
);
if ( $is_ocs ) {
$elements_options = array(
'loader' => 'always',
'locale' => $language,
'paymentMethodCreation' => 'manual',
);
$variables = array(
'fontWeightNormal' => $data_font_weight,
'fontSizeBase' => $data_font_size,
'iconColor' => $data_icon_color,
'iconHoverColor' => $data_icon_color_hover,
'iconCardErrorColor' => $data_icon_color_error,
'iconCardCvcErrorColor' => $data_icon_color_error,
'colorTextPlaceholder' => $data_placeholder,
);
$custom_appearance = self::get_property( 'custom_appearance', $field, false );
if ( $custom_appearance ) {
$spacing = self::get_property( 'spacing_unit', $field, '' );
if ( $spacing ) {
$variables['spacingUnit'] = $spacing . 'px';
}
$border_radius = self::get_property( 'border_radius', $field, '' );
if ( $border_radius ) {
$variables['borderRadius'] = $border_radius . 'px';
}
$variables['colorPrimary'] = self::get_property( 'primary_color', $field, '' );
$variables['colorBackground'] = self::get_property( 'background_color', $field, '' );
$variables['colorText'] = self::get_property( 'text_color', $field, '' );
$variables['colorDanger'] = self::get_property( 'error', $field, '' );
}
// Remove empty values.
$variables = array_filter( $variables );
$appearance = array(
'theme' => self::get_property( 'theme', $field, 'stripe' ),
'variables' => $variables,
);
if ( $custom_fonts && $data_font_family ) {
$appearance['variables']['fontFamily'] = $data_font_family;
$elements_options['fonts'][] = array(
'family' => $data_font_family,
'cssSrc' => 'https://fonts.bunny.net/css?family=' . $data_font_family,
);
}
$elements_options['appearance'] = $appearance;
$dynamic_methods = self::get_property( 'automatic_payment_methods', $field, 'true' );
// Checkout Sessions manages payment method availability from the session configuration.
if ( 'checkout_session' !== $payment_api && 'false' === $dynamic_methods ) {
$elements_options['paymentMethodTypes'] = array( 'card' );
}
/**
* Filter Stripe OCS Elements options
*
* @since 1.38
*
* @param array $elements_options Elements options.
* @param array $field Field.
*/
$elements_options = apply_filters( 'forminator_field_stripe_ocs_elements_options', $elements_options, $field );
$checkout_elements_options = $elements_options;
unset(
$checkout_elements_options['paymentMethodTypes'],
$checkout_elements_options['paymentMethodCreation'],
$checkout_elements_options['locale']
);
$payment_options = array(
'layout' => self::get_layout( $field ),
);
if ( 'checkout_session' !== $payment_api && 'false' === $dynamic_methods ) {
$payment_options['wallets'] = array(
'applePay' => 'never',
'googlePay' => 'never',
);
}
/**
* Filter Stripe OCS Payment options
*
* @since 1.38
*
* @param array $payment_options Payment options.
* @param array $field Field.
*/
$payment_options = apply_filters( 'forminator_field_stripe_ocs_elements_options', $payment_options, $field );
$billing_phone = self::get_property( 'billing_phone', $field, '' );
$attr = array_merge(
$this->build_common_element_attributes( $uniqid, $currency, $mode, $is_ocs ),
array(
'data-elements-options' => wp_json_encode( $elements_options, JSON_PRETTY_PRINT ),
'data-checkout-elements-options' => wp_json_encode( $checkout_elements_options, JSON_PRETTY_PRINT ),
'data-payment-options' => wp_json_encode( $payment_options, JSON_PRETTY_PRINT ),
'data-payment-api' => $payment_api,
'data-adaptive-pricing' => self::is_adaptive_pricing_configured( $field ),
'data-dynamic-methods' => $dynamic_methods,
'data-checkout-email' => esc_html( $checkout_email ),
'data-checkout-phone' => esc_html( $checkout_phone ),
'data-checkout-required-message' => esc_html__( 'This field is required to complete your payment.', 'forminator' ),
'data-receipt' => filter_var( $receipt, FILTER_VALIDATE_BOOLEAN ),
'data-receipt-email' => esc_html( $customer_email ),
'data-billing' => filter_var( $billing, FILTER_VALIDATE_BOOLEAN ),
'data-billing-name' => esc_html( $billing_name ),
'data-billing-email' => esc_html( $billing_email ),
'data-billing-phone' => esc_html( $billing_phone ),
'data-billing-address' => esc_html( $billing_address ),
'data-return-url' => esc_url( self::get_return_url() ),
)
);
if ( $this->is_checkout_session( $field ) && forminator_show_stripe_developer_widget( $mode ) ) {
$attr['data-show-stripe-developer-widget'] = 'true';
}
}
if ( ! empty( $description ) ) {
$attr['aria-describedby'] = esc_attr( $full_id . '-description' );
}
/**
* Filter Stripe field attributes.
*
* @since 1.56
*
* @param array $attr Stripe field attributes.
* @param array $field Field settings.
* @param bool $is_ocs Whether Stripe is using the Optimized Checkout Suite.
*/
$attr = apply_filters( 'forminator_field_stripe_attributes', $attr, $field, $is_ocs );
$attributes = self::implode_attr( $attr );
$html = '
';
return apply_filters( 'forminator_field_stripe_markup', $html, $attr, $field );
}
/**
* Get layout Stripe settings
*
* @param array $field Field.
* @return array
*/
private static function get_layout( $field ) {
$layout = self::get_property( 'layout', $field, 'tabs' );
if ( 'accordion+radio' === $layout ) {
$radios = true;
$layout = 'accordion';
}
$data = array(
'type' => $layout,
'defaultCollapsed' => false,
);
if ( 'accordion' === $layout ) {
$data['spacedAccordionItems'] = false;
$data['radios'] = ! empty( $radios );
}
return $data;
}
/**
* Generate Payment Intent object
*
* @since 1.7.3
*
* @param int|float $amount Amount.
* @param array $field Field.
*
* @return mixed
*/
public function generate_paymentIntent( $amount, $field ) {
if ( $this->is_checkout_session( $field ) ) {
return $this->generate_checkout_session( $amount, $field );
}
$context = $this->build_payment_request_context( $field );
$metadata_data = $this->build_metadata_context( $field );
$currency = $context['currency'];
$description = $context['description'];
$company = $context['company'];
$key = $context['key'];
$metadata_object = $metadata_data['metadata'];
\Forminator\Stripe\Stripe::setApiKey( $key );
Forminator_Gateway_Stripe::set_stripe_app_info();
// Default options.
$options = array(
'amount' => $this->calculate_amount( $amount, $currency ),
'currency' => $currency,
'confirm' => false,
);
$dynamic_methods = self::get_property( 'automatic_payment_methods', $field, 'true' );
if ( 'false' === $dynamic_methods ) {
$options['payment_method_types'] = array( 'card' );
} else {
$options['automatic_payment_methods'] = array(
'enabled' => true,
);
$options['payment_method_options'] = array(
'wechat_pay' => array(
'client' => 'web', // Specify the client type.
),
);
}
if ( ! empty( Forminator_CForm_Front_Action::$prepared_data['paymentmethod'] ) ) {
$options['payment_method'] = Forminator_CForm_Front_Action::$prepared_data['paymentmethod'];
}
// Check if metadata is not empty and add it to the options.
if ( ! empty( $metadata_object ) ) {
$options['metadata'] = $metadata_object;
}
// Check if statement_description is not empty and add it to the options.
if ( ! empty( $company ) ) {
$options['statement_descriptor_suffix'] = $company;
}
// Check if description is not empty and add it to the options.
if ( ! empty( $description ) ) {
$options['description'] = $description;
}
$options = apply_filters( 'forminator_stripe_payment_intent_options', $options, $field );
try {
// Create Payment Intent object.
$intent = \Forminator\Stripe\PaymentIntent::create( $options );
} catch ( Exception $e ) {
forminator_maybe_log(
__METHOD__,
array(
'action' => 'PaymentIntent::create',
'error_class' => get_class( $e ),
'error_message' => $e->getMessage(),
'error_code' => $e->getCode(),
'options' => $options,
)
);
$response = array(
'message' => $e->getMessage(),
'errors' => array(),
'paymentPlan' => $this->payment_plan_hash,
);
wp_send_json_error( $response );
}
return $intent;
}
/**
* Check if the field uses Checkout Sessions
*
* @since 1.56
*
* @param array $field Field settings.
* @return bool
*/
public function is_checkout_session( $field ): bool {
return 'stripe-ocs' === self::get_property( 'type', $field, '' )
&& 'checkout_session' === self::get_payment_api( $field );
}
/**
* Get the configured Stripe payment API.
*
* @since 1.56
*
* @param array $field Field settings.
* @return string
*/
private static function get_payment_api( $field ): string {
$payment_api = self::get_property( 'payment_api', $field, 'checkout_session' );
// Force use of Checkout Sessions for the Stripe OCS field, regardless of the configured payment API.
if ( 'stripe-ocs' === self::get_property( 'type', $field, '' ) ) {
$payment_api = 'checkout_session';
}
return apply_filters( 'forminator_stripe_payment_api', $payment_api, $field );
}
/**
* Build shared Stripe metadata payload.
*
* @since 1.56
*
* @param array $field Field settings.
* @return array{form_id:string,metadata:array}
*/
private function build_metadata_context( $field ): array {
$metadata = self::get_property( 'options', $field, '' );
$metadata = is_array( $metadata ) ? $metadata : array();
$metadata_object = array();
foreach ( $metadata as $meta ) {
$label = trim( $meta['label'] );
$value = $meta['value'] ?? '';
// Stripe metadata entries cannot have both empty keys and values.
if ( '' === $label && '' === $value ) {
continue;
}
if ( '' === $label ) {
$label = $value;
}
$metadata_object[ $label ] = forminator_replace_form_data( '{' . $value . '}', Forminator_Front_Action::$module_object );
}
$form_id = ! empty( Forminator_Front_Action::$module_object->id ) ? (string) Forminator_Front_Action::$module_object->id : '';
if ( '' !== $form_id ) {
$metadata_object['forminator_form_id'] = $form_id;
}
return array(
'form_id' => $form_id,
'metadata' => $metadata_object,
);
}
/**
* Get the Forminator form ID stored in Stripe metadata.
*
* @since 1.56
*
* @param mixed $metadata Stripe metadata object.
* @return string
*/
public static function get_form_id_from_stripe_metadata( $metadata ): string {
if ( is_object( $metadata ) && isset( $metadata->forminator_form_id ) ) {
return (string) $metadata->forminator_form_id;
}
if ( is_array( $metadata ) && isset( $metadata['forminator_form_id'] ) ) {
return (string) $metadata['forminator_form_id'];
}
return '';
}
/**
* Prepare shared Stripe API context values.
*
* @since 1.56
*
* @param array $field Field settings.
* @return array{currency:string,mode:string,description:string,company:string,key:string}
*/
private function build_payment_request_context( $field ): array {
$currency = self::get_property( 'currency', $field, $this->get_default_currency() );
$mode = self::get_property( 'mode', $field, 'test' );
$description = self::get_property( 'product_description', $field, '' );
if ( ! empty( $description ) ) {
$description = forminator_replace_form_data( $description, Forminator_Front_Action::$module_object );
}
$company = self::get_statement_descriptor_suffix( $field, Forminator_Front_Action::$module_object );
$key = $this->get_secret_key( 'test' !== $mode );
return array(
'currency' => $currency,
'mode' => $mode,
'description' => $description,
'company' => $company,
'key' => $key,
);
}
/**
* Get statement descriptor suffix within Stripe's length limit.
*
* @since 1.56
*
* @param array $field Field settings.
* @param object|null $custom_form Custom form object.
* @return string
*/
public static function get_statement_descriptor_suffix( $field, $custom_form = null ): string {
$company = self::get_property( 'company_name', $field, '' );
if ( ! empty( $company ) && $custom_form ) {
$company = forminator_replace_form_data( $company, $custom_form );
}
if ( mb_strlen( $company ) > 22 ) {
$company = mb_substr( $company, 0, 19 ) . '...';
}
return $company;
}
/**
* Build shared Stripe element data attributes.
*
* @since 1.56
*
* @param string $uniqid Unique field id.
* @param string $currency Currency.
* @param string $mode Mode.
* @param bool $is_ocs Whether this is the OCS field variant.
* @return array
*/
private function build_common_element_attributes( $uniqid, $currency, $mode, $is_ocs ): array {
return array(
'data-field-id' => $uniqid,
'data-is-payment' => 'true',
'data-payment-type' => $this->type,
'data-is-ocs' => $is_ocs,
'data-secret' => '',
'data-paymentid' => '',
'data-currency' => strtolower( $currency ),
'data-key' => esc_html( $this->get_publishable_key( 'test' !== $mode ) ),
);
}
/**
* Resolve Checkout Session billing mappings that are safe to expose to the frontend.
*
* Stripe Checkout billing-name mapping should only be passed through when the
* paired billing address mapping is still active and its Country subfield is enabled.
*
* @since 1.56
*
* @param array $field Field settings.
* @param Forminator_Form_Model $form_model Form model.
* @param int $form_id Form ID.
* @return array{billing_name:string,billing_address:string}
*/
private function get_checkout_session_billing_mappings( $field, Forminator_Form_Model $form_model, $form_id ): array {
$billing = filter_var( self::get_property( 'billing', $field, false ), FILTER_VALIDATE_BOOLEAN );
if ( ! $billing ) {
return array(
'billing_name' => '',
'billing_address' => '',
);
}
$billing_name = self::get_property( 'billing_name', $field, '' );
$billing_address = self::get_property( 'billing_address', $field, '' );
$form_fields = $form_model->get_fields();
$billing_name = forminator_get_active_mapped_field_id( $billing_name, $form_fields, $form_id );
$billing_address = forminator_get_active_mapped_field_id( $billing_address, $form_fields, $form_id );
$has_valid_billing_address = ! empty( $billing_address ) && $this->address_field_has_country_subfield( $billing_address, $form_model );
if ( ! $has_valid_billing_address ) {
$billing_name = '';
$billing_address = '';
}
return array(
'billing_name' => $billing_name,
'billing_address' => $billing_address,
);
}
/**
* Check whether an address field has its Country subfield enabled.
*
* @since 1.56
*
* @param string $field_id Address field ID.
* @param Forminator_Form_Model $form_model Form model.
* @return bool
*/
private function address_field_has_country_subfield( $field_id, Forminator_Form_Model $form_model ): bool {
$field = $form_model->get_field( $field_id, false );
return $field && filter_var( $field->__get( 'address_country' ), FILTER_VALIDATE_BOOLEAN );
}
/**
* Generate Checkout Session object
*
* @since 1.56
*
* @param int|float $amount Amount.
* @param array $field Field.
* @param bool $send_json_error Whether to send a JSON error or return a WP_Error on failure.
*
* @return mixed|WP_Error
*/
private function generate_checkout_session( $amount, $field, $send_json_error = true ) {
$context = $this->build_payment_request_context( $field );
$metadata_data = $this->build_metadata_context( $field );
$currency = $context['currency'];
$description = $context['description'];
$company = $context['company'];
$key = $context['key'];
$language = self::get_property( 'language', $field, 'auto' );
$form_id = $metadata_data['form_id'];
$plan_name = $this->payment_plan['plan_name'] ?? esc_html__( 'Plan 1', 'forminator' );
$quantity = 1;
$dynamic_methods = self::get_property( 'automatic_payment_methods', $field, 'true' );
\Forminator\Stripe\Stripe::setApiKey( $key );
Forminator_Gateway_Stripe::set_stripe_app_info();
$options = array(
'line_items' => $this->build_checkout_session_line_items( $amount, $currency, $plan_name, $quantity ),
'mode' => 'payment',
'ui_mode' => 'elements',
'locale' => $language,
'return_url' => self::get_return_url(),
'client_reference_id' => $form_id,
);
if ( 'false' === $dynamic_methods ) {
$options['payment_method_types'] = array( 'card' );
} else {
$options['excluded_payment_method_types'] = self::get_checkout_session_excluded_payment_methods();
}
$checkout_phone_enabled = filter_var( self::get_property( 'checkout_phone_enabled', $field, false ), FILTER_VALIDATE_BOOLEAN );
$custom_form = Forminator_Base_Form_Model::get_model( $form_id );
$checkout_phone = $custom_form instanceof Forminator_Form_Model
? forminator_get_active_mapped_field_id( self::get_property( 'checkout_phone', $field, '' ), $custom_form->get_fields(), $form_id )
: '';
// Enable Stripe phone collection only when the saved mapping still points to an active Phone field.
if (
$checkout_phone_enabled
&& ! empty( $checkout_phone )
) {
$options['phone_number_collection'] = array(
'enabled' => true,
);
}
if ( self::is_adaptive_pricing_configured( $field ) ) {
$options['adaptive_pricing'] = array(
'enabled' => true,
);
}
if ( ! empty( $description ) ) {
// Set the PaymentIntent description because Stripe does not copy it from the product.
$options['payment_intent_data']['description'] = $description;
}
if ( ! empty( $company ) ) {
$options['payment_intent_data']['statement_descriptor_suffix'] = $company;
}
try {
$options = apply_filters( 'forminator_stripe_checkout_session_options', $options, $field );
$session = \Forminator\Stripe\Checkout\Session::create(
$options,
self::get_checkout_session_request_options()
);
self::set_pending_checkout_session( $session->id );
} catch ( Exception $e ) {
forminator_maybe_log(
__METHOD__,
array(
'action' => 'Checkout\\Session::create',
'error_class' => get_class( $e ),
'error_message' => $e->getMessage(),
'error_code' => $e->getCode(),
'options' => $options,
'request_options' => isset( $request_options ) ? $request_options : self::get_checkout_session_request_options(),
)
);
$stripe_code = $e instanceof \Forminator\Stripe\Exception\ApiErrorException ? $e->getStripeCode() : '';
$message = \Forminator\Stripe\ErrorObject::CODE_AMOUNT_TOO_SMALL === $stripe_code
? esc_html__( 'Unable to process checkout. Amount is below the minimum payment allowed.', 'forminator' )
: esc_html__( 'Unable to initialize checkout. Please try again.', 'forminator' );
$response = array(
'message' => $message,
'errors' => array(),
'paymentPlan' => $this->payment_plan_hash,
'exception_class' => get_class( $e ),
'stripe_code' => $stripe_code,
);
if ( ! $send_json_error ) {
return new WP_Error( 'forminator_stripe_error', $e->getMessage(), $response );
}
wp_send_json_error( $response );
}
return $session;
}
/**
* Build line items for Checkout Session requests.
*
* @since 1.56
*
* @param int|float $amount Charge amount.
* @param string $currency Currency.
* @param string $plan_name Product name.
* @param int $quantity Quantity.
* @return array>
*/
private function build_checkout_session_line_items( $amount, $currency, $plan_name, $quantity ): array {
return array(
array(
'price_data' => array(
'currency' => strtolower( $currency ),
'product_data' => array(
'name' => $plan_name,
),
'unit_amount' => $this->calculate_amount( $amount, $currency ),
),
'quantity' => max( 1, $quantity ),
),
);
}
/**
* Calculate Stripe amount
*
* @since 1.11
*
* @param int|float $amount Amount.
* @param string $currency Currency.
*
* @return int
*/
public function calculate_amount( $amount, $currency ) {
$zero_decimal_currencies = self::get_zero_decimal_currencies();
// Check if currency is zero decimal, then return the rounded whole-number amount.
if ( in_array( $currency, $zero_decimal_currencies, true ) ) {
return (int) round( (float) $amount );
}
// If JOD, amount needs to have 3 decimals and multiplied to 1000.
if ( 'JOD' === $currency ) {
$amount = number_format( $amount, 3, '.', '' );
return (int) round( (float) $amount * 1000 );
}
$amount = number_format( $amount, 2, '.', '' );
// Currency has decimals, multiply by 100.
return (int) round( (float) $amount * 100 );
}
/**
* Get a payment-mode Checkout Session amount that is only used to render Checkout Elements.
*
* @since 1.56
*
* @param int|float $amount Amount.
* @param string $currency Currency.
* @param array $field Field.
* @return int|float
*/
private function get_checkout_session_render_amount( $amount, $currency, $field ) {
$currency = strtoupper( (string) $currency );
$minimum_amount = in_array( $currency, self::get_zero_decimal_currencies(), true ) ? 50 : 5;
$currency_minimum_amount = array(
'BIF' => 2000,
'CLP' => 600,
'CZK' => 20,
'DJF' => 120,
'GNF' => 6000,
'HKD' => 10,
'HUF' => 200,
'JPY' => 100,
'KMF' => 400,
'KRW' => 1000,
'MGA' => 3000,
'MXN' => 20,
'PYG' => 4000,
'RWF' => 1000,
'THB' => 20,
'UGX' => 2500,
'VND' => 20000,
'VUV' => 100,
'XAF' => 400,
'XOF' => 400,
'XPF' => 7000,
);
if ( 'JOD' === $currency ) {
$minimum_amount = 1;
} elseif ( isset( $currency_minimum_amount[ $currency ] ) ) {
$minimum_amount = $currency_minimum_amount[ $currency ];
}
$amount = max( (float) $amount, $minimum_amount );
return apply_filters( 'forminator_stripe_default_payment_intent_amount', $amount, $field );
}
/**
* Return currencies without decimal
*
* @since 1.11
*
* @return array
*/
public static function get_zero_decimal_currencies() {
return array(
'MGA',
'BIF',
'CLP',
'PYG',
'DJF',
'RWF',
'GNF',
'UGX',
'VND',
'JPY',
'VUV',
'XAF',
'KMF',
'XOF',
'KRW',
'XPF',
);
}
/**
* Update amount
*
* @since 1.7.3
*
* @param array $submitted_data Submitted data.
* @param array $field Field.
* @throws Exception When there is an error.
*/
public function update_paymentIntent( $submitted_data, $field ) {
if ( $this->is_checkout_session( $field ) ) {
$this->update_checkout_session( $submitted_data, $field );
return;
}
$mode = self::get_property( 'mode', $field, 'test' );
$currency = self::get_property( 'currency', $field, $this->get_default_currency() );
$is_multi = self::get_property( 'automatic_payment_methods', $field, 'true' );
$is_intent = ! empty( $submitted_data['stripe-intent'] );
if ( ! empty( $this->payment_plan['payment_method'] ) && 'subscription' === $this->payment_plan['payment_method'] ) {
$response_data = array(
'paymentid' => 'subscription',
'paymentsecret' => 'subscription',
'paymentPlan' => $this->payment_plan_hash,
);
// Wallet preview: set amount for Apple/Google Pay preview from local price × qty (no Stripe API).
if ( $is_intent && class_exists( 'Forminator_Stripe_Subscription' ) ) {
try {
$field_object = Forminator_Core::get_field_object( 'stripe' );
$payment_plan = $field_object->get_payment_plan( $field );
$amount = $this->get_subscription_amount( $payment_plan, $field_object, Forminator_Front_Action::$prepared_data, $field );
if ( $amount > 0 ) {
$response_data['amount'] = $this->calculate_amount( $amount, $currency );
}
} catch ( Exception $e ) {
forminator_maybe_log( __METHOD__, $e->getMessage() );
}
}
wp_send_json_success( $response_data );
}
// apply merge tags to payment description.
$product_description = isset( $field['product_description'] ) ? $field['product_description'] : '';
if ( ! empty( $product_description ) ) {
$product_description = forminator_replace_form_data( $product_description, Forminator_Front_Action::$module_object );
$field['product_description'] = $product_description;
}
// Get Stripe key.
$key = $this->get_secret_key( 'test' !== $mode );
// Set Stripe key.
\Forminator\Stripe\Stripe::setApiKey( $key );
Forminator_Gateway_Stripe::set_stripe_app_info();
$field_id = Forminator_Field::get_property( 'element_id', $field );
$amount = $submitted_data[ $field_id ] ?? 0;
$id = $submitted_data['paymentid'];
if ( ! $amount && ! empty( $submitted_data['stripe_first_payment_intent'] ) ) {
// If amount is empty, set it to 1 for payment intent. Anyway, it will be updated during actual payment.
$amount = 1;
// Filter amount. It can be used to modify amount before creating payment intent for low-value currency
// to achieve minimum Stripe charge amount .5 euro. Use $field['currency'] to get currency code.
$amount = apply_filters( 'forminator_stripe_default_payment_intent_amount', $amount, $field );
}
$payment_method = filter_var( $field['automatic_payment_methods'], FILTER_VALIDATE_BOOLEAN ) ? 'dynamic' : 'card';
$payment_intent_key = $mode . '_' . $currency . '_' . $amount . '_' . substr( $key, -5 ) . '_' . $payment_method;
// Check if we already have payment ID, if not generate new one.
if ( empty( $id ) ) {
$generate_new = ! $is_intent;
$id = $this->get_payment_intent_id( $amount, $field, $payment_intent_key, $generate_new );
}
try {
// Retrieve PI object.
$intent = \Forminator\Stripe\PaymentIntent::retrieve( $id );
if ( 'succeeded' === $intent->status ) {
// throw error if payment intent already succeeded.
throw new Exception( esc_html__( 'Payment already succeeded.', 'forminator' ) );
}
} catch ( Exception $e ) {
forminator_maybe_log(
__METHOD__,
array(
'action' => 'PaymentIntent::retrieve',
'error_class' => get_class( $e ),
'error_message' => $e->getMessage(),
'error_code' => $e->getCode(),
'payment_intent_id' => $id,
)
);
$id = $this->get_payment_intent_id( $amount, $field, $payment_intent_key, true );
$intent = \Forminator\Stripe\PaymentIntent::retrieve( $id );
}
$metadata_data = $this->build_metadata_context( $field );
$metadata = $metadata_data['metadata'];
// Throw error if payment ID is empty.
if ( empty( $id ) ) {
$response = array(
'paymentPlan' => $this->payment_plan_hash,
'message' => esc_html__( 'Your Payment ID is empty, please reload the page and try again!', 'forminator' ),
'errors' => array(),
);
wp_send_json_error( $response );
}
if ( $is_intent ) {
wp_send_json_success(
array(
'paymentid' => $id,
'paymentsecret' => $intent->client_secret,
'paymentPlan' => $this->payment_plan_hash,
)
);
} elseif ( 'succeeded' === $intent->status ) {
// Check if the PaymentIntent already succeeded and continue.
wp_send_json_success(
array(
'paymentid' => $id,
'paymentsecret' => $intent->client_secret,
)
);
} else {
try {
// Check payment method.
if ( ! empty( $submitted_data['payment_method_type'] ) && in_array( $submitted_data['payment_method_type'], self::get_unsupported_payment_methods(), true ) ) {
throw new Exception( esc_html__( 'The selected Payment Method is not supported.', 'forminator' ) );
}
// Check payment amount.
if ( 0 > $amount ) {
throw new Exception( esc_html__( 'Payment amount should be greater than 0.', 'forminator' ) );
}
// Check payment ID.
if ( empty( $id ) ) {
throw new Exception( esc_html__( 'Your Payment ID is empty!', 'forminator' ) );
}
// Check payment method.
if ( empty( $submitted_data['payment_method'] ) ) {
throw new Exception( esc_html__( 'Your Payment Method is empty!', 'forminator' ) );
}
$options = array(
'amount' => $this->calculate_amount( $amount, $currency ),
);
if ( 'blik' !== $submitted_data['payment_method_type'] ) {
$options['payment_method'] = $submitted_data['payment_method'];
}
// Update receipt email if set on front-end.
if ( isset( $submitted_data['receipt_email'] ) && ! empty( $submitted_data['receipt_email'] ) ) {
$options['receipt_email'] = $submitted_data['receipt_email'];
}
if ( ! empty( $metadata ) ) {
$options['metadata'] = $metadata;
}
// Update Payment Intent amount.
\Forminator\Stripe\PaymentIntent::update(
$id,
$options
);
// Return success.
wp_send_json_success(
array(
'paymentid' => $id,
'paymentsecret' => $intent->client_secret,
'paymentPlan' => $this->payment_plan_hash,
)
);
} catch ( Exception $e ) {
forminator_maybe_log(
__METHOD__,
array(
'action' => 'PaymentIntent::update',
'error_class' => get_class( $e ),
'error_message' => $e->getMessage(),
'error_code' => $e->getCode(),
'payment_intent_id' => $id,
'options' => isset( $options ) ? $options : array(),
)
);
$response = array(
'message' => $e->getMessage(),
'errors' => array(),
'paymentPlan' => $this->payment_plan_hash,
);
wp_send_json_error( $response );
}
}
}
/**
* Create Checkout Session for front-end updates
*
* @since 1.56
*
* @param array $submitted_data Submitted data.
* @param array $field Field.
*/
private function update_checkout_session( $submitted_data, $field ) {
$payment_id = $submitted_data['paymentid'] ?? '';
if ( ! empty( $this->payment_plan['payment_method'] ) && 'subscription' === $this->payment_plan['payment_method'] ) {
if ( ! class_exists( 'Forminator_Stripe_Subscription' ) ) {
wp_send_json_error(
array(
'message' => esc_html(
apply_filters(
'forminator_payment_require_stripe_subscription_addon_error_message',
esc_html__( 'Forminator Stripe Subscription Add-on is required to submit this form.', 'forminator' )
)
),
'errors' => array(),
'paymentPlan' => $this->payment_plan_hash,
)
);
}
$stripe_addon = Forminator_Stripe_Subscription::get_instance();
if ( ! method_exists( $stripe_addon, 'update_checkout_session' ) ) {
wp_send_json_error(
array(
'message' => esc_html(
apply_filters(
'forminator_payment_update_stripe_subscription_addon_error_message',
esc_html__( 'Please update the Forminator Stripe Subscription Add-on to the latest version to submit this form.', 'forminator' )
)
),
'errors' => array(),
'paymentPlan' => $this->payment_plan_hash,
)
);
}
$response = $stripe_addon->update_checkout_session( $this, Forminator_Front_Action::$module_object, $submitted_data, $field, $this->payment_plan );
if ( is_wp_error( $response ) ) {
wp_send_json_error(
array(
'message' => $response->get_error_message(),
'errors' => array(),
'paymentPlan' => $this->payment_plan_hash,
)
);
}
wp_send_json_success(
array(
'paymentid' => $response['paymentid'] ?? '',
'paymentsecret' => $response['paymentsecret'] ?? '',
'paymentPlan' => $this->payment_plan_hash,
'paymentApi' => 'checkout_session',
)
);
}
$field_id = Forminator_Field::get_property( 'element_id', $field );
$amount = $submitted_data[ $field_id ] ?? 0;
if ( empty( $submitted_data['stripe-intent'] ) && ! empty( $payment_id ) ) {
$this->retrieve_checkout_session_response( $payment_id, $field );
}
$this->create_checkout_session_response( $amount, $field, empty( $payment_id ) );
}
/**
* Create a Checkout Session response for front-end updates.
*
* @since 1.56
*
* @param int|float $amount Amount.
* @param array $field Field.
* @param bool $allow_default_amount Allow default amount for initial field render.
*/
private function create_checkout_session_response( $amount, $field, $allow_default_amount = false ) {
$validation_amount = null;
$force_mount_stripe_field = false;
$currency = self::get_property( 'currency', $field, $this->get_default_currency() );
if ( 0 >= (float) $amount && $allow_default_amount ) {
// Initial render still needs a Checkout Session; mark the fallback so submit can rebuild with real pricing.
$amount = $this->get_checkout_session_render_amount( 1, $currency, $field );
$validation_amount = $this->calculate_amount( $amount, $currency );
$force_mount_stripe_field = true;
}
if ( 0 >= (float) $amount ) {
wp_send_json_error(
array(
'message' => esc_html__( 'Payment amount should be greater than 0.', 'forminator' ),
'errors' => array(),
'paymentPlan' => $this->payment_plan_hash,
)
);
}
$session = $this->generate_checkout_session( $amount, $field, ! $allow_default_amount );
if (
is_wp_error( $session )
&& \Forminator\Stripe\Exception\InvalidRequestException::class === ( $session->get_error_data()['exception_class'] ?? '' )
&& \Forminator\Stripe\ErrorObject::CODE_AMOUNT_TOO_SMALL === ( $session->get_error_data()['stripe_code'] ?? '' )
) {
$amount = $this->get_checkout_session_render_amount( $amount, $currency, $field );
$validation_amount = $this->calculate_amount( $amount, $currency );
$force_mount_stripe_field = true;
$session = $this->generate_checkout_session( $amount, $field );
}
if ( is_wp_error( $session ) ) {
wp_send_json_error( $session->get_error_data() );
}
// Block Checkout confirmation when the newly created session no longer matches the current form state.
$valid = $this->validate_checkout_session( $session, $field, false, $validation_amount );
if ( is_wp_error( $valid ) ) {
wp_send_json_error(
array(
'message' => $valid->get_error_message(),
'errors' => array(),
'paymentPlan' => $this->payment_plan_hash,
)
);
}
wp_send_json_success(
array(
'paymentid' => $session->id,
'paymentsecret' => $session->client_secret,
'paymentPlan' => $this->payment_plan_hash,
'paymentApi' => 'checkout_session',
'forceMountStripeField' => $force_mount_stripe_field,
)
);
}
/**
* Retrieve a Checkout Session and send the front-end response.
*
* @since 1.56
*
* @param string $payment_id Checkout Session ID.
* @param array $field Field.
*/
private function retrieve_checkout_session_response( $payment_id, $field ) {
if ( 0 >= (float) $this->get_payment_amount( $field ) ) {
wp_send_json_error(
array(
'message' => esc_html__( 'Payment amount should be greater than 0.', 'forminator' ),
'errors' => array(),
'paymentPlan' => $this->payment_plan_hash,
)
);
}
$session = $this->get_checkout_session_by_id( $payment_id, $field );
if ( is_wp_error( $session ) ) {
wp_send_json_error(
array(
'message' => $session->get_error_message(),
'errors' => array(),
'paymentPlan' => $this->payment_plan_hash,
)
);
}
$valid = $this->validate_checkout_session( $session, $field );
if ( is_wp_error( $valid ) ) {
wp_send_json_error(
array(
'message' => $valid->get_error_message(),
'errors' => array(),
'paymentPlan' => $this->payment_plan_hash,
)
);
}
wp_send_json_success(
array(
'paymentid' => $session->id,
'paymentsecret' => $session->client_secret ?? '',
'paymentPlan' => $this->payment_plan_hash,
'paymentApi' => 'checkout_session',
)
);
}
/**
* Validate Checkout Session details against the current form state.
*
* @since 1.56
*
* @param mixed $session Checkout Session object.
* @param array $field Field.
* @param bool $require_paid Whether the session must already be paid.
* @param int|null $expected_amount_override Optional amount to validate against instead of recalculating from the form state.
* @return true|WP_Error
*/
private function validate_checkout_session( $session, $field, $require_paid = false, $expected_amount_override = null ) {
$charge_amount = $this->get_payment_amount( $field );
$currency = self::get_property( 'currency', $field, $this->get_default_currency() );
$expected_amount = null !== $expected_amount_override
? (int) $expected_amount_override
: $this->calculate_amount( $charge_amount, $currency );
$session_amount = isset( $session->amount_total ) ? (int) $session->amount_total : 0;
$session_currency = isset( $session->currency ) ? strtoupper( (string) $session->currency ) : '';
$expected_currency = strtoupper( (string) $currency );
$form_id_meta = isset( $session->client_reference_id ) ? (string) $session->client_reference_id : '';
$expected_form_id = ! empty( Forminator_Front_Action::$module_object->id ) ? (string) Forminator_Front_Action::$module_object->id : '';
$payment_status = $session->payment_status ?? '';
if (
( $form_id_meta !== $expected_form_id )
|| ! self::does_checkout_session_match_expected_pricing( $session, $expected_amount, $expected_currency )
) {
forminator_maybe_log(
__METHOD__,
array(
'action' => 'checkout_session_validation_failed',
'error_message' => esc_html__( 'Checkout Session ID is not valid', 'forminator' ),
'payment_id' => Forminator_CForm_Front_Action::$prepared_data['paymentid'] ?? '',
'form_id' => $expected_form_id,
'field_id' => self::get_property( 'element_id', $field, '' ),
'require_paid' => $require_paid,
'session_id' => $session->id ?? '',
'client_reference' => $form_id_meta,
'expected_form_id' => $expected_form_id,
'session_amount' => $session_amount,
'expected_amount' => $expected_amount,
'session_currency' => $session_currency,
'expected_currency' => $expected_currency,
'payment_status' => $payment_status,
)
);
return new WP_Error( 'forminator_stripe_error', esc_html__( 'Checkout Session ID is not valid', 'forminator' ) );
}
if ( $require_paid && 'paid' !== $payment_status ) {
forminator_maybe_log(
__METHOD__,
array(
'action' => 'checkout_session_payment_pending',
'error_message' => esc_html__( 'Payment failed. Please try again.', 'forminator' ),
'payment_id' => Forminator_CForm_Front_Action::$prepared_data['paymentid'] ?? '',
'form_id' => $expected_form_id,
'field_id' => self::get_property( 'element_id', $field, '' ),
'session_id' => $session->id ?? '',
'payment_status' => $payment_status,
)
);
return new WP_Error(
'forminator_stripe_checkout_session_pending',
esc_html__( 'Payment failed. Please try again.', 'forminator' )
);
}
return true;
}
/**
* Copy current Checkout Session metadata to the generated PaymentIntent.
*
* @since 1.56
*
* @param mixed $session Checkout Session object.
* @param array $field Field.
*/
private function sync_checkout_session_payment_intent_metadata( $session, $field ) {
$payment_intent = $session->payment_intent ?? '';
if ( is_object( $payment_intent ) ) {
$payment_intent = $payment_intent->id ?? '';
}
if ( empty( $payment_intent ) ) {
return;
}
$metadata_data = $this->build_metadata_context( $field );
$context = $this->build_payment_request_context( $field );
$options = array();
if ( ! empty( $metadata_data['metadata'] ) ) {
$options['metadata'] = $metadata_data['metadata'];
}
if ( '' !== trim( (string) self::get_property( 'product_description', $field, '' ) ) ) {
$options['description'] = (string) $context['description'];
}
if ( empty( $options ) ) {
return;
}
try {
\Forminator\Stripe\PaymentIntent::update(
$payment_intent,
$options
);
} catch ( Exception $e ) {
forminator_maybe_log(
__METHOD__,
array(
'action' => 'PaymentIntent::update',
'error_class' => get_class( $e ),
'error_message' => $e->getMessage(),
'error_code' => $e->getCode(),
'payment_intent_id' => $payment_intent,
'options' => $options,
)
);
}
}
/**
* Get payment intent ID
*
* @param int|float $amount Amount.
* @param array $field Field.
* @param string $payment_intent_key Payment intent key.
* @param bool $force Use saved payment intents or not.
*
* @return string
*/
private function get_payment_intent_id( $amount, $field, $payment_intent_key, $force = false ): string {
$saved_payment_intents = get_option( 'forminator_stripe_payment_intents', array() );
/**
* Filter to force payment intent generation
*
* @param bool $force Force payment intent generation.
* @param array $field Field.
*/
$force = apply_filters( 'forminator_stripe_force_payment_intent', $force, $field );
if ( ! $force && ! empty( $saved_payment_intents[ $payment_intent_key ] ) ) {
$id = $saved_payment_intents[ $payment_intent_key ];
} else {
$payment_intent = $this->generate_paymentIntent( $amount, $field );
$id = $payment_intent->id;
$saved_payment_intents[ $payment_intent_key ] = $id;
update_option( 'forminator_stripe_payment_intents', $saved_payment_intents );
}
return $id;
}
/**
* Get unsupported payment methods
*
* @return array
*/
public static function get_unsupported_payment_methods() {
return apply_filters(
'forminator_stripe_unsupported_payment_methods',
// All Stripe dynamic payment methods without immediate confirmation.
array(
'sepa_debit',
'multibanco',
'boleto',
'ach_credit_transfer',
'ach_debit',
'sofort',
'funded',
)
);
}
/**
* Get unsupported payment methods that are valid for Checkout Sessions.
*
* Checkout Sessions does not accept some legacy Payment Intent method IDs
* in excluded_payment_method_types, so only remove those known invalid
* values from the shared unsupported list.
*
* @return array
*/
public static function get_checkout_session_excluded_payment_methods() {
$checkout_unsupported_methods = array(
'ach_credit_transfer',
'ach_debit',
'funded',
);
return array_values( array_diff( self::get_unsupported_payment_methods(), $checkout_unsupported_methods ) );
}
/**
* Check whether adaptive pricing should be enabled for a field.
*
* @since 1.56
*
* @param array $field Field settings.
* @return bool
*/
public static function is_adaptive_pricing_configured( $field ) {
$payments = self::get_property( 'payments', $field, array() );
foreach ( $payments as $payment ) {
// Adaptive Pricing is only safe when Stripe receives a fixed source amount.
if ( ! empty( self::get_plan_dependent_fields( $payment ) ) ) {
return false;
}
}
return 'checkout_session' === self::get_payment_api( $field )
&& filter_var( self::get_property( 'adaptive_pricing', $field, false ), FILTER_VALIDATE_BOOLEAN );
}
/**
* Validate Checkout Session totals against the expected source pricing.
*
* @since 1.56
*
* @param object $session Checkout Session object.
* @param int $expected_amount Expected amount in the Checkout Session's minor unit.
* @param string $expected_currency Expected Checkout Session currency.
* @return bool
*/
public static function does_checkout_session_match_expected_pricing( $session, $expected_amount, $expected_currency ) {
if ( ! is_object( $session ) ) {
return false;
}
if ( ! isset( $session->amount_total, $session->currency ) ) {
return false;
}
$expected_currency = strtoupper( (string) $expected_currency );
return (int) $session->amount_total === $expected_amount
&& strtoupper( (string) $session->currency ) === $expected_currency;
}
/**
* Get form setting
*
* @since 1.9
*
* @param int $id Id.
* @param array $settings Settings.
* @param mixed $fallback Fallback method.
*
* @return mixed
*/
public function get_form_setting( $id, $settings, $fallback ) {
// Check if user settings exist.
if ( isset( $settings[ $id ] ) ) {
return $settings[ $id ];
}
// Return fallback.
return $fallback;
}
/**
* Field back-end validation
*
* @param array $field Field.
* @param array|string $data Data.
*/
public function validate( $field, $data ) {
if ( $this->is_checkout_session( $field ) ) {
$this->payment_plan = $this->get_payment_plan( $field );
$is_subscription = ! empty( $this->payment_plan['payment_method'] ) && 'subscription' === $this->payment_plan['payment_method'];
$amount = $this->get_payment_amount( $field );
if ( $is_subscription ) {
$amount = self::get_property( 'subscription_amount', $this->payment_plan, 0.0 );
if ( class_exists( 'Forminator_Stripe_Subscription' ) ) {
$amount = $this->get_subscription_amount( $this->payment_plan, $this, Forminator_CForm_Front_Action::$prepared_data, $field );
}
}
if ( 0 >= (float) $amount ) {
$dependent_fields = self::get_plan_dependent_fields( $this->payment_plan );
$field_id = ! empty( $dependent_fields ) ? reset( $dependent_fields ) : Forminator_Field::get_property( 'element_id', $field );
Forminator_CForm_Front_Action::$submit_errors[] = array(
$field_id => esc_html__( 'Payment amount should be greater than 0.', 'forminator' ),
);
}
}
$checkout_email_enabled = filter_var( self::get_property( 'checkout_email_enabled', $field, false ), FILTER_VALIDATE_BOOLEAN );
if ( $checkout_email_enabled ) {
$this->validate_checkout_session_required_field( $field, 'checkout_email' );
}
$checkout_phone_enabled = filter_var( self::get_property( 'checkout_phone_enabled', $field, false ), FILTER_VALIDATE_BOOLEAN );
if ( $checkout_phone_enabled ) {
$this->validate_checkout_session_required_field( $field, 'checkout_phone' );
}
if ( Forminator_Front_Action::$module_object instanceof Forminator_Form_Model ) {
$checkout_billing_mappings = $this->get_checkout_session_billing_mappings(
$field,
Forminator_Front_Action::$module_object,
Forminator_Front_Action::$module_id
);
if ( ! empty( $checkout_billing_mappings['billing_address'] ) ) {
$this->validate_checkout_session_required_field( $field, 'billing_address', '-country' );
}
}
}
/**
* Validate a mapped Checkout Session required field.
*
* @since 1.56
*
* @param array $field Field.
* @param string $property Field property.
* @param string $field_suffix Optional mapped subfield suffix.
*/
public function validate_checkout_session_required_field( $field, $property, $field_suffix = '' ) {
$field_id = self::get_property( $property, $field, '' );
if ( ! $this->is_checkout_session( $field ) ) {
return;
}
// Old saved mappings should not block submission once the mapped field is gone from the form.
$field_id = forminator_get_active_mapped_field_id(
$field_id,
Forminator_Front_Action::$module_object->get_fields(),
Forminator_Front_Action::$module_id
);
if ( empty( $field_id ) ) {
return;
}
// Billing country is submitted as an address subfield, so allow callers to validate a derived field like billing_address-country.
if ( ! empty( $field_suffix ) ) {
$field_id .= $field_suffix;
}
$submitted_data = Forminator_CForm_Front_Action::$prepared_data;
$submitted_value = $submitted_data[ $field_id ] ?? '';
if ( is_array( $submitted_value ) ) {
$submitted_value = reset( $submitted_value );
}
if ( '' !== trim( (string) $submitted_value ) ) {
return;
}
Forminator_CForm_Front_Action::$submit_errors[] = array(
$field_id => esc_html__( 'This field is required to complete your payment.', 'forminator' ),
);
}
/**
* Sanitize data
*
* @param array $field Field.
* @param array|string $data - the data to be sanitized.
*
* @return array|string $data - the data after sanitization
*/
public function sanitize( $field, $data ) {
$original_data = $data;
// Sanitize.
$data = forminator_sanitize_field( $data );
return apply_filters( 'forminator_field_stripe_sanitize', $data, $field, $original_data );
}
/**
* Is available
*
* @since 1.7
* @inheritdoc
* @param array $field Field.
*/
public function is_available( $field ) {
$mode = self::get_property( 'mode', $field, 'test' );
try {
$stripe = new Forminator_Gateway_Stripe();
if ( 'test' !== $mode ) {
$stripe->set_live( true );
}
if ( $stripe->is_ready() ) {
return true;
}
} catch ( Forminator_Gateway_Exception $e ) {
return false;
}
}
/**
* Get publishable key
*
* @since 1.7
*
* @param bool $live Live?.
*
* @return bool|string
*/
private function get_publishable_key( $live = false ) {
try {
$stripe = new Forminator_Gateway_Stripe();
if ( $live ) {
return $stripe->get_live_key();
}
return $stripe->get_test_key();
} catch ( Forminator_Gateway_Exception $e ) {
return false;
}
}
/**
* Get publishable key
*
* @since 1.7
*
* @param bool $live Live?.
*
* @return bool|string
*/
private function get_secret_key( $live = false ) {
try {
$stripe = new Forminator_Gateway_Stripe();
if ( $live ) {
return $stripe->get_live_secret( true );
}
return $stripe->get_test_secret( true );
} catch ( Forminator_Gateway_Exception $e ) {
return false;
}
}
/**
* Get default currency
*
* @return string
*/
private function get_default_currency() {
try {
$stripe = new Forminator_Gateway_Stripe();
return $stripe->get_default_currency();
} catch ( Forminator_Gateway_Exception $e ) {
return 'USD';
}
}
/**
* Process to entry data
*
* @param array $field Field.
*
* @return array|WP_Error
* @throws Exception When there is an error.
*/
public function process_to_entry_data( $field ) {
if ( $this->is_checkout_session( $field ) ) {
return $this->process_checkout_session_to_entry_data( $field );
}
$entry_data = array(
'mode' => '',
'product_name' => '',
'payment_type' => '',
'amount' => '',
'quantity' => '',
'currency' => '',
'transaction_id' => '',
'transaction_link' => '',
);
$mode = self::get_property( 'mode', $field, 'test' );
$currency = self::get_property( 'currency', $field, $this->get_default_currency() );
try {
// Get Payment intent.
$intent = $this->get_paymentIntent( $field );
if ( is_wp_error( $intent ) ) {
throw new Exception( $intent->get_error_message() );
} elseif ( ! is_object( $intent ) ) {
// Make sure Payment Intent is object.
throw new Exception( esc_html__( 'Payment Intent object is not valid Payment object.', 'forminator' ) );
}
// Check if the PaymentIntent is set or empty.
if ( empty( $intent->id ) ) {
throw new Exception( esc_html__( 'Payment Intent ID is not valid!', 'forminator' ) );
}
// Check if the PaymentIntent is valid.
if ( ! self::is_valid_payment_intent( $intent->id ) ) {
return new WP_Error(
'forminator_stripe_payment_intent_already_handled',
esc_html__( 'Payment Intent ID is not valid', 'forminator' )
);
}
$charge_amount = $this->get_payment_amount( $field );
$expected_amount = $this->calculate_amount( $charge_amount, $currency );
$intent_amount = isset( $intent->amount ) ? (int) $intent->amount : 0;
$intent_currency = isset( $intent->currency ) ? strtoupper( (string) $intent->currency ) : '';
$expected_currency = strtoupper( (string) $currency );
$form_id_meta = isset( $intent->metadata->forminator_form_id ) ? (string) $intent->metadata->forminator_form_id : '';
$expected_form_id = ! empty( Forminator_Front_Action::$module_object->id ) ? (string) Forminator_Front_Action::$module_object->id : '';
if (
( $form_id_meta !== $expected_form_id )
|| ( $expected_amount !== $intent_amount )
|| ( $expected_currency !== $intent_currency )
) {
return new WP_Error( 'forminator_stripe_error', esc_html__( 'Payment Intent ID is not valid', 'forminator' ) );
}
$entry_data['mode'] = $mode;
$entry_data['currency'] = $currency;
$entry_data['amount'] = $charge_amount;
if ( ! empty( $this->payment_plan ) ) {
$entry_data['product_name'] = $this->payment_plan['plan_name'];
$entry_data['payment_type'] = $this->payment_method( $this->payment_plan['payment_method'] );
$entry_data['quantity'] = $this->payment_plan['quantity'];
}
$entry_data['transaction_link'] = self::get_transanction_link( $mode, $intent->id );
$entry_data['transaction_id'] = $intent->id;
} catch ( Exception $e ) {
$entry_data['error'] = $e->getMessage();
}
/**
* Filter stripe entry data that will be stored
*
* @since 1.7
*
* @param array $entry_data Entry data.
* @param array $field Field properties.
* @param Forminator_Form_Model $module_object Forminator_Form_Model.
* @param array $submitted_data Submitted data.
* @param array $field_data_array current entry meta.
*
* @return array
*/
$entry_data = apply_filters( 'forminator_field_stripe_process_to_entry_data', $entry_data, $field, Forminator_Front_Action::$module_object, Forminator_CForm_Front_Action::$prepared_data, Forminator_CForm_Front_Action::$info['field_data_array'] );
return $entry_data;
}
/**
* Process Checkout Session entry data
*
* @since 1.56
*
* @param array $field Field.
* @throws Exception When the Checkout Session cannot be retrieved or validated.
* @return array|WP_Error
*/
private function process_checkout_session_to_entry_data( $field ) {
$entry_data = array(
'mode' => '',
'product_name' => '',
'payment_type' => '',
'amount' => '',
'quantity' => '',
'currency' => '',
'transaction_id' => '',
'transaction_link' => '',
);
$mode = self::get_property( 'mode', $field, 'test' );
$currency = self::get_property( 'currency', $field, $this->get_default_currency() );
try {
$session = $this->get_checkout_session( $field );
if ( is_wp_error( $session ) ) {
forminator_maybe_log(
__METHOD__,
array(
'action' => 'checkout_session_entry_retrieve_failed',
'error_message' => $session->get_error_message(),
'payment_id' => Forminator_CForm_Front_Action::$prepared_data['paymentid'] ?? '',
'form_id' => ! empty( Forminator_Front_Action::$module_object->id ) ? Forminator_Front_Action::$module_object->id : '',
'field_id' => self::get_property( 'element_id', $field, '' ),
)
);
throw new Exception( $session->get_error_message() );
} elseif ( ! is_object( $session ) ) {
forminator_maybe_log(
__METHOD__,
array(
'action' => 'checkout_session_entry_invalid_object',
'error_message' => esc_html__( 'Checkout Session object is not valid.', 'forminator' ),
'payment_id' => Forminator_CForm_Front_Action::$prepared_data['paymentid'] ?? '',
'form_id' => ! empty( Forminator_Front_Action::$module_object->id ) ? Forminator_Front_Action::$module_object->id : '',
'field_id' => self::get_property( 'element_id', $field, '' ),
'session' => $session,
)
);
throw new Exception( esc_html__( 'Checkout Session object is not valid.', 'forminator' ) );
}
if ( empty( $session->id ) ) {
forminator_maybe_log(
__METHOD__,
array(
'action' => 'checkout_session_entry_missing_id',
'error_message' => esc_html__( 'Checkout Session ID is not valid!', 'forminator' ),
'payment_id' => Forminator_CForm_Front_Action::$prepared_data['paymentid'] ?? '',
'form_id' => ! empty( Forminator_Front_Action::$module_object->id ) ? Forminator_Front_Action::$module_object->id : '',
'field_id' => self::get_property( 'element_id', $field, '' ),
'session' => $session,
)
);
throw new Exception( esc_html__( 'Checkout Session ID is not valid!', 'forminator' ) );
}
if ( ! self::is_valid_checkout_session( $session->id ) ) {
forminator_maybe_log(
__METHOD__,
array(
'action' => 'checkout_session_entry_already_handled',
'error_message' => esc_html__( 'Checkout Session ID is not valid', 'forminator' ),
'payment_id' => Forminator_CForm_Front_Action::$prepared_data['paymentid'] ?? '',
'form_id' => ! empty( Forminator_Front_Action::$module_object->id ) ? Forminator_Front_Action::$module_object->id : '',
'field_id' => self::get_property( 'element_id', $field, '' ),
'session_id' => $session->id,
)
);
return new WP_Error(
'forminator_stripe_checkout_session_already_handled',
esc_html__( 'Checkout Session ID is not valid', 'forminator' )
);
}
$charge_amount = $this->get_payment_amount( $field );
$expected_amount = $this->calculate_amount( $charge_amount, $currency );
$expected_currency = strtoupper( (string) $currency );
$form_id_meta = isset( $session->client_reference_id ) ? (string) $session->client_reference_id : '';
$expected_form_id = ! empty( Forminator_Front_Action::$module_object->id ) ? (string) Forminator_Front_Action::$module_object->id : '';
if (
( $form_id_meta !== $expected_form_id )
|| ! self::does_checkout_session_match_expected_pricing( $session, $expected_amount, $expected_currency )
) {
forminator_maybe_log(
__METHOD__,
array(
'action' => 'checkout_session_entry_validation_failed',
'error_message' => esc_html__( 'Checkout Session ID is not valid', 'forminator' ),
'payment_id' => Forminator_CForm_Front_Action::$prepared_data['paymentid'] ?? '',
'form_id' => $expected_form_id,
'field_id' => self::get_property( 'element_id', $field, '' ),
'session_id' => $session->id,
'client_reference' => $form_id_meta,
'expected_form_id' => $expected_form_id,
'expected_amount' => $expected_amount,
'expected_currency' => $expected_currency,
'payment_status' => $session->payment_status ?? '',
)
);
return new WP_Error( 'forminator_stripe_error', esc_html__( 'Checkout Session ID is not valid', 'forminator' ) );
}
if ( 'paid' !== ( $session->payment_status ?? '' ) ) {
forminator_maybe_log(
__METHOD__,
array(
'action' => 'checkout_session_entry_payment_pending',
'error_message' => esc_html__( 'Payment failed. Please try again.', 'forminator' ),
'payment_id' => Forminator_CForm_Front_Action::$prepared_data['paymentid'] ?? '',
'form_id' => $expected_form_id,
'field_id' => self::get_property( 'element_id', $field, '' ),
'session_id' => $session->id,
'payment_status' => $session->payment_status ?? '',
)
);
return new WP_Error(
'forminator_stripe_checkout_session_pending',
esc_html__( 'Payment failed. Please try again.', 'forminator' )
);
}
$entry_data['status'] = 'COMPLETED';
$payment_intent = $session->payment_intent ?? '';
if ( is_object( $payment_intent ) ) {
$payment_intent = $payment_intent->id ?? '';
}
$this->sync_checkout_session_payment_intent_metadata( $session, $field );
$entry_data['mode'] = $mode;
$entry_data['currency'] = $currency;
$entry_data['amount'] = $charge_amount;
if ( ! empty( $this->payment_plan ) ) {
$entry_data['product_name'] = $this->payment_plan['plan_name'];
$entry_data['payment_type'] = $this->payment_method( $this->payment_plan['payment_method'] );
$entry_data['quantity'] = $this->payment_plan['quantity'];
}
$entry_data['transaction_id'] = $payment_intent ? $payment_intent : $session->id;
if ( $payment_intent ) {
$entry_data['transaction_link'] = self::get_transanction_link( $mode, $payment_intent );
}
} catch ( Exception $e ) {
$entry_data['error'] = $e->getMessage();
}
return apply_filters( 'forminator_field_stripe_process_to_entry_data', $entry_data, $field, Forminator_Front_Action::$module_object, Forminator_CForm_Front_Action::$prepared_data, Forminator_CForm_Front_Action::$info['field_data_array'] );
}
/**
* Check if payment intent is valid
*
* @param string $intent_id Payment Intent ID.
*
* @return bool
*/
private static function is_valid_payment_intent( $intent_id ): bool {
$payment_intents = get_option( 'forminator_stripe_payment_intents', array() );
if ( is_array( $payment_intents ) && in_array( $intent_id, $payment_intents, true ) ) {
// Remove payment intent after handling it.
add_action(
'forminator_after_handle_form',
function () use ( $intent_id ) {
$option_key = 'forminator_stripe_payment_intents';
$payment_intents = get_option( $option_key, array() );
$payment_intents = array_diff( $payment_intents, array( $intent_id ) );
update_option( $option_key, $payment_intents );
}
);
return true;
}
return false;
}
/**
* Check if checkout session is valid
*
* @since 1.56
*
* @param string $session_id Checkout Session ID.
* @return bool
*/
public static function is_valid_checkout_session( $session_id ): bool {
if ( self::has_pending_checkout_session( $session_id ) ) {
add_action(
'forminator_after_handle_form',
function () use ( $session_id ) {
self::remove_pending_checkout_session( $session_id );
}
);
return true;
}
return false;
}
/**
* Check whether a pending Checkout Session ID is still tracked locally.
*
* @since 1.56
*
* @param string $session_id Checkout Session ID.
* @return bool
*/
public static function has_pending_checkout_session( $session_id ): bool {
return self::has_pending_checkout_session_option( $session_id );
}
/**
* Store an individual pending Checkout Session marker.
*
* Each Checkout Session uses its own option so multiple forms cannot
* overwrite each other's pending markers on the same page.
*
* @since 1.56
*
* @param string $session_id Checkout Session ID.
* @return void
*/
private static function set_pending_checkout_session( $session_id ) {
if ( empty( $session_id ) ) {
return;
}
update_option(
self::get_checkout_session_option_key( $session_id ),
array(
'created_at' => time(),
),
false
);
$checkout_sessions = get_option( self::CHECKOUT_SESSION_CLEANUP_OPTION_KEY, array() );
$checkout_sessions = is_array( $checkout_sessions ) ? $checkout_sessions : array();
$checkout_sessions[ $session_id ] = time();
update_option( self::CHECKOUT_SESSION_CLEANUP_OPTION_KEY, $checkout_sessions, false );
}
/**
* Restore a missing pending Checkout Session marker after Stripe validates the returned session.
*
* This covers redirect methods that return after local pending state was lost,
* while still allowing the caller to verify the session belongs to the form first.
*
* @since 1.56
*
* @param string $session_id Checkout Session ID.
* @return void
*/
public static function restore_pending_checkout_session( $session_id ) {
if ( empty( $session_id ) ) {
return;
}
self::set_pending_checkout_session( $session_id );
}
/**
* Check whether an individual pending Checkout Session marker exists.
*
* @since 1.56
*
* @param string $session_id Checkout Session ID.
* @return bool
*/
private static function has_pending_checkout_session_option( $session_id ): bool {
if ( empty( $session_id ) ) {
return false;
}
return false !== get_option( self::get_checkout_session_option_key( $session_id ), false );
}
/**
* Build the option key for a Checkout Session ID.
*
* @since 1.56
*
* @param string $session_id Checkout Session ID.
* @return string
*/
private static function get_checkout_session_option_key( $session_id ): string {
return self::CHECKOUT_SESSION_OPTION_PREFIX . md5( $session_id );
}
/**
* Remove a pending Checkout Session ID from local tracking.
*
* @since 1.56
*
* @param string $session_id Checkout Session ID.
* @return void
*/
public static function remove_pending_checkout_session( $session_id ) {
if ( empty( $session_id ) ) {
return;
}
delete_option( self::get_checkout_session_option_key( $session_id ) );
$checkout_sessions = get_option( self::CHECKOUT_SESSION_CLEANUP_OPTION_KEY, array() );
if ( is_array( $checkout_sessions ) && isset( $checkout_sessions[ $session_id ] ) ) {
unset( $checkout_sessions[ $session_id ] );
update_option( self::CHECKOUT_SESSION_CLEANUP_OPTION_KEY, $checkout_sessions, false );
}
// Also remove any uploaded files data associated with this Checkout Session.
self::remove_checkout_session_uploaded_files_data( $session_id );
}
/**
* Remove abandoned pending Checkout Session markers.
*
* @since 1.56
*
* @return void
*/
public function cleanup_checkout_sessions() {
$checkout_sessions = get_option( self::CHECKOUT_SESSION_CLEANUP_OPTION_KEY, array() );
if ( empty( $checkout_sessions ) || ! is_array( $checkout_sessions ) ) {
return;
}
/**
* Filter how long abandoned Stripe Checkout Session markers are retained.
*
* @since 1.56
*
* @param int $retention Retention time in seconds.
*/
$retention = apply_filters( 'forminator_stripe_checkout_session_retention', DAY_IN_SECONDS );
$expired_sessions = array();
foreach ( $checkout_sessions as $session_id => $created_at ) {
if ( ! is_numeric( $created_at ) || time() - (int) $created_at > $retention ) {
$expired_sessions[] = $session_id;
unset( $checkout_sessions[ $session_id ] );
delete_option( self::get_checkout_session_option_key( $session_id ) );
}
}
update_option( self::CHECKOUT_SESSION_CLEANUP_OPTION_KEY, $checkout_sessions, false );
if ( ! empty( $expired_sessions ) ) {
self::remove_checkout_session_uploaded_files_data( $expired_sessions );
}
}
/**
* Determine whether a Checkout Session can still be recovered for retry.
*
* @since 1.56
*
* @param mixed $session Checkout Session object.
* @return bool
*/
public static function is_recoverable_checkout_session( $session ): bool {
if ( ! is_object( $session ) || empty( $session->id ) ) {
return false;
}
$session_status = isset( $session->status ) ? (string) $session->status : '';
$payment_status = isset( $session->payment_status ) ? (string) $session->payment_status : '';
if ( 'paid' === $payment_status ) {
return true;
}
if ( 'expired' === $session_status || 'complete' === $session_status ) {
return false;
}
if ( 'unpaid' === $payment_status ) {
return false;
}
return 'paid' !== $payment_status;
}
/**
* Make linkify transaction_id
*
* @param string $transaction_id Transaction Id.
* @param array $meta_value Meta value.
*
* @return string
*/
public static function linkify_transaction_id( $transaction_id, $meta_value ) {
$transaction_link = $transaction_id;
if ( isset( $meta_value['transaction_link'] ) && ! empty( $meta_value['transaction_link'] ) ) {
$url = $meta_value['transaction_link'];
$transaction_link = '' . $transaction_id . '';
}
/**
* Filter link to Stripe transaction id
*
* @since 1.7
*
* @param string $transaction_link
* @param string $transaction_id
* @param array $meta_value
*
* @return string
*/
$transaction_link = apply_filters( 'forminator_field_stripe_linkify_transaction_id', $transaction_link, $transaction_id, $meta_value );
return $transaction_link;
}
/**
* Retrieve PaymentIntent object
*
* @param array $field Field.
*
* @return mixed object|string
* @throws Exception When there is an error.
*/
public function get_paymentIntent( $field ) {
if ( $this->is_checkout_session( $field ) ) {
return $this->get_checkout_session( $field );
}
$mode = self::get_property( 'mode', $field, 'test' );
$currency = self::get_property( 'currency', $field, $this->get_default_currency() );
// Check Stripe key.
$key = $this->get_secret_key( 'test' !== $mode );
// Set Stripe key.
\Forminator\Stripe\Stripe::setApiKey( $key );
Forminator_Gateway_Stripe::set_stripe_app_info();
try {
// Makue sure payment ID exist.
if ( empty( Forminator_CForm_Front_Action::$prepared_data['paymentid'] ) ) {
throw new Exception( esc_html__( 'Stripe Payment ID does not exist.', 'forminator' ) );
}
// Check payment amount.
$intent = \Forminator\Stripe\PaymentIntent::retrieve( Forminator_CForm_Front_Action::$prepared_data['paymentid'] );
return $intent;
} catch ( Exception $e ) {
return $this->get_error( $e );
}
}
/**
* Retrieve Checkout Session object
*
* @since 1.56
*
* @param array $field Field.
* @return mixed
*/
public function get_checkout_session( $field ) {
return $this->get_checkout_session_by_id( Forminator_CForm_Front_Action::$prepared_data['paymentid'] ?? '', $field );
}
/**
* Retrieve Checkout Session object by id.
*
* @since 1.56
*
* @param string $session_id Checkout Session ID.
* @param array $field Field.
* @throws Exception When the Checkout Session cannot be retrieved.
* @return mixed
*/
private function get_checkout_session_by_id( $session_id, $field ) {
$mode = self::get_property( 'mode', $field, 'test' );
$key = $this->get_secret_key( 'test' !== $mode );
\Forminator\Stripe\Stripe::setApiKey( $key );
Forminator_Gateway_Stripe::set_stripe_app_info();
try {
if ( empty( $session_id ) ) {
throw new Exception( esc_html__( 'Stripe Checkout Session ID does not exist.', 'forminator' ) );
}
$session = \Forminator\Stripe\Checkout\Session::retrieve(
$session_id,
self::get_checkout_session_request_options()
);
return $session;
} catch ( Exception $e ) {
forminator_maybe_log(
__METHOD__,
array(
'action' => 'Checkout\\Session::retrieve',
'error_class' => get_class( $e ),
'error_message' => $e->getMessage(),
'error_code' => $e->getCode(),
'session_id' => $session_id,
'request_options' => self::get_checkout_session_request_options(),
)
);
return $this->get_error( $e );
}
}
/**
* Request options for Checkout Session API calls.
*
* @since 1.56
*
* @return array
*/
public static function get_checkout_session_request_options(): array {
return array(
'stripe_version' => self::CHECKOUT_SESSION_STRIPE_VERSION,
);
}
/**
* Retrieve PaymentMethod object
*
* @since 1.15
*
* @param array $field Field.
* @param array $submitted_data Submitted data.
*
* @return mixed object|string
* @throws Exception When there is an error.
*/
public function get_paymentMethod( $field, $submitted_data ) {
$mode = self::get_property( 'mode', $field, 'test' );
$currency = self::get_property( 'currency', $field, $this->get_default_currency() );
// Check Stripe key.
$key = $this->get_secret_key( 'test' !== $mode );
// Set Stripe key.
\Forminator\Stripe\Stripe::setApiKey( $key );
Forminator_Gateway_Stripe::set_stripe_app_info();
try {
// Makue sure payment ID exist.
if ( ! isset( $submitted_data['paymentid'] ) ) {
throw new Exception( esc_html__( 'Stripe Payment ID does not exist.', 'forminator' ) );
}
// Check payment amount.
$intent = \Forminator\Stripe\PaymentMethod::retrieve( $submitted_data['paymentmethod'] );
return $intent;
} catch ( Exception $e ) {
return $this->get_error( $e );
}
}
/**
* Get Stripe return URL to pass it in API calls
*
* @return string
*/
public static function get_return_url() {
$return_url = forminator_get_current_url();
if ( empty( $return_url ) && ! empty( Forminator_CForm_Front_Action::$prepared_data['current_url'] ) ) {
$return_url = esc_url_raw( wp_unslash( Forminator_CForm_Front_Action::$prepared_data['current_url'] ) );
}
if ( empty( $return_url ) && ! empty( Forminator_CForm_Front_Action::$prepared_data['page_id'] ) ) {
$page_id = absint( Forminator_CForm_Front_Action::$prepared_data['page_id'] );
if ( $page_id > 0 ) {
$return_url = get_permalink( $page_id );
}
}
if ( empty( $return_url ) ) {
$return_url = home_url( '/' );
}
return apply_filters( 'forminator_stripe_return_url', $return_url );
}
/**
* Confirm paymentIntent
*
* @param mixed $intent Payment Intent.
*
* @since 1.14.9
*
* @return object|WP_Error
*/
public function confirm_paymentIntent( $intent ) {
try {
return $intent->confirm( array( 'return_url' => self::get_return_url() ) );
} catch ( Exception $e ) {
return $this->get_error( $e );
}
}
/**
* Get the exception error and return WP_Error
*
* @param mixed $e Exception.
*
* @since 1.14.9
*
* @return WP_Error
*/
private function get_error( $e ) {
$code = $e->getCode();
if ( is_int( $code ) ) {
$code = ( 0 === $code ) ? 'zero' : $code;
return new WP_Error( $code, $e->getMessage() );
} else {
return new WP_Error( $e->getError()->code, $e->getMessage() );
}
}
/**
* Get ALL fields that payment amount depends on
*
* @param array $field_settings Field settings.
* @return array
*/
public function get_amount_dependent_fields_all( $field_settings ) {
$depend_field = self::get_conditions_dependent_fields( $field_settings );
$plans = self::get_property( 'payments', $field_settings, array() );
foreach ( $plans as $plan ) {
$plan_depends = self::get_plan_dependent_fields( $plan );
$depend_field = array_merge( $depend_field, $plan_depends );
}
return array_values( array_unique( $depend_field ) );
}
/**
* Get form field IDs used by one or more merge-tag values.
*
* @since 1.56
*
* @param mixed $values Merge-tag value or metadata config.
* @return array
*/
public function get_merge_tag_dependent_fields( $values ) {
$depend_field = array();
if ( ! is_array( $values ) ) {
$values = array( $values );
}
foreach ( $values as $value ) {
if ( is_array( $value ) ) {
$value = $value['value'] ?? '';
}
if ( empty( $value ) || ! is_scalar( $value ) ) {
continue;
}
$value = (string) $value;
if ( false === strpos( $value, '{' ) ) {
$depend_field[] = forminator_clear_field_id( $value );
continue;
}
if ( preg_match_all( '/\{([^}]+)\}/', $value, $matches ) ) {
foreach ( $matches[1] as $match ) {
$depend_field[] = forminator_clear_field_id( $match );
}
}
}
return array_values( array_unique( array_filter( $depend_field ) ) );
}
/**
* Get the fields that an amount depends on
*
* @param array $field_settings Field settings.
* @return array
*/
public function get_amount_dependent_fields( $field_settings ) {
$this->payment_plan = $this->get_payment_plan( $field_settings );
$plan = $this->payment_plan;
$amount = $this->get_payment_amount( $field_settings );
// Subscription plans use subscription_amount_* keys; include live total so paymentPlan hash changes when variable price/qty changes.
if ( ! empty( $plan['payment_method'] ) && 'subscription' === $plan['payment_method'] && class_exists( 'Forminator_Stripe_Subscription' ) ) {
$amount = $this->get_subscription_amount( $plan, $this, Forminator_CForm_Front_Action::$prepared_data, $field_settings );
}
$this->payment_plan_hash = md5( wp_json_encode( $plan ) . $amount );
$conditions_depends = self::get_conditions_dependent_fields( $field_settings );
$plan_depends = self::get_plan_dependent_fields( $plan );
$depend_field = array_merge( $conditions_depends, $plan_depends );
return array_unique( $depend_field );
}
/**
* Get subscription amount including quantity.
*
* @param array $payment_plan Payment plan.
* @param Forminator_Field $field_object Field object.
* @param array $prepared_data Prepared submission data.
* @param array $field_settings Field settings.
*
* @return float|int
*/
private function get_subscription_amount( $payment_plan, $field_object, $prepared_data, $field_settings ) {
$stripe_addon = Forminator_Stripe_Subscription::get_instance();
$price = $stripe_addon->calculate_price( $payment_plan, Forminator_Front_Action::$module_object, $field_object, $prepared_data, $field_settings );
$quantity = $stripe_addon->get_quantity( $payment_plan, Forminator_Front_Action::$module_object, $field_object, $prepared_data, $field_settings );
if ( $quantity < 1 ) {
$quantity = 1;
}
return $price * $quantity;
}
/**
* Get the fields that conditions based on
*
* @param array $field_settings Field settings.
*
* @return array
*/
private static function get_conditions_dependent_fields( $field_settings ) {
$depend_field = array();
$all_conditions = self::get_property( 'conditions', $field_settings, array() );
$payments = self::get_property( 'payments', $field_settings, array() );
foreach ( $payments as $payment ) {
$conditions = $payment['conditions'] ?? array();
if ( empty( $conditions ) || ! is_array( $conditions ) ) {
continue;
}
$all_conditions = array_merge( $all_conditions, $conditions );
}
foreach ( $all_conditions as $condition ) {
if ( ! empty( $condition['element_id'] ) ) {
$depend_field[] = $condition['element_id'];
}
}
return $depend_field;
}
/**
* Get the fields that a plan depends on
*
* @param array $plan Plan.
* @return array
*/
private static function get_plan_dependent_fields( $plan ) {
$depend_field = array();
if ( empty( $plan['payment_method'] ) ) {
return $depend_field;
}
if ( 'single' === $plan['payment_method']
&& ! empty( $plan['amount_type'] )
&& 'variable' === $plan['amount_type']
&& ! empty( $plan['variable'] ) ) {
$depend_field[] = $plan['variable'];
}
if ( 'subscription' === $plan['payment_method']
&& ! empty( $plan['subscription_amount_type'] )
&& 'variable' === $plan['subscription_amount_type']
&& ! empty( $plan['subscription_variable'] ) ) {
$depend_field[] = $plan['subscription_variable'];
}
if ( 'subscription' === $plan['payment_method']
&& ! empty( $plan['quantity_type'] )
&& 'variable' === $plan['quantity_type']
&& ! empty( $plan['variable_quantity'] ) ) {
$depend_field[] = $plan['variable_quantity'];
}
return $depend_field;
}
/**
* Get payment amount
*
* @since 1.7
*
* @param array $field Field.
*
* @return double
*/
public function get_payment_amount( $field ) {
$payment_amount = 0.0;
$amount_type = self::get_property( 'amount_type', $field, 'fixed' );
$amount = self::get_property( 'amount', $field, '0' );
$amount_variable = self::get_property( 'variable', $field, '' );
$submitted_data = Forminator_CForm_Front_Action::$prepared_data;
if ( ! empty( $this->payment_plan ) ) {
$amount_type = isset( $this->payment_plan['amount_type'] ) ? $this->payment_plan['amount_type'] : $amount_type;
$amount = isset( $this->payment_plan['amount'] ) ? $this->payment_plan['amount'] : $amount;
$amount_variable = isset( $this->payment_plan['variable'] ) ? $this->payment_plan['variable'] : $amount_variable;
}
if ( 'fixed' === $amount_type ) {
$payment_amount = $amount;
} else {
$amount_var = $amount_variable;
$form_field = Forminator_Front_Action::$module_object->get_field( $amount_var, false );
if ( $form_field ) {
$form_field = $form_field->to_formatted_array();
if ( isset( $form_field['type'] ) ) {
if ( 'calculation' === $form_field['type'] ) {
// Calculation field get the amount from pseudo_submit_data.
if ( isset( Forminator_CForm_Front_Action::$prepared_data[ $amount_var ] ) ) {
$payment_amount = Forminator_CForm_Front_Action::$prepared_data[ $amount_var ];
}
} elseif ( 'currency' === $form_field['type'] ) {
// Currency field get the amount from submitted_data.
$field_id = $form_field['element_id'];
if ( isset( $submitted_data[ $field_id ] ) ) {
$payment_amount = self::forminator_replace_number( $form_field, $submitted_data[ $field_id ] );
}
} else {
$field_object = Forminator_Core::get_field_object( $form_field['type'] );
if ( $field_object ) {
$submitted_field_data = $submitted_data[ $amount_var ] ?? null;
$payment_amount = $field_object::get_calculable_value( $submitted_field_data, $form_field );
}
}
}
}
}
if ( ! is_numeric( $payment_amount ) ) {
$payment_amount = 0.0;
}
/**
* Filter payment amount of stripe
*
* @since 1.7
*
* @param double $payment_amount
* @param array $field field settings.
* @param Forminator_Form_Model $module_object
* @param array $prepared_data
*/
$payment_amount = apply_filters( 'forminator_field_stripe_payment_amount', $payment_amount, $field, Forminator_Front_Action::$module_object, Forminator_CForm_Front_Action::$prepared_data );
return $payment_amount;
}
/**
* Get Payment plan
*
* @param array $field Field.
*
* @return array
*/
public function get_payment_plan( $field ) {
$payments = self::get_property( 'payments', $field, array() );
if ( ! empty( $payments ) ) {
foreach ( $payments as $payment_settings ) {
$payment_settings['condition_rule'] = ! empty( $payment_settings['condition_rule'] ) ? $payment_settings['condition_rule'] : 'all';
$payment_settings['condition_action'] = 'show';
if ( ! Forminator_Field::is_hidden( $field, $payment_settings ) ) {
return $payment_settings;
}
}
}
return array();
}
/**
* Get transaction link
*
* @param string $mode Payment mode.
* @param string $transaction_id Transaction id.
* @return string
*/
public static function get_transanction_link( $mode, $transaction_id ) {
if ( 'test' === $mode ) {
$link_base = 'https://dashboard.stripe.com/test/payments/';
} else {
$link_base = 'https://dashboard.stripe.com/payments/';
}
$transaction_link = $link_base . rawurlencode( $transaction_id );
return $transaction_link;
}
/**
* Payment method
*
* @param string $method Payment method.
*
* @return string|void
*/
public function payment_method( $method ) {
switch ( $method ) {
case 'single':
$method = esc_html__( 'One Time', 'forminator' );
break;
case 'subscription':
$method = esc_html__( 'Subscription', 'forminator' );
break;
default:
$method = '';
}
return $method;
}
/**
* Check if the checkout session is valid for file upload
*
* @since 1.56
*
* @param string $session_id Checkout Session ID.
* @param array $field Field.
* @return bool
*/
public function is_valid_session_for_file_upload( $session_id, $field ) {
$session = $this->get_checkout_session_by_id( $session_id, $field );
if ( is_wp_error( $session ) || empty( $session->id ) ) {
return false;
}
$session_status = isset( $session->status ) ? (string) $session->status : '';
$payment_status = isset( $session->payment_status ) ? (string) $session->payment_status : '';
if ( 'expired' === $session_status || 'complete' === $session_status ) {
return false;
}
if ( 'paid' !== $payment_status ) {
return true;
}
return false;
}
/**
* Save uploaded files on checkout session
*
* @param string $session_id Stripe Checkout Session ID.
* @param array $uploaded_files Uploaded files data.
* @param string $type Field type.
*
* @return void
*/
public function add_uploaded_files_on_checkout_session( $session_id, $uploaded_files, $type = 'upload' ) {
$saved_files = get_option( self::CHECKOUT_SESSION_UPLOADED_FILES, array() );
if ( ! is_array( $saved_files ) ) {
$saved_files = array();
}
$saved_files[ $session_id ][ $type ] = $uploaded_files;
update_option( self::CHECKOUT_SESSION_UPLOADED_FILES, $saved_files, false );
}
/**
* Get uploaded files on checkout session
*
* @param string $session_id Stripe Checkout Session ID.
* @param string $type Field type.
* @return array
*/
public function get_uploaded_files_on_checkout_session( $session_id, $type = 'upload' ) {
$saved_files = get_option( self::CHECKOUT_SESSION_UPLOADED_FILES, array() );
if ( ! empty( $saved_files[ $session_id ] ) && ! empty( $saved_files[ $session_id ][ $type ] ) ) {
return $saved_files[ $session_id ][ $type ];
}
return array();
}
/**
* Remove uploaded files data associated with a Checkout Session ID.
*
* @since 1.56
*
* @param string|array $session_id Checkout Session ID.
* @return void
*/
public static function remove_checkout_session_uploaded_files_data( $session_id ) {
$uploaded_files = get_option( self::CHECKOUT_SESSION_UPLOADED_FILES, array() );
if ( is_array( $session_id ) ) {
foreach ( $session_id as $id ) {
if ( isset( $uploaded_files[ $id ] ) ) {
self::maybe_delete_file( $uploaded_files[ $id ] );
unset( $uploaded_files[ $id ] );
}
}
} elseif ( isset( $uploaded_files[ $session_id ] ) ) {
self::maybe_delete_file( $uploaded_files[ $session_id ] );
unset( $uploaded_files[ $session_id ] );
}
update_option( self::CHECKOUT_SESSION_UPLOADED_FILES, $uploaded_files, false );
}
/**
* Delete uploaded files if they are not used as featured images in any post.
*
* @param array $data Uploaded files data.
* @return void
*/
private static function maybe_delete_file( $data ) {
if ( empty( $data['postdata'] ) ) {
return;
}
foreach ( $data['postdata'] as $postdata ) {
if ( ! empty( $postdata['attachment_id'] ) ) {
$attachment_id = $postdata['attachment_id'];
// Check if the attachment is set as a featured image for any post before deleting it.
$is_featured = self::is_attachment_featured( $attachment_id );
if ( ! $is_featured ) {
wp_delete_attachment( $attachment_id, true );
}
}
}
}
/**
* Check if an attachment is set as a featured image for any post.
*
* @param int $attachment_id Attachment ID to check.
* @return bool
*/
private static function is_attachment_featured( $attachment_id ) {
global $wpdb;
return (bool) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->postmeta WHERE meta_key = '_thumbnail_id' AND meta_value = %d", (int) $attachment_id )
);
}
}