Initial commit
diff --git a/includes/payment-methods/class-wc-gateway-stripe-alipay.php b/includes/payment-methods/class-wc-gateway-stripe-alipay.php
new file mode 100644
index 0000000..534d780
--- /dev/null
+++ b/includes/payment-methods/class-wc-gateway-stripe-alipay.php
@@ -0,0 +1,308 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Class that handles Alipay payment method.
+ *
+ * @extends WC_Gateway_Stripe
+ *
+ * @since 4.0.0
+ */
+class WC_Gateway_Stripe_Alipay extends WC_Stripe_Payment_Gateway {
+
+ const ID = 'stripe_alipay';
+
+ /**
+ * Notices (array)
+ *
+ * @var array
+ */
+ public $notices = [];
+
+ /**
+ * Is test mode active?
+ *
+ * @var bool
+ */
+ public $testmode;
+
+ /**
+ * Alternate credit card statement name
+ *
+ * @var bool
+ */
+ public $statement_descriptor;
+
+ /**
+ * API access secret key
+ *
+ * @var string
+ */
+ public $secret_key;
+
+ /**
+ * Api access publishable key
+ *
+ * @var string
+ */
+ public $publishable_key;
+
+ /**
+ * Should we store the users credit cards?
+ *
+ * @var bool
+ */
+ public $saved_cards;
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ $this->id = self::ID;
+ $this->method_title = __( 'Stripe Alipay', 'woocommerce-gateway-stripe' );
+ $this->method_description = sprintf(
+ /* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+ __( 'All other general Stripe settings can be adjusted %1$shere%2$s.', 'woocommerce-gateway-stripe' ),
+ '<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=stripe' ) ) . '">',
+ '</a>'
+ );
+ $this->supports = [
+ 'products',
+ 'refunds',
+ ];
+
+ // Load the form fields.
+ $this->init_form_fields();
+
+ // Load the settings.
+ $this->init_settings();
+
+ $main_settings = get_option( 'woocommerce_stripe_settings' );
+ $this->title = $this->get_option( 'title' );
+ $this->description = $this->get_option( 'description' );
+ $this->enabled = $this->get_option( 'enabled' );
+ $this->testmode = ( ! empty( $main_settings['testmode'] ) && 'yes' === $main_settings['testmode'] ) ? true : false;
+ $this->saved_cards = ( ! empty( $main_settings['saved_cards'] ) && 'yes' === $main_settings['saved_cards'] ) ? true : false;
+ $this->publishable_key = ! empty( $main_settings['publishable_key'] ) ? $main_settings['publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['secret_key'] ) ? $main_settings['secret_key'] : '';
+ $this->statement_descriptor = ! empty( $main_settings['statement_descriptor'] ) ? $main_settings['statement_descriptor'] : '';
+
+ if ( $this->testmode ) {
+ $this->publishable_key = ! empty( $main_settings['test_publishable_key'] ) ? $main_settings['test_publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['test_secret_key'] ) ? $main_settings['test_secret_key'] : '';
+ }
+
+ add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, [ $this, 'process_admin_options' ] );
+ add_action( 'wp_enqueue_scripts', [ $this, 'payment_scripts' ] );
+ }
+
+ /**
+ * Returns all supported currencies for this payment method.
+ *
+ * @since 4.0.0
+ * @version 5.8.0
+ * @return array
+ */
+ public function get_supported_currency() {
+ return apply_filters(
+ 'wc_stripe_alipay_supported_currencies',
+ [
+ 'EUR',
+ 'AUD',
+ 'CAD',
+ 'CNY',
+ 'GBP',
+ 'HKD',
+ 'JPY',
+ 'NZD',
+ 'SGD',
+ 'USD',
+ 'MYR',
+ ]
+ );
+ }
+
+ /**
+ * Checks to see if all criteria is met before showing payment method.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return bool
+ */
+ public function is_available() {
+ if ( ! in_array( get_woocommerce_currency(), $this->get_supported_currency() ) ) {
+ return false;
+ }
+
+ return parent::is_available();
+ }
+
+ /**
+ * Get_icon function.
+ *
+ * @since 1.0.0
+ * @version 4.0.0
+ * @return string
+ */
+ public function get_icon() {
+ $icons = $this->payment_icons();
+
+ $icons_str = '';
+
+ $icons_str .= isset( $icons['alipay'] ) ? $icons['alipay'] : '';
+
+ return apply_filters( 'woocommerce_gateway_icon', $icons_str, $this->id );
+ }
+
+ /**
+ * Payment_scripts function.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ */
+ public function payment_scripts() {
+ if ( ! is_cart() && ! is_checkout() && ! isset( $_GET['pay_for_order'] ) && ! is_add_payment_method_page() ) {
+ return;
+ }
+
+ wp_enqueue_style( 'stripe_styles' );
+ wp_enqueue_script( 'woocommerce_stripe' );
+ }
+
+ /**
+ * Initialize Gateway Settings Form Fields.
+ */
+ public function init_form_fields() {
+ $this->form_fields = require WC_STRIPE_PLUGIN_PATH . '/includes/admin/stripe-alipay-settings.php';
+ }
+
+ /**
+ * Payment form on checkout page
+ */
+ public function payment_fields() {
+ global $wp;
+ $user = wp_get_current_user();
+ $total = WC()->cart->total;
+ $description = $this->get_description();
+
+ // If paying from order, we need to get total from order not cart.
+ if ( isset( $_GET['pay_for_order'] ) && ! empty( $_GET['key'] ) ) {
+ $order = wc_get_order( wc_clean( $wp->query_vars['order-pay'] ) );
+ $total = $order->get_total();
+ }
+
+ if ( is_add_payment_method_page() ) {
+ $pay_button_text = __( 'Add Payment', 'woocommerce-gateway-stripe' );
+ $total = '';
+ } else {
+ $pay_button_text = '';
+ }
+
+ echo '<div
+ id="stripe-alipay-payment-data"
+ data-amount="' . esc_attr( WC_Stripe_Helper::get_stripe_amount( $total ) ) . '"
+ data-currency="' . esc_attr( strtolower( get_woocommerce_currency() ) ) . '">';
+
+ if ( $description ) {
+ echo apply_filters( 'wc_stripe_description', wpautop( wp_kses_post( $description ) ), $this->id );
+ }
+
+ echo '</div>';
+ }
+
+ /**
+ * Creates the source for charge.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @param object $order
+ * @return mixed
+ */
+ public function create_source( $order ) {
+ $currency = $order->get_currency();
+ $return_url = $this->get_stripe_return_url( $order );
+ $post_data = [];
+ $post_data['amount'] = WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $currency );
+ $post_data['currency'] = strtolower( $currency );
+ $post_data['type'] = 'alipay';
+ $post_data['owner'] = $this->get_owner_details( $order );
+ $post_data['redirect'] = [ 'return_url' => $return_url ];
+
+ if ( ! empty( $this->statement_descriptor ) ) {
+ $post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $this->statement_descriptor );
+ }
+
+ WC_Stripe_Logger::log( 'Info: Begin creating Alipay source' );
+
+ return WC_Stripe_API::request( apply_filters( 'wc_stripe_alipay_source', $post_data, $order ), 'sources' );
+ }
+
+ /**
+ * Process the payment
+ *
+ * @param int $order_id Reference.
+ * @param bool $retry Should we retry on fail.
+ * @param bool $force_save_source Force payment source to be saved.
+ *
+ * @throws Exception If payment will not be accepted.
+ *
+ * @return array|void
+ */
+ public function process_payment( $order_id, $retry = true, $force_save_save = false ) {
+ try {
+ $order = wc_get_order( $order_id );
+
+ // This will throw exception if not valid.
+ $this->validate_minimum_order_amount( $order );
+
+ // This comes from the create account checkbox in the checkout page.
+ $create_account = ! empty( $_POST['createaccount'] ) ? true : false;
+
+ if ( $create_account ) {
+ $new_customer_id = $order->get_customer_id();
+ $new_stripe_customer = new WC_Stripe_Customer( $new_customer_id );
+ $new_stripe_customer->create_customer();
+ }
+
+ $response = $this->create_source( $order );
+
+ if ( ! empty( $response->error ) ) {
+ $order->add_order_note( $response->error->message );
+
+ throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
+ }
+
+ $order->update_meta_data( '_stripe_source_id', $response->id );
+ $order->save();
+
+ WC_Stripe_Logger::log( 'Info: Redirecting to Alipay...' );
+
+ return [
+ 'result' => 'success',
+ 'redirect' => esc_url_raw( $response->redirect->url ),
+ ];
+ } catch ( WC_Stripe_Exception $e ) {
+ wc_add_notice( $e->getLocalizedMessage(), 'error' );
+ WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
+
+ do_action( 'wc_gateway_stripe_process_payment_error', $e, $order );
+
+ $statuses = apply_filters(
+ 'wc_stripe_allowed_payment_processing_statuses',
+ [ 'pending', 'failed' ],
+ $order
+ );
+
+ if ( $order->has_status( $statuses ) ) {
+ $this->send_failed_order_email( $order_id );
+ }
+
+ return [
+ 'result' => 'fail',
+ 'redirect' => '',
+ ];
+ }
+ }
+}
diff --git a/includes/payment-methods/class-wc-gateway-stripe-bancontact.php b/includes/payment-methods/class-wc-gateway-stripe-bancontact.php
new file mode 100644
index 0000000..d983af8
--- /dev/null
+++ b/includes/payment-methods/class-wc-gateway-stripe-bancontact.php
@@ -0,0 +1,296 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Class that handles Bancontact payment method.
+ *
+ * @extends WC_Gateway_Stripe
+ *
+ * @since 4.0.0
+ */
+class WC_Gateway_Stripe_Bancontact extends WC_Stripe_Payment_Gateway {
+
+ const ID = 'stripe_bancontact';
+
+ /**
+ * Notices (array)
+ *
+ * @var array
+ */
+ public $notices = [];
+
+ /**
+ * Is test mode active?
+ *
+ * @var bool
+ */
+ public $testmode;
+
+ /**
+ * Alternate credit card statement name
+ *
+ * @var bool
+ */
+ public $statement_descriptor;
+
+ /**
+ * API access secret key
+ *
+ * @var string
+ */
+ public $secret_key;
+
+ /**
+ * Api access publishable key
+ *
+ * @var string
+ */
+ public $publishable_key;
+
+ /**
+ * Should we store the users credit cards?
+ *
+ * @var bool
+ */
+ public $saved_cards;
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ $this->id = self::ID;
+ $this->method_title = __( 'Stripe Bancontact', 'woocommerce-gateway-stripe' );
+ $this->method_description = sprintf(
+ /* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+ __( 'All other general Stripe settings can be adjusted %1$shere%2$s.', 'woocommerce-gateway-stripe' ),
+ '<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=stripe' ) ) . '">',
+ '</a>'
+ );
+ $this->supports = [
+ 'products',
+ 'refunds',
+ ];
+
+ // Load the form fields.
+ $this->init_form_fields();
+
+ // Load the settings.
+ $this->init_settings();
+
+ $main_settings = get_option( 'woocommerce_stripe_settings' );
+ $this->title = $this->get_option( 'title' );
+ $this->description = $this->get_option( 'description' );
+ $this->enabled = $this->get_option( 'enabled' );
+ $this->testmode = ( ! empty( $main_settings['testmode'] ) && 'yes' === $main_settings['testmode'] ) ? true : false;
+ $this->saved_cards = ( ! empty( $main_settings['saved_cards'] ) && 'yes' === $main_settings['saved_cards'] ) ? true : false;
+ $this->publishable_key = ! empty( $main_settings['publishable_key'] ) ? $main_settings['publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['secret_key'] ) ? $main_settings['secret_key'] : '';
+ $this->statement_descriptor = ! empty( $main_settings['statement_descriptor'] ) ? $main_settings['statement_descriptor'] : '';
+
+ if ( $this->testmode ) {
+ $this->publishable_key = ! empty( $main_settings['test_publishable_key'] ) ? $main_settings['test_publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['test_secret_key'] ) ? $main_settings['test_secret_key'] : '';
+ }
+
+ add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, [ $this, 'process_admin_options' ] );
+ add_action( 'wp_enqueue_scripts', [ $this, 'payment_scripts' ] );
+ }
+
+ /**
+ * Returns all supported currencies for this payment method.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return array
+ */
+ public function get_supported_currency() {
+ return apply_filters(
+ 'wc_stripe_bancontact_supported_currencies',
+ [
+ 'EUR',
+ ]
+ );
+ }
+
+ /**
+ * Checks to see if all criteria is met before showing payment method.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return bool
+ */
+ public function is_available() {
+ if ( ! in_array( get_woocommerce_currency(), $this->get_supported_currency() ) ) {
+ return false;
+ }
+
+ return parent::is_available();
+ }
+
+ /**
+ * Get_icon function.
+ *
+ * @since 1.0.0
+ * @version 4.0.0
+ * @return string
+ */
+ public function get_icon() {
+ $icons = $this->payment_icons();
+
+ $icons_str = '';
+
+ $icons_str .= isset( $icons['bancontact'] ) ? $icons['bancontact'] : '';
+
+ return apply_filters( 'woocommerce_gateway_icon', $icons_str, $this->id );
+ }
+
+ /**
+ * Outputs scripts used for stripe payment
+ */
+ public function payment_scripts() {
+ if ( ! is_cart() && ! is_checkout() && ! isset( $_GET['pay_for_order'] ) && ! is_add_payment_method_page() ) {
+ return;
+ }
+
+ wp_enqueue_style( 'stripe_styles' );
+ wp_enqueue_script( 'woocommerce_stripe' );
+ }
+
+ /**
+ * Initialize Gateway Settings Form Fields.
+ */
+ public function init_form_fields() {
+ $this->form_fields = require WC_STRIPE_PLUGIN_PATH . '/includes/admin/stripe-bancontact-settings.php';
+ }
+
+ /**
+ * Payment form on checkout page
+ */
+ public function payment_fields() {
+ global $wp;
+ $user = wp_get_current_user();
+ $total = WC()->cart->total;
+ $description = $this->get_description();
+
+ // If paying from order, we need to get total from order not cart.
+ if ( isset( $_GET['pay_for_order'] ) && ! empty( $_GET['key'] ) ) {
+ $order = wc_get_order( wc_clean( $wp->query_vars['order-pay'] ) );
+ $total = $order->get_total();
+ }
+
+ if ( is_add_payment_method_page() ) {
+ $pay_button_text = __( 'Add Payment', 'woocommerce-gateway-stripe' );
+ $total = '';
+ } else {
+ $pay_button_text = '';
+ }
+
+ echo '<div
+ id="stripe-bancontact-payment-data"
+ data-amount="' . esc_attr( WC_Stripe_Helper::get_stripe_amount( $total ) ) . '"
+ data-currency="' . esc_attr( strtolower( get_woocommerce_currency() ) ) . '">';
+
+ if ( $description ) {
+ echo apply_filters( 'wc_stripe_description', wpautop( wp_kses_post( $description ) ), $this->id );
+ }
+
+ echo '</div>';
+ }
+
+ /**
+ * Creates the source for charge.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @param object $order
+ * @return mixed
+ */
+ public function create_source( $order ) {
+ $currency = $order->get_currency();
+ $return_url = $this->get_stripe_return_url( $order );
+ $post_data = [];
+ $post_data['amount'] = WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $currency );
+ $post_data['currency'] = strtolower( $currency );
+ $post_data['type'] = 'bancontact';
+ $post_data['owner'] = $this->get_owner_details( $order );
+ $post_data['redirect'] = [ 'return_url' => $return_url ];
+ $post_data['bancontact'] = [ 'preferred_language' => $this->get_locale() ];
+
+ if ( ! empty( $this->statement_descriptor ) ) {
+ $post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $this->statement_descriptor );
+ }
+
+ WC_Stripe_Logger::log( 'Info: Begin creating Bancontact source' );
+
+ return WC_Stripe_API::request( apply_filters( 'wc_stripe_bancontact_source', $post_data, $order ), 'sources' );
+ }
+
+ /**
+ * Process the payment
+ *
+ * @param int $order_id Reference.
+ * @param bool $retry Should we retry on fail.
+ * @param bool $force_save_source Force payment source to be saved.
+ *
+ * @throws Exception If payment will not be accepted.
+ *
+ * @return array|void
+ */
+ public function process_payment( $order_id, $retry = true, $force_save_source = false ) {
+ try {
+ $order = wc_get_order( $order_id );
+
+ // This will throw exception if not valid.
+ $this->validate_minimum_order_amount( $order );
+
+ // This comes from the create account checkbox in the checkout page.
+ $create_account = ! empty( $_POST['createaccount'] ) ? true : false;
+
+ if ( $create_account ) {
+ $new_customer_id = $order->get_customer_id();
+ $new_stripe_customer = new WC_Stripe_Customer( $new_customer_id );
+ $new_stripe_customer->create_customer();
+ }
+
+ $response = $this->create_source( $order );
+
+ if ( ! empty( $response->error ) ) {
+ $order->add_order_note( $response->error->message );
+
+ throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
+ }
+
+ $order->update_meta_data( '_stripe_source_id', $response->id );
+ $order->save();
+
+ WC_Stripe_Logger::log( 'Info: Redirecting to Bancontact...' );
+
+ return [
+ 'result' => 'success',
+ 'redirect' => esc_url_raw( $response->redirect->url ),
+ ];
+ } catch ( WC_Stripe_Exception $e ) {
+ wc_add_notice( $e->getLocalizedMessage(), 'error' );
+ WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
+
+ do_action( 'wc_gateway_stripe_process_payment_error', $e, $order );
+
+ if ( $order->has_status(
+ apply_filters(
+ 'wc_stripe_allowed_payment_processing_statuses',
+ [ 'pending', 'failed' ],
+ $order
+ )
+ ) ) {
+ $this->send_failed_order_email( $order_id );
+ }
+
+ return [
+ 'result' => 'fail',
+ 'redirect' => '',
+ ];
+ }
+ }
+}
diff --git a/includes/payment-methods/class-wc-gateway-stripe-boleto.php b/includes/payment-methods/class-wc-gateway-stripe-boleto.php
new file mode 100644
index 0000000..bac1ba1
--- /dev/null
+++ b/includes/payment-methods/class-wc-gateway-stripe-boleto.php
@@ -0,0 +1,200 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Class that handles Boleto payment method.
+ *
+ * @extends WC_Gateway_Stripe
+ *
+ * @since 5.8.0
+ */
+class WC_Gateway_Stripe_Boleto extends WC_Stripe_Payment_Gateway_Voucher {
+
+ /**
+ * ID used by UPE
+ *
+ * @var string
+ */
+ const ID = 'stripe_boleto';
+
+ /**
+ * ID used by WooCommerce to identify the payment method
+ *
+ * @var string
+ */
+ public $id = 'stripe_boleto';
+
+ /**
+ * ID used by stripe
+ */
+ protected $stripe_id = 'boleto';
+
+ /**
+ * List of accepted currencies
+ *
+ * @var array
+ */
+ protected $supported_currencies = [ 'BRL' ];
+
+ /**
+ * List of accepted countries
+ */
+ protected $supported_countries = [ 'BR' ];
+
+ /**
+ * Constructor
+ *
+ * @since 5.8.0
+ */
+ public function __construct() {
+ $this->method_title = __( 'Stripe Boleto', 'woocommerce-gateway-stripe' );
+ parent::__construct();
+
+ add_filter( 'wc_stripe_allowed_payment_processing_statuses', [ $this, 'add_allowed_payment_processing_statuses' ], 10, 2 );
+ }
+
+ /**
+ * Add payment gateway voucher expiration to API request body.
+ *
+ * @param array $body API request body.
+ * @return array
+ */
+ protected function update_request_body_on_create_or_update_payment_intent( $body ) {
+ $body['payment_method_options'] = [
+ 'boleto' => [
+ 'expires_after_days' => $this->get_option( 'expiration' ),
+ ],
+ ];
+ return $body;
+ }
+
+ /**
+ * Add payment gateway voucher expiration.
+ *
+ * @param array $settings Settings array.
+ * @return array
+ */
+ public function get_unique_settings( $settings ) {
+ $settings[ $this->id . '_expiration' ] = $this->get_option( 'expiration' );
+ return $settings;
+ }
+
+ /**
+ * Updates payment gateway voucher expiration.
+ *
+ * @param WP_REST_Request $request Request object.
+ * @return void
+ */
+ public function update_unique_settings( WP_REST_Request $request ) {
+ $field_name = $this->id . '_expiration';
+ $expiration = $request->get_param( $field_name );
+
+ if ( null === $expiration ) {
+ return;
+ }
+
+ $value = absint( $expiration );
+ $value = min( 60, $value );
+ $value = max( 0, $value );
+ $this->update_option( 'expiration', $value );
+ }
+
+ /**
+ * Adds on-hold as accepted status during webhook handling on orders paid with voucher
+ *
+ * @param $allowed_statuses
+ * @param $order
+ *
+ * @return mixed
+ */
+ public function add_allowed_payment_processing_statuses( $allowed_statuses, $order ) {
+ if ( $this->stripe_id === $order->get_meta( '_stripe_upe_payment_type' ) && ! in_array( 'on-hold', $allowed_statuses ) ) {
+ $allowed_statuses[] = 'on-hold';
+ }
+
+ return $allowed_statuses;
+ }
+
+ /**
+ * Payment_scripts function.
+ *
+ * @since 5.8.0
+ */
+ public function payment_scripts() {
+ if ( ! is_cart() && ! is_checkout() && ! isset( $_GET['pay_for_order'] ) && ! is_add_payment_method_page() ) {
+ return;
+ }
+
+ parent::payment_scripts();
+ wp_enqueue_script( 'jquery-mask', plugins_url( 'assets/js/jquery.mask.min.js', WC_STRIPE_MAIN_FILE ), [], WC_STRIPE_VERSION );
+ }
+
+ /**
+ * Payment form on checkout page
+ *
+ * @since 5.8.0
+ */
+ public function payment_fields() {
+ $description = $this->get_description();
+ apply_filters( 'wc_stripe_description', wpautop( wp_kses_post( $description ) ), $this->id )
+
+ ?>
+ <label>CPF/CNPJ: <abbr class="required" title="required">*</abbr></label><br>
+ <input id="stripe_boleto_tax_id" name="stripe_boleto_tax_id" type="text"><br><br>
+ <div class="stripe-source-errors" role="alert"></div>
+
+ <div id="stripe-boleto-payment-data"><?php echo $description; ?></div>
+ <?php
+ }
+
+ /**
+ * Validates the minimum and maximum amount. Throws exception when out of range value is added
+ *
+ * @since 5.8.0
+ *
+ * @param $amount
+ *
+ * @throws WC_Stripe_Exception
+ */
+ protected function validate_amount_limits( $amount ) {
+
+ if ( $amount < 5.00 ) {
+ /* translators: 1) amount (including currency symbol) */
+ throw new WC_Stripe_Exception( sprintf( __( 'Sorry, the minimum allowed order total is %1$s to use this payment method.', 'woocommerce-gateway-stripe' ), wc_price( 5.00 ) ) );
+ } elseif ( $amount > 49999.99 ) {
+ /* translators: 1) amount (including currency symbol) */
+ throw new WC_Stripe_Exception( sprintf( __( 'Sorry, the maximum allowed order total is %1$s to use this payment method.', 'woocommerce-gateway-stripe' ), wc_price( 49999.99 ) ) );
+ }
+ }
+
+ /**
+ * Gather the data necessary to confirm the payment via javascript
+ * Override this when extending the class
+ *
+ * @param WC_Order $order
+ *
+ * @return array
+ */
+ protected function get_confirm_payment_data( $order ) {
+ return [
+ 'payment_method' => [
+ 'boleto' => [
+ 'tax_id' => isset( $_POST['stripe_boleto_tax_id'] ) ? wc_clean( wp_unslash( $_POST['stripe_boleto_tax_id'] ) ) : null,
+ ],
+ 'billing_details' => [
+ 'name' => $order->get_formatted_billing_full_name(),
+ 'email' => $order->get_billing_email(),
+ 'address' => [
+ 'line1' => $order->get_billing_address_1(),
+ 'city' => $order->get_billing_city(),
+ 'state' => $order->get_billing_state(),
+ 'postal_code' => $order->get_billing_postcode(),
+ 'country' => $order->get_billing_country(),
+ ],
+ ],
+ ],
+ ];
+ }
+}
diff --git a/includes/payment-methods/class-wc-gateway-stripe-eps.php b/includes/payment-methods/class-wc-gateway-stripe-eps.php
new file mode 100644
index 0000000..6f5b77e
--- /dev/null
+++ b/includes/payment-methods/class-wc-gateway-stripe-eps.php
@@ -0,0 +1,295 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Class that handles EPS payment method.
+ *
+ * @extends WC_Gateway_Stripe
+ *
+ * @since 4.1.0
+ */
+class WC_Gateway_Stripe_Eps extends WC_Stripe_Payment_Gateway {
+
+ const ID = 'stripe_eps';
+
+ /**
+ * Notices (array)
+ *
+ * @var array
+ */
+ public $notices = [];
+
+ /**
+ * Is test mode active?
+ *
+ * @var bool
+ */
+ public $testmode;
+
+ /**
+ * Alternate credit card statement name
+ *
+ * @var bool
+ */
+ public $statement_descriptor;
+
+ /**
+ * API access secret key
+ *
+ * @var string
+ */
+ public $secret_key;
+
+ /**
+ * Api access publishable key
+ *
+ * @var string
+ */
+ public $publishable_key;
+
+ /**
+ * Should we store the users credit cards?
+ *
+ * @var bool
+ */
+ public $saved_cards;
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ $this->id = self::ID;
+ $this->method_title = __( 'Stripe EPS', 'woocommerce-gateway-stripe' );
+ $this->method_description = sprintf(
+ /* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+ __( 'All other general Stripe settings can be adjusted %1$shere%2$s.', 'woocommerce-gateway-stripe' ),
+ '<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=stripe' ) ) . '">',
+ '</a>'
+ );
+ $this->supports = [
+ 'products',
+ 'refunds',
+ ];
+
+ // Load the form fields.
+ $this->init_form_fields();
+
+ // Load the settings.
+ $this->init_settings();
+
+ $main_settings = get_option( 'woocommerce_stripe_settings' );
+ $this->title = $this->get_option( 'title' );
+ $this->description = $this->get_option( 'description' );
+ $this->enabled = $this->get_option( 'enabled' );
+ $this->testmode = ( ! empty( $main_settings['testmode'] ) && 'yes' === $main_settings['testmode'] ) ? true : false;
+ $this->saved_cards = ( ! empty( $main_settings['saved_cards'] ) && 'yes' === $main_settings['saved_cards'] ) ? true : false;
+ $this->publishable_key = ! empty( $main_settings['publishable_key'] ) ? $main_settings['publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['secret_key'] ) ? $main_settings['secret_key'] : '';
+ $this->statement_descriptor = ! empty( $main_settings['statement_descriptor'] ) ? $main_settings['statement_descriptor'] : '';
+
+ if ( $this->testmode ) {
+ $this->publishable_key = ! empty( $main_settings['test_publishable_key'] ) ? $main_settings['test_publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['test_secret_key'] ) ? $main_settings['test_secret_key'] : '';
+ }
+
+ add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, [ $this, 'process_admin_options' ] );
+ add_action( 'wp_enqueue_scripts', [ $this, 'payment_scripts' ] );
+ }
+
+ /**
+ * Returns all supported currencies for this payment method.
+ *
+ * @since 4.1.0
+ * @version 4.1.0
+ * @return array
+ */
+ public function get_supported_currency() {
+ return apply_filters(
+ 'wc_stripe_eps_supported_currencies',
+ [
+ 'EUR',
+ ]
+ );
+ }
+
+ /**
+ * Checks to see if all criteria is met before showing payment method.
+ *
+ * @since 4.1.0
+ * @version 4.1.0
+ * @return bool
+ */
+ public function is_available() {
+ if ( ! in_array( get_woocommerce_currency(), $this->get_supported_currency() ) ) {
+ return false;
+ }
+
+ return parent::is_available();
+ }
+
+ /**
+ * Get_icon function.
+ *
+ * @since 1.0.0
+ * @version 4.1.0
+ * @return string
+ */
+ public function get_icon() {
+ $icons = $this->payment_icons();
+
+ $icons_str = '';
+
+ $icons_str .= isset( $icons['eps'] ) ? $icons['eps'] : '';
+
+ return apply_filters( 'woocommerce_gateway_icon', $icons_str, $this->id );
+ }
+
+ /**
+ * Outputs scripts used for stripe payment
+ */
+ public function payment_scripts() {
+ if ( ! is_cart() && ! is_checkout() && ! isset( $_GET['pay_for_order'] ) && ! is_add_payment_method_page() ) {
+ return;
+ }
+
+ wp_enqueue_style( 'stripe_styles' );
+ wp_enqueue_script( 'woocommerce_stripe' );
+ }
+
+ /**
+ * Initialize Gateway Settings Form Fields.
+ */
+ public function init_form_fields() {
+ $this->form_fields = require WC_STRIPE_PLUGIN_PATH . '/includes/admin/stripe-eps-settings.php';
+ }
+
+ /**
+ * Payment form on checkout page
+ */
+ public function payment_fields() {
+ global $wp;
+ $user = wp_get_current_user();
+ $total = WC()->cart->total;
+ $description = $this->get_description();
+
+ // If paying from order, we need to get total from order not cart.
+ if ( isset( $_GET['pay_for_order'] ) && ! empty( $_GET['key'] ) ) {
+ $order = wc_get_order( wc_clean( $wp->query_vars['order-pay'] ) );
+ $total = $order->get_total();
+ }
+
+ if ( is_add_payment_method_page() ) {
+ $pay_button_text = __( 'Add Payment', 'woocommerce-gateway-stripe' );
+ $total = '';
+ } else {
+ $pay_button_text = '';
+ }
+
+ echo '<div
+ id="stripe-eps-payment-data"
+ data-amount="' . esc_attr( WC_Stripe_Helper::get_stripe_amount( $total ) ) . '"
+ data-currency="' . esc_attr( strtolower( get_woocommerce_currency() ) ) . '">';
+
+ if ( $description ) {
+ echo apply_filters( 'wc_stripe_description', wpautop( wp_kses_post( $description ) ), $this->id );
+ }
+
+ echo '</div>';
+ }
+
+ /**
+ * Creates the source for charge.
+ *
+ * @since 4.1.0
+ * @version 4.1.0
+ * @param object $order
+ * @return mixed
+ */
+ public function create_source( $order ) {
+ $currency = $order->get_currency();
+ $return_url = $this->get_stripe_return_url( $order );
+ $post_data = [];
+ $post_data['amount'] = WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $currency );
+ $post_data['currency'] = strtolower( $currency );
+ $post_data['type'] = 'eps';
+ $post_data['owner'] = $this->get_owner_details( $order );
+ $post_data['redirect'] = [ 'return_url' => $return_url ];
+
+ if ( ! empty( $this->statement_descriptor ) ) {
+ $post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $this->statement_descriptor );
+ }
+
+ WC_Stripe_Logger::log( 'Info: Begin creating EPS source' );
+
+ return WC_Stripe_API::request( $post_data, 'sources' );
+ }
+
+ /**
+ * Process the payment
+ *
+ * @param int $order_id Reference.
+ * @param bool $retry Should we retry on fail.
+ * @param bool $force_save_source Force payment source to be saved.
+ *
+ * @throws Exception If payment will not be accepted.
+ *
+ * @return array|void
+ */
+ public function process_payment( $order_id, $retry = true, $force_save_source = false ) {
+ try {
+ $order = wc_get_order( $order_id );
+
+ // This will throw exception if not valid.
+ $this->validate_minimum_order_amount( $order );
+
+ // This comes from the create account checkbox in the checkout page.
+ $create_account = ! empty( $_POST['createaccount'] ) ? true : false;
+
+ if ( $create_account ) {
+ $new_customer_id = $order->get_customer_id();
+ $new_stripe_customer = new WC_Stripe_Customer( $new_customer_id );
+ $new_stripe_customer->create_customer();
+ }
+
+ $response = $this->create_source( $order );
+
+ if ( ! empty( $response->error ) ) {
+ $order->add_order_note( $response->error->message );
+
+ throw new Exception( $response->error->message );
+ }
+
+ $order->update_meta_data( '_stripe_source_id', $response->id );
+ $order->save();
+
+ WC_Stripe_Logger::log( 'Info: Redirecting to EPS...' );
+
+ return [
+ 'result' => 'success',
+ 'redirect' => esc_url_raw( $response->redirect->url ),
+ ];
+ } catch ( Exception $e ) {
+ wc_add_notice( $e->getMessage(), 'error' );
+ WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
+
+ do_action( 'wc_gateway_stripe_process_payment_error', $e, $order );
+
+ if ( $order->has_status(
+ apply_filters(
+ 'wc_stripe_allowed_payment_processing_statuses',
+ [ 'pending', 'failed' ],
+ $order
+ )
+ ) ) {
+ $this->send_failed_order_email( $order_id );
+ }
+
+ return [
+ 'result' => 'fail',
+ 'redirect' => '',
+ ];
+ }
+ }
+}
diff --git a/includes/payment-methods/class-wc-gateway-stripe-giropay.php b/includes/payment-methods/class-wc-gateway-stripe-giropay.php
new file mode 100644
index 0000000..96ca48e
--- /dev/null
+++ b/includes/payment-methods/class-wc-gateway-stripe-giropay.php
@@ -0,0 +1,295 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Class that handles giropay payment method.
+ *
+ * @extends WC_Gateway_Stripe
+ *
+ * @since 4.0.0
+ */
+class WC_Gateway_Stripe_Giropay extends WC_Stripe_Payment_Gateway {
+
+ const ID = 'stripe_giropay';
+
+ /**
+ * Notices (array)
+ *
+ * @var array
+ */
+ public $notices = [];
+
+ /**
+ * Is test mode active?
+ *
+ * @var bool
+ */
+ public $testmode;
+
+ /**
+ * Alternate credit card statement name
+ *
+ * @var bool
+ */
+ public $statement_descriptor;
+
+ /**
+ * API access secret key
+ *
+ * @var string
+ */
+ public $secret_key;
+
+ /**
+ * Api access publishable key
+ *
+ * @var string
+ */
+ public $publishable_key;
+
+ /**
+ * Should we store the users credit cards?
+ *
+ * @var bool
+ */
+ public $saved_cards;
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ $this->id = self::ID;
+ $this->method_title = __( 'Stripe giropay', 'woocommerce-gateway-stripe' );
+ $this->method_description = sprintf(
+ /* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+ __( 'All other general Stripe settings can be adjusted %1$shere%2$s.', 'woocommerce-gateway-stripe' ),
+ '<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=stripe' ) ) . '">',
+ '</a>'
+ );
+ $this->supports = [
+ 'products',
+ 'refunds',
+ ];
+
+ // Load the form fields.
+ $this->init_form_fields();
+
+ // Load the settings.
+ $this->init_settings();
+
+ $main_settings = get_option( 'woocommerce_stripe_settings' );
+ $this->title = $this->get_option( 'title' );
+ $this->description = $this->get_option( 'description' );
+ $this->enabled = $this->get_option( 'enabled' );
+ $this->testmode = ( ! empty( $main_settings['testmode'] ) && 'yes' === $main_settings['testmode'] ) ? true : false;
+ $this->saved_cards = ( ! empty( $main_settings['saved_cards'] ) && 'yes' === $main_settings['saved_cards'] ) ? true : false;
+ $this->publishable_key = ! empty( $main_settings['publishable_key'] ) ? $main_settings['publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['secret_key'] ) ? $main_settings['secret_key'] : '';
+ $this->statement_descriptor = ! empty( $main_settings['statement_descriptor'] ) ? $main_settings['statement_descriptor'] : '';
+
+ if ( $this->testmode ) {
+ $this->publishable_key = ! empty( $main_settings['test_publishable_key'] ) ? $main_settings['test_publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['test_secret_key'] ) ? $main_settings['test_secret_key'] : '';
+ }
+
+ add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, [ $this, 'process_admin_options' ] );
+ add_action( 'wp_enqueue_scripts', [ $this, 'payment_scripts' ] );
+ }
+
+ /**
+ * Returns all supported currencies for this payment method.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return array
+ */
+ public function get_supported_currency() {
+ return apply_filters(
+ 'wc_stripe_giropay_supported_currencies',
+ [
+ 'EUR',
+ ]
+ );
+ }
+
+ /**
+ * Checks to see if all criteria is met before showing payment method.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return bool
+ */
+ public function is_available() {
+ if ( ! in_array( get_woocommerce_currency(), $this->get_supported_currency() ) ) {
+ return false;
+ }
+
+ return parent::is_available();
+ }
+
+ /**
+ * Get_icon function.
+ *
+ * @since 1.0.0
+ * @version 4.0.0
+ * @return string
+ */
+ public function get_icon() {
+ $icons = $this->payment_icons();
+
+ $icons_str = '';
+
+ $icons_str .= isset( $icons['giropay'] ) ? $icons['giropay'] : '';
+
+ return apply_filters( 'woocommerce_gateway_icon', $icons_str, $this->id );
+ }
+
+ /**
+ * Outputs scripts used for stripe payment
+ */
+ public function payment_scripts() {
+ if ( ! is_cart() && ! is_checkout() && ! isset( $_GET['pay_for_order'] ) && ! is_add_payment_method_page() ) {
+ return;
+ }
+
+ wp_enqueue_style( 'stripe_styles' );
+ wp_enqueue_script( 'woocommerce_stripe' );
+ }
+
+ /**
+ * Initialize Gateway Settings Form Fields.
+ */
+ public function init_form_fields() {
+ $this->form_fields = require WC_STRIPE_PLUGIN_PATH . '/includes/admin/stripe-giropay-settings.php';
+ }
+
+ /**
+ * Payment form on checkout page
+ */
+ public function payment_fields() {
+ global $wp;
+ $user = wp_get_current_user();
+ $total = WC()->cart->total;
+ $description = $this->get_description();
+
+ // If paying from order, we need to get total from order not cart.
+ if ( isset( $_GET['pay_for_order'] ) && ! empty( $_GET['key'] ) ) {
+ $order = wc_get_order( wc_clean( $wp->query_vars['order-pay'] ) );
+ $total = $order->get_total();
+ }
+
+ if ( is_add_payment_method_page() ) {
+ $pay_button_text = __( 'Add Payment', 'woocommerce-gateway-stripe' );
+ $total = '';
+ } else {
+ $pay_button_text = '';
+ }
+
+ echo '<div
+ id="stripe-giropay-payment-data"
+ data-amount="' . esc_attr( WC_Stripe_Helper::get_stripe_amount( $total ) ) . '"
+ data-currency="' . esc_attr( strtolower( get_woocommerce_currency() ) ) . '">';
+
+ if ( $description ) {
+ echo apply_filters( 'wc_stripe_description', wpautop( wp_kses_post( $description ) ), $this->id );
+ }
+
+ echo '</div>';
+ }
+
+ /**
+ * Creates the source for charge.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @param object $order
+ * @return mixed
+ */
+ public function create_source( $order ) {
+ $currency = $order->get_currency();
+ $return_url = $this->get_stripe_return_url( $order );
+ $post_data = [];
+ $post_data['amount'] = WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $currency );
+ $post_data['currency'] = strtolower( $currency );
+ $post_data['type'] = 'giropay';
+ $post_data['owner'] = $this->get_owner_details( $order );
+ $post_data['redirect'] = [ 'return_url' => $return_url ];
+
+ if ( ! empty( $this->statement_descriptor ) ) {
+ $post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $this->statement_descriptor );
+ }
+
+ WC_Stripe_Logger::log( 'Info: Begin creating giropay source' );
+
+ return WC_Stripe_API::request( apply_filters( 'wc_stripe_giropay_source', $post_data, $order ), 'sources' );
+ }
+
+ /**
+ * Process the payment
+ *
+ * @param int $order_id Reference.
+ * @param bool $retry Should we retry on fail.
+ * @param bool $force_save_source Force payment source to be saved.
+ *
+ * @throws Exception If payment will not be accepted.
+ *
+ * @return array|void
+ */
+ public function process_payment( $order_id, $retry = true, $force_save_source = false ) {
+ try {
+ $order = wc_get_order( $order_id );
+
+ // This will throw exception if not valid.
+ $this->validate_minimum_order_amount( $order );
+
+ // This comes from the create account checkbox in the checkout page.
+ $create_account = ! empty( $_POST['createaccount'] ) ? true : false;
+
+ if ( $create_account ) {
+ $new_customer_id = $order->get_customer_id();
+ $new_stripe_customer = new WC_Stripe_Customer( $new_customer_id );
+ $new_stripe_customer->create_customer();
+ }
+
+ $response = $this->create_source( $order );
+
+ if ( ! empty( $response->error ) ) {
+ $order->add_order_note( $response->error->message );
+
+ throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
+ }
+
+ $order->update_meta_data( '_stripe_source_id', $response->id );
+ $order->save();
+
+ WC_Stripe_Logger::log( 'Info: Redirecting to giropay...' );
+
+ return [
+ 'result' => 'success',
+ 'redirect' => esc_url_raw( $response->redirect->url ),
+ ];
+ } catch ( WC_Stripe_Exception $e ) {
+ wc_add_notice( $e->getLocalizedMessage(), 'error' );
+ WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
+
+ do_action( 'wc_gateway_stripe_process_payment_error', $e, $order );
+
+ if ( $order->has_status(
+ apply_filters(
+ 'wc_stripe_allowed_payment_processing_statuses',
+ [ 'pending', 'failed' ],
+ $order
+ )
+ ) ) {
+ $this->send_failed_order_email( $order_id );
+ }
+
+ return [
+ 'result' => 'fail',
+ 'redirect' => '',
+ ];
+ }
+ }
+}
diff --git a/includes/payment-methods/class-wc-gateway-stripe-ideal.php b/includes/payment-methods/class-wc-gateway-stripe-ideal.php
new file mode 100644
index 0000000..86e98a8
--- /dev/null
+++ b/includes/payment-methods/class-wc-gateway-stripe-ideal.php
@@ -0,0 +1,295 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Class that handles iDEAL payment method.
+ *
+ * @extends WC_Gateway_Stripe
+ *
+ * @since 4.0.0
+ */
+class WC_Gateway_Stripe_Ideal extends WC_Stripe_Payment_Gateway {
+
+ const ID = 'stripe_ideal';
+
+ /**
+ * Notices (array)
+ *
+ * @var array
+ */
+ public $notices = [];
+
+ /**
+ * Is test mode active?
+ *
+ * @var bool
+ */
+ public $testmode;
+
+ /**
+ * Alternate credit card statement name
+ *
+ * @var bool
+ */
+ public $statement_descriptor;
+
+ /**
+ * API access secret key
+ *
+ * @var string
+ */
+ public $secret_key;
+
+ /**
+ * Api access publishable key
+ *
+ * @var string
+ */
+ public $publishable_key;
+
+ /**
+ * Should we store the users credit cards?
+ *
+ * @var bool
+ */
+ public $saved_cards;
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ $this->id = self::ID;
+ $this->method_title = __( 'Stripe iDEAL', 'woocommerce-gateway-stripe' );
+ $this->method_description = sprintf(
+ /* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+ __( 'All other general Stripe settings can be adjusted %1$shere%2$s.', 'woocommerce-gateway-stripe' ),
+ '<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=stripe' ) ) . '">',
+ '</a>'
+ );
+ $this->supports = [
+ 'products',
+ 'refunds',
+ ];
+
+ // Load the form fields.
+ $this->init_form_fields();
+
+ // Load the settings.
+ $this->init_settings();
+
+ $main_settings = get_option( 'woocommerce_stripe_settings' );
+ $this->title = $this->get_option( 'title' );
+ $this->description = $this->get_option( 'description' );
+ $this->enabled = $this->get_option( 'enabled' );
+ $this->testmode = ( ! empty( $main_settings['testmode'] ) && 'yes' === $main_settings['testmode'] ) ? true : false;
+ $this->saved_cards = ( ! empty( $main_settings['saved_cards'] ) && 'yes' === $main_settings['saved_cards'] ) ? true : false;
+ $this->publishable_key = ! empty( $main_settings['publishable_key'] ) ? $main_settings['publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['secret_key'] ) ? $main_settings['secret_key'] : '';
+ $this->statement_descriptor = ! empty( $main_settings['statement_descriptor'] ) ? $main_settings['statement_descriptor'] : '';
+
+ if ( $this->testmode ) {
+ $this->publishable_key = ! empty( $main_settings['test_publishable_key'] ) ? $main_settings['test_publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['test_secret_key'] ) ? $main_settings['test_secret_key'] : '';
+ }
+
+ add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, [ $this, 'process_admin_options' ] );
+ add_action( 'wp_enqueue_scripts', [ $this, 'payment_scripts' ] );
+ }
+
+ /**
+ * Returns all supported currencies for this payment method.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return array
+ */
+ public function get_supported_currency() {
+ return apply_filters(
+ 'wc_stripe_ideal_supported_currencies',
+ [
+ 'EUR',
+ ]
+ );
+ }
+
+ /**
+ * Checks to see if all criteria is met before showing payment method.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return bool
+ */
+ public function is_available() {
+ if ( ! in_array( get_woocommerce_currency(), $this->get_supported_currency() ) ) {
+ return false;
+ }
+
+ return parent::is_available();
+ }
+
+ /**
+ * Get_icon function.
+ *
+ * @since 1.0.0
+ * @version 4.0.0
+ * @return string
+ */
+ public function get_icon() {
+ $icons = $this->payment_icons();
+
+ $icons_str = '';
+
+ $icons_str .= isset( $icons['ideal'] ) ? $icons['ideal'] : '';
+
+ return apply_filters( 'woocommerce_gateway_icon', $icons_str, $this->id );
+ }
+
+ /**
+ * Outputs scripts used for stripe payment
+ */
+ public function payment_scripts() {
+ if ( ! is_cart() && ! is_checkout() && ! isset( $_GET['pay_for_order'] ) && ! is_add_payment_method_page() ) {
+ return;
+ }
+
+ wp_enqueue_style( 'stripe_styles' );
+ wp_enqueue_script( 'woocommerce_stripe' );
+ }
+
+ /**
+ * Initialize Gateway Settings Form Fields.
+ */
+ public function init_form_fields() {
+ $this->form_fields = require WC_STRIPE_PLUGIN_PATH . '/includes/admin/stripe-ideal-settings.php';
+ }
+
+ /**
+ * Payment form on checkout page
+ */
+ public function payment_fields() {
+ global $wp;
+ $user = wp_get_current_user();
+ $total = WC()->cart->total;
+ $description = $this->get_description();
+
+ // If paying from order, we need to get total from order not cart.
+ if ( isset( $_GET['pay_for_order'] ) && ! empty( $_GET['key'] ) ) {
+ $order = wc_get_order( wc_clean( $wp->query_vars['order-pay'] ) );
+ $total = $order->get_total();
+ }
+
+ if ( is_add_payment_method_page() ) {
+ $pay_button_text = __( 'Add Payment', 'woocommerce-gateway-stripe' );
+ $total = '';
+ } else {
+ $pay_button_text = '';
+ }
+
+ echo '<div
+ id="stripe-ideal-payment-data"
+ data-amount="' . esc_attr( WC_Stripe_Helper::get_stripe_amount( $total ) ) . '"
+ data-currency="' . esc_attr( strtolower( get_woocommerce_currency() ) ) . '">';
+
+ if ( $description ) {
+ echo apply_filters( 'wc_stripe_description', wpautop( wp_kses_post( $description ) ), $this->id );
+ }
+
+ echo '</div>';
+ }
+
+ /**
+ * Creates the source for charge.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @param object $order
+ * @return mixed
+ */
+ public function create_source( $order ) {
+ $currency = $order->get_currency();
+ $return_url = $this->get_stripe_return_url( $order );
+ $post_data = [];
+ $post_data['amount'] = WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $currency );
+ $post_data['currency'] = strtolower( $currency );
+ $post_data['type'] = 'ideal';
+ $post_data['owner'] = $this->get_owner_details( $order );
+ $post_data['redirect'] = [ 'return_url' => $return_url ];
+
+ if ( ! empty( $this->statement_descriptor ) ) {
+ $post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $this->statement_descriptor );
+ }
+
+ WC_Stripe_Logger::log( 'Info: Begin creating iDEAL source' );
+
+ return WC_Stripe_API::request( apply_filters( 'wc_stripe_ideal_source', $post_data, $order ), 'sources' );
+ }
+
+ /**
+ * Process the payment
+ *
+ * @param int $order_id Reference.
+ * @param bool $retry Should we retry on fail.
+ * @param bool $force_save_source Force payment source to be saved.
+ *
+ * @throws Exception If payment will not be accepted.
+ *
+ * @return array|void
+ */
+ public function process_payment( $order_id, $retry = true, $force_save_source = false ) {
+ try {
+ $order = wc_get_order( $order_id );
+
+ // This will throw exception if not valid.
+ $this->validate_minimum_order_amount( $order );
+
+ // This comes from the create account checkbox in the checkout page.
+ $create_account = ! empty( $_POST['createaccount'] ) ? true : false;
+
+ if ( $create_account ) {
+ $new_customer_id = $order->get_customer_id();
+ $new_stripe_customer = new WC_Stripe_Customer( $new_customer_id );
+ $new_stripe_customer->create_customer();
+ }
+
+ $response = $this->create_source( $order );
+
+ if ( ! empty( $response->error ) ) {
+ $order->add_order_note( $response->error->message );
+
+ throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
+ }
+
+ $order->update_meta_data( '_stripe_source_id', $response->id );
+ $order->save();
+
+ WC_Stripe_Logger::log( 'Info: Redirecting to iDEAL...' );
+
+ return [
+ 'result' => 'success',
+ 'redirect' => esc_url_raw( $response->redirect->url ),
+ ];
+ } catch ( WC_Stripe_Exception $e ) {
+ wc_add_notice( $e->getLocalizedMessage(), 'error' );
+ WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
+
+ do_action( 'wc_gateway_stripe_process_payment_error', $e, $order );
+
+ if ( $order->has_status(
+ apply_filters(
+ 'wc_stripe_allowed_payment_processing_statuses',
+ [ 'pending', 'failed' ],
+ $order
+ )
+ ) ) {
+ $this->send_failed_order_email( $order_id );
+ }
+
+ return [
+ 'result' => 'fail',
+ 'redirect' => '',
+ ];
+ }
+ }
+}
diff --git a/includes/payment-methods/class-wc-gateway-stripe-multibanco.php b/includes/payment-methods/class-wc-gateway-stripe-multibanco.php
new file mode 100644
index 0000000..a10402d
--- /dev/null
+++ b/includes/payment-methods/class-wc-gateway-stripe-multibanco.php
@@ -0,0 +1,403 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Class that handles Multibanco payment method.
+ *
+ * @extends WC_Gateway_Stripe
+ *
+ * @since 4.1.0
+ */
+class WC_Gateway_Stripe_Multibanco extends WC_Stripe_Payment_Gateway {
+ /**
+ * Notices (array)
+ *
+ * @var array
+ */
+ public $notices = [];
+
+ /**
+ * Is test mode active?
+ *
+ * @var bool
+ */
+ public $testmode;
+
+ /**
+ * Alternate credit card statement name
+ *
+ * @var bool
+ */
+ public $statement_descriptor;
+
+ /**
+ * API access secret key
+ *
+ * @var string
+ */
+ public $secret_key;
+
+ /**
+ * Api access publishable key
+ *
+ * @var string
+ */
+ public $publishable_key;
+
+ /**
+ * Should we store the users credit cards?
+ *
+ * @var bool
+ */
+ public $saved_cards;
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ $this->id = 'stripe_multibanco';
+ $this->method_title = __( 'Stripe Multibanco', 'woocommerce-gateway-stripe' );
+ $this->method_description = sprintf(
+ /* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+ __( 'All other general Stripe settings can be adjusted %1$shere%2$s.', 'woocommerce-gateway-stripe' ),
+ '<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=stripe' ) ) . '">',
+ '</a>'
+ );
+ $this->supports = [
+ 'products',
+ 'refunds',
+ ];
+
+ // Load the form fields.
+ $this->init_form_fields();
+
+ // Load the settings.
+ $this->init_settings();
+
+ $main_settings = get_option( 'woocommerce_stripe_settings' );
+ $this->title = $this->get_option( 'title' );
+ $this->description = $this->get_option( 'description' );
+ $this->enabled = $this->get_option( 'enabled' );
+ $this->testmode = ( ! empty( $main_settings['testmode'] ) && 'yes' === $main_settings['testmode'] ) ? true : false;
+ $this->saved_cards = ( ! empty( $main_settings['saved_cards'] ) && 'yes' === $main_settings['saved_cards'] ) ? true : false;
+ $this->publishable_key = ! empty( $main_settings['publishable_key'] ) ? $main_settings['publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['secret_key'] ) ? $main_settings['secret_key'] : '';
+ $this->statement_descriptor = ! empty( $main_settings['statement_descriptor'] ) ? $main_settings['statement_descriptor'] : '';
+
+ if ( $this->testmode ) {
+ $this->publishable_key = ! empty( $main_settings['test_publishable_key'] ) ? $main_settings['test_publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['test_secret_key'] ) ? $main_settings['test_secret_key'] : '';
+ }
+
+ add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, [ $this, 'process_admin_options' ] );
+ add_action( 'wp_enqueue_scripts', [ $this, 'payment_scripts' ] );
+ add_action( 'woocommerce_thankyou_stripe_multibanco', [ $this, 'thankyou_page' ] );
+
+ // Customer Emails
+ add_action( 'woocommerce_email_before_order_table', [ $this, 'email_instructions' ], 10, 3 );
+ }
+
+ /**
+ * Returns all supported currencies for this payment method.
+ *
+ * @since 4.1.0
+ * @version 4.1.0
+ * @return array
+ */
+ public function get_supported_currency() {
+ return apply_filters(
+ 'wc_stripe_multibanco_supported_currencies',
+ [
+ 'EUR',
+ ]
+ );
+ }
+
+ /**
+ * Checks to see if all criteria is met before showing payment method.
+ *
+ * @since 4.1.0
+ * @version 4.1.0
+ * @return bool
+ */
+ public function is_available() {
+ if ( ! in_array( get_woocommerce_currency(), $this->get_supported_currency() ) ) {
+ return false;
+ }
+
+ return parent::is_available();
+ }
+
+ /**
+ * Get_icon function.
+ *
+ * @since 1.0.0
+ * @version 4.1.0
+ * @return string
+ */
+ public function get_icon() {
+ $icons = $this->payment_icons();
+
+ $icons_str = '';
+
+ $icons_str .= isset( $icons['multibanco'] ) ? $icons['multibanco'] : '';
+
+ return apply_filters( 'woocommerce_gateway_icon', $icons_str, $this->id );
+ }
+
+ /**
+ * Outputs scripts used for stripe payment
+ */
+ public function payment_scripts() {
+ if ( ! is_cart() && ! is_checkout() && ! isset( $_GET['pay_for_order'] ) && ! is_add_payment_method_page() ) {
+ return;
+ }
+
+ wp_enqueue_style( 'stripe_styles' );
+ wp_enqueue_script( 'woocommerce_stripe' );
+ }
+
+ /**
+ * Initialize Gateway Settings Form Fields.
+ */
+ public function init_form_fields() {
+ $this->form_fields = require WC_STRIPE_PLUGIN_PATH . '/includes/admin/stripe-multibanco-settings.php';
+ }
+
+ /**
+ * Payment form on checkout page
+ */
+ public function payment_fields() {
+ global $wp;
+ $user = wp_get_current_user();
+ $total = WC()->cart->total;
+ $description = $this->get_description();
+
+ // If paying from order, we need to get total from order not cart.
+ if ( isset( $_GET['pay_for_order'] ) && ! empty( $_GET['key'] ) ) {
+ $order = wc_get_order( wc_clean( $wp->query_vars['order-pay'] ) );
+ $total = $order->get_total();
+ }
+
+ if ( is_add_payment_method_page() ) {
+ $pay_button_text = __( 'Add Payment', 'woocommerce-gateway-stripe' );
+ $total = '';
+ } else {
+ $pay_button_text = '';
+ }
+
+ echo '<div
+ id="stripe-multibanco-payment-data"
+ data-amount="' . esc_attr( WC_Stripe_Helper::get_stripe_amount( $total ) ) . '"
+ data-currency="' . esc_attr( strtolower( get_woocommerce_currency() ) ) . '">';
+
+ if ( $description ) {
+ echo apply_filters( 'wc_stripe_description', wpautop( wp_kses_post( $description ) ), $this->id );
+ }
+
+ echo '</div>';
+ }
+
+ /**
+ * Output for the order received page.
+ *
+ * @param int $order_id
+ */
+ public function thankyou_page( $order_id ) {
+ $this->get_instructions( $order_id );
+ }
+
+ /**
+ * Add content to the WC emails.
+ *
+ * @since 4.1.0
+ * @version 4.1.0
+ * @param WC_Order $order
+ * @param bool $sent_to_admin
+ * @param bool $plain_text
+ */
+ public function email_instructions( $order, $sent_to_admin, $plain_text = false ) {
+ $order_id = $order->get_id();
+
+ $payment_method = $order->get_payment_method();
+
+ if ( ! $sent_to_admin && 'stripe_multibanco' === $payment_method && $order->has_status( 'on-hold' ) ) {
+ WC_Stripe_Logger::log( 'Sending multibanco email for order #' . $order_id );
+
+ $this->get_instructions( $order, $plain_text );
+ }
+ }
+
+ /**
+ * Gets the Multibanco instructions for customer to pay.
+ *
+ * @since 4.1.0
+ * @version 4.1.0
+ * @param int|WC_Order $order
+ */
+ public function get_instructions( $order, $plain_text = false ) {
+ if ( true === is_int( $order ) ) {
+ $order = wc_get_order( $order );
+ }
+
+ $data = $order->get_meta( '_stripe_multibanco' );
+
+ if ( $plain_text ) {
+ esc_html_e( 'MULTIBANCO INFORMAÇÕES DE ENCOMENDA:', 'woocommerce-gateway-stripe' ) . "\n\n";
+ echo "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n";
+ esc_html_e( 'Montante:', 'woocommerce-gateway-stripe' ) . "\n\n";
+ echo $data['amount'] . "\n\n";
+ echo "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n";
+ esc_html_e( 'Entidade:', 'woocommerce-gateway-stripe' ) . "\n\n";
+ echo $data['entity'] . "\n\n";
+ echo "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n";
+ esc_html_e( 'Referencia:', 'woocommerce-gateway-stripe' ) . "\n\n";
+ echo $data['reference'] . "\n\n";
+ } else {
+ ?>
+ <h3><?php esc_html_e( 'MULTIBANCO INFORMAÇÕES DE ENCOMENDA:', 'woocommerce-gateway-stripe' ); ?></h3>
+ <ul class="woocommerce-order-overview woocommerce-thankyou-order-details order_details">
+ <li class="woocommerce-order-overview__order order">
+ <?php esc_html_e( 'Montante:', 'woocommerce-gateway-stripe' ); ?>
+ <strong><?php echo $data['amount']; ?></strong>
+ </li>
+ <li class="woocommerce-order-overview__order order">
+ <?php esc_html_e( 'Entidade:', 'woocommerce-gateway-stripe' ); ?>
+ <strong><?php echo $data['entity']; ?></strong>
+ </li>
+ <li class="woocommerce-order-overview__order order">
+ <?php esc_html_e( 'Referencia:', 'woocommerce-gateway-stripe' ); ?>
+ <strong><?php echo $data['reference']; ?></strong>
+ </li>
+ </ul>
+ <?php
+ }
+ }
+
+ /**
+ * Saves Multibanco information to the order meta for later use.
+ *
+ * @since 4.1.0
+ * @version 4.1.0
+ * @param object $order
+ * @param object $source_object
+ */
+ public function save_instructions( $order, $source_object ) {
+ $data = [
+ 'amount' => $order->get_formatted_order_total(),
+ 'entity' => $source_object->multibanco->entity,
+ 'reference' => $source_object->multibanco->reference,
+ ];
+
+ $order_id = $order->get_id();
+
+ $order->update_meta_data( '_stripe_multibanco', $data );
+ }
+
+ /**
+ * Creates the source for charge.
+ *
+ * @since 4.1.0
+ * @version 4.1.0
+ * @param object $order
+ * @return mixed
+ */
+ public function create_source( $order ) {
+ $currency = $order->get_currency();
+ $return_url = $this->get_stripe_return_url( $order );
+ $post_data = [];
+ $post_data['amount'] = WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $currency );
+ $post_data['currency'] = strtolower( $currency );
+ $post_data['type'] = 'multibanco';
+ $post_data['owner'] = $this->get_owner_details( $order );
+ $post_data['redirect'] = [ 'return_url' => $return_url ];
+
+ if ( ! empty( $this->statement_descriptor ) ) {
+ $post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $this->statement_descriptor );
+ }
+
+ WC_Stripe_Logger::log( 'Info: Begin creating Multibanco source' );
+
+ return WC_Stripe_API::request( $post_data, 'sources' );
+ }
+
+ /**
+ * Process the payment
+ *
+ * @param int $order_id Reference.
+ * @param bool $retry Should we retry on fail.
+ * @param bool $force_save_source Force payment source to be saved.
+ *
+ * @throws Exception If payment will not be accepted.
+ *
+ * @return array|void
+ */
+ public function process_payment( $order_id, $retry = true, $force_save_source = false ) {
+ try {
+ $order = wc_get_order( $order_id );
+
+ // This will throw exception if not valid.
+ $this->validate_minimum_order_amount( $order );
+
+ // This comes from the create account checkbox in the checkout page.
+ $create_account = ! empty( $_POST['createaccount'] ) ? true : false;
+
+ if ( $create_account ) {
+ $new_customer_id = $order->get_customer_id();
+ $new_stripe_customer = new WC_Stripe_Customer( $new_customer_id );
+ $new_stripe_customer->create_customer();
+ }
+
+ $response = $this->create_source( $order );
+
+ if ( ! empty( $response->error ) ) {
+ $order->add_order_note( $response->error->message );
+
+ throw new Exception( $response->error->message );
+ }
+
+ $order->update_meta_data( '_stripe_source_id', $response->id );
+ $order->save();
+
+ $this->save_instructions( $order, $response );
+
+ // Mark as on-hold (we're awaiting the payment)
+ $order->update_status( 'on-hold', __( 'Awaiting Multibanco payment', 'woocommerce-gateway-stripe' ) );
+
+ // Reduce stock levels
+ wc_reduce_stock_levels( $order_id );
+
+ // Remove cart
+ WC()->cart->empty_cart();
+
+ WC_Stripe_Logger::log( 'Info: Redirecting to Multibanco...' );
+
+ return [
+ 'result' => 'success',
+ 'redirect' => esc_url_raw( $response->redirect->url ),
+ ];
+ } catch ( Exception $e ) {
+ wc_add_notice( $e->getMessage(), 'error' );
+ WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
+
+ do_action( 'wc_gateway_stripe_process_payment_error', $e, $order );
+
+ if ( $order->has_status(
+ apply_filters(
+ 'wc_stripe_allowed_payment_processing_statuses',
+ [ 'pending', 'failed' ],
+ $order
+ )
+ ) ) {
+ $this->send_failed_order_email( $order_id );
+ }
+
+ return [
+ 'result' => 'fail',
+ 'redirect' => '',
+ ];
+ }
+ }
+}
diff --git a/includes/payment-methods/class-wc-gateway-stripe-oxxo.php b/includes/payment-methods/class-wc-gateway-stripe-oxxo.php
new file mode 100644
index 0000000..1c8bc2b
--- /dev/null
+++ b/includes/payment-methods/class-wc-gateway-stripe-oxxo.php
@@ -0,0 +1,127 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Class that handles OXXO payment method.
+ *
+ * @extends WC_Gateway_Stripe
+ *
+ * @since 5.8.0
+ */
+class WC_Gateway_Stripe_Oxxo extends WC_Stripe_Payment_Gateway_Voucher {
+
+ /**
+ * ID used by UPE
+ *
+ * @var string
+ */
+ const ID = 'stripe_boleto';
+
+ /**
+ * ID used by WooCommerce to identify the payment method
+ *
+ * @var string
+ */
+ public $id = 'stripe_oxxo';
+
+ /**
+ * ID used by stripe
+ */
+ protected $stripe_id = 'oxxo';
+
+ /**
+ * List of accepted currencies
+ *
+ * @var array
+ */
+ protected $supported_currencies = [ 'MXN' ];
+
+ /**
+ * List of accepted countries
+ */
+ protected $supported_countries = [ 'MX' ];
+
+ /**
+ * Constructor
+ *
+ * @since 5.8.0
+ */
+ public function __construct() {
+ $this->method_title = __( 'Stripe OXXO', 'woocommerce-gateway-stripe' );
+ parent::__construct();
+
+ add_filter( 'wc_stripe_allowed_payment_processing_statuses', [ $this, 'add_allowed_payment_processing_statuses' ], 10, 2 );
+ }
+
+ /**
+ * Adds on-hold as accepted status during webhook handling on orders paid with voucher
+ *
+ * @param $allowed_statuses
+ * @param $order
+ *
+ * @return mixed
+ */
+ public function add_allowed_payment_processing_statuses( $allowed_statuses, $order ) {
+ if ( $this->stripe_id === $order->get_meta( '_stripe_upe_payment_type' ) && ! in_array( 'on-hold', $allowed_statuses ) ) {
+ $allowed_statuses[] = 'on-hold';
+ }
+
+ return $allowed_statuses;
+ }
+
+ /**
+ * Payment form on checkout page
+ *
+ * @since 5.8.0
+ */
+ public function payment_fields() {
+ $description = $this->get_description();
+ apply_filters( 'wc_stripe_description', wpautop( wp_kses_post( $description ) ), $this->id )
+ ?>
+ <div class="stripe-source-errors" role="alert"></div>
+
+ <div id="stripe-boleto-payment-data"><?php echo $description; ?></div>
+ <?php
+ }
+
+ /**
+ * Validates the minimum and maximum amount. Throws exception when out of range value is added
+ *
+ * @since 5.8.0
+ *
+ * @param $amount
+ *
+ * @throws WC_Stripe_Exception
+ */
+ protected function validate_amount_limits( $amount ) {
+
+ if ( $amount < 10.00 ) {
+ /* translators: 1) amount (including currency symbol) */
+ throw new WC_Stripe_Exception( sprintf( __( 'Sorry, the minimum allowed order total is %1$s to use this payment method.', 'woocommerce-gateway-stripe' ), wc_price( 10.0 ) ) );
+ } elseif ( $amount > 10000.00 ) {
+ /* translators: 1) amount (including currency symbol) */
+ throw new WC_Stripe_Exception( sprintf( __( 'Sorry, the maximum allowed order total is %1$s to use this payment method.', 'woocommerce-gateway-stripe' ), wc_price( 10000.00 ) ) );
+ }
+ }
+
+ /**
+ * Gather the data necessary to confirm the payment via javascript
+ * Override this when extending the class
+ *
+ * @param WC_Order $order
+ *
+ * @return array
+ */
+ protected function get_confirm_payment_data( $order ) {
+ return [
+ 'payment_method' => [
+ 'billing_details' => [
+ 'name' => $order->get_formatted_billing_full_name(),
+ 'email' => $order->get_billing_email(),
+ ],
+ ],
+ ];
+ }
+}
diff --git a/includes/payment-methods/class-wc-gateway-stripe-p24.php b/includes/payment-methods/class-wc-gateway-stripe-p24.php
new file mode 100644
index 0000000..c673f5f
--- /dev/null
+++ b/includes/payment-methods/class-wc-gateway-stripe-p24.php
@@ -0,0 +1,292 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Class that handles P24 payment method.
+ *
+ * @extends WC_Gateway_Stripe
+ *
+ * @since 4.0.0
+ */
+class WC_Gateway_Stripe_P24 extends WC_Stripe_Payment_Gateway {
+
+ const ID = 'stripe_p24';
+
+ /**
+ * Notices (array)
+ *
+ * @var array
+ */
+ public $notices = [];
+
+ /**
+ * Is test mode active?
+ *
+ * @var bool
+ */
+ public $testmode;
+
+ /**
+ * Alternate credit card statement name
+ *
+ * @var bool
+ */
+ public $statement_descriptor;
+
+ /**
+ * API access secret key
+ *
+ * @var string
+ */
+ public $secret_key;
+
+ /**
+ * Api access publishable key
+ *
+ * @var string
+ */
+ public $publishable_key;
+
+ /**
+ * Should we store the users credit cards?
+ *
+ * @var bool
+ */
+ public $saved_cards;
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ $this->id = 'stripe_p24';
+ $this->method_title = __( 'Stripe P24', 'woocommerce-gateway-stripe' );
+ $this->method_description = sprintf(
+ /* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+ __( 'All other general Stripe settings can be adjusted %1$shere%2$s.', 'woocommerce-gateway-stripe' ),
+ '<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=stripe' ) ) . '">',
+ '</a>'
+ );
+ $this->supports = [
+ 'products',
+ 'refunds',
+ ];
+
+ // Load the form fields.
+ $this->init_form_fields();
+
+ // Load the settings.
+ $this->init_settings();
+
+ $main_settings = get_option( 'woocommerce_stripe_settings' );
+ $this->title = $this->get_option( 'title' );
+ $this->description = $this->get_option( 'description' );
+ $this->enabled = $this->get_option( 'enabled' );
+ $this->testmode = ( ! empty( $main_settings['testmode'] ) && 'yes' === $main_settings['testmode'] ) ? true : false;
+ $this->saved_cards = ( ! empty( $main_settings['saved_cards'] ) && 'yes' === $main_settings['saved_cards'] ) ? true : false;
+ $this->publishable_key = ! empty( $main_settings['publishable_key'] ) ? $main_settings['publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['secret_key'] ) ? $main_settings['secret_key'] : '';
+ $this->statement_descriptor = ! empty( $main_settings['statement_descriptor'] ) ? $main_settings['statement_descriptor'] : '';
+
+ if ( $this->testmode ) {
+ $this->publishable_key = ! empty( $main_settings['test_publishable_key'] ) ? $main_settings['test_publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['test_secret_key'] ) ? $main_settings['test_secret_key'] : '';
+ }
+
+ add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, [ $this, 'process_admin_options' ] );
+ add_action( 'wp_enqueue_scripts', [ $this, 'payment_scripts' ] );
+ }
+
+ /**
+ * Returns all supported currencies for this payment method.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return array
+ */
+ public function get_supported_currency() {
+ return apply_filters(
+ 'wc_stripe_p24_supported_currencies',
+ [
+ 'EUR',
+ 'PLN',
+ ]
+ );
+ }
+
+ /**
+ * Checks to see if all criteria is met before showing payment method.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return bool
+ */
+ public function is_available() {
+ if ( ! in_array( get_woocommerce_currency(), $this->get_supported_currency() ) ) {
+ return false;
+ }
+
+ return parent::is_available();
+ }
+
+ /**
+ * Get_icon function.
+ *
+ * @since 1.0.0
+ * @version 4.0.0
+ * @return string
+ */
+ public function get_icon() {
+ $icons = $this->payment_icons();
+
+ $icons_str = '';
+
+ $icons_str .= isset( $icons['p24'] ) ? $icons['p24'] : '';
+
+ return apply_filters( 'woocommerce_gateway_icon', $icons_str, $this->id );
+ }
+
+ /**
+ * Outputs scripts used for stripe payment
+ */
+ public function payment_scripts() {
+ if ( ! is_cart() && ! is_checkout() && ! isset( $_GET['pay_for_order'] ) && ! is_add_payment_method_page() ) {
+ return;
+ }
+
+ wp_enqueue_style( 'stripe_styles' );
+ wp_enqueue_script( 'woocommerce_stripe' );
+ }
+
+ /**
+ * Initialize Gateway Settings Form Fields.
+ */
+ public function init_form_fields() {
+ $this->form_fields = require WC_STRIPE_PLUGIN_PATH . '/includes/admin/stripe-p24-settings.php';
+ }
+
+ /**
+ * Payment form on checkout page
+ */
+ public function payment_fields() {
+ global $wp;
+ $user = wp_get_current_user();
+ $total = WC()->cart->total;
+ $description = $this->get_description();
+
+ // If paying from order, we need to get total from order not cart.
+ if ( isset( $_GET['pay_for_order'] ) && ! empty( $_GET['key'] ) ) {
+ $order = wc_get_order( wc_clean( $wp->query_vars['order-pay'] ) );
+ $total = $order->get_total();
+ }
+
+ if ( is_add_payment_method_page() ) {
+ $pay_button_text = __( 'Add Payment', 'woocommerce-gateway-stripe' );
+ $total = '';
+ } else {
+ $pay_button_text = '';
+ }
+
+ echo '<div
+ id="stripe-p24-payment-data"
+ data-amount="' . esc_attr( WC_Stripe_Helper::get_stripe_amount( $total ) ) . '"
+ data-currency="' . esc_attr( strtolower( get_woocommerce_currency() ) ) . '">';
+
+ if ( $description ) {
+ echo apply_filters( 'wc_stripe_description', wpautop( wp_kses_post( $description ) ), $this->id );
+ }
+
+ echo '</div>';
+ }
+
+ /**
+ * Creates the source for charge.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @param object $order
+ * @return mixed
+ */
+ public function create_source( $order ) {
+ $currency = $order->get_currency();
+ $return_url = $this->get_stripe_return_url( $order );
+ $post_data = [];
+ $post_data['amount'] = WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $currency );
+ $post_data['currency'] = strtolower( $currency );
+ $post_data['type'] = 'p24';
+ $post_data['owner'] = $this->get_owner_details( $order );
+ $post_data['redirect'] = [ 'return_url' => $return_url ];
+
+ WC_Stripe_Logger::log( 'Info: Begin creating P24 source' );
+
+ return WC_Stripe_API::request( apply_filters( 'wc_stripe_p24_source', $post_data, $order ), 'sources' );
+ }
+
+ /**
+ * Process the payment
+ *
+ * @param int $order_id Reference.
+ * @param bool $retry Should we retry on fail.
+ * @param bool $force_save_source Force payment source to be saved.
+ *
+ * @throws Exception If payment will not be accepted.
+ *
+ * @return array|void
+ */
+ public function process_payment( $order_id, $retry = true, $force_save_source = false ) {
+ try {
+ $order = wc_get_order( $order_id );
+
+ // This will throw exception if not valid.
+ $this->validate_minimum_order_amount( $order );
+
+ // This comes from the create account checkbox in the checkout page.
+ $create_account = ! empty( $_POST['createaccount'] ) ? true : false;
+
+ if ( $create_account ) {
+ $new_customer_id = $order->get_customer_id();
+ $new_stripe_customer = new WC_Stripe_Customer( $new_customer_id );
+ $new_stripe_customer->create_customer();
+ }
+
+ $response = $this->create_source( $order );
+
+ if ( ! empty( $response->error ) ) {
+ $order->add_order_note( $response->error->message );
+
+ throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
+ }
+
+ $order->update_meta_data( '_stripe_source_id', $response->id );
+ $order->save();
+
+ WC_Stripe_Logger::log( 'Info: Redirecting to P24...' );
+
+ return [
+ 'result' => 'success',
+ 'redirect' => esc_url_raw( $response->redirect->url ),
+ ];
+ } catch ( WC_Stripe_Exception $e ) {
+ wc_add_notice( $e->getLocalizedMessage(), 'error' );
+ WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
+
+ do_action( 'wc_gateway_stripe_process_payment_error', $e, $order );
+
+ if ( $order->has_status(
+ apply_filters(
+ 'wc_stripe_allowed_payment_processing_statuses',
+ [ 'pending', 'failed' ],
+ $order
+ )
+ ) ) {
+ $this->send_failed_order_email( $order_id );
+ }
+
+ return [
+ 'result' => 'fail',
+ 'redirect' => '',
+ ];
+ }
+ }
+}
diff --git a/includes/payment-methods/class-wc-gateway-stripe-sepa.php b/includes/payment-methods/class-wc-gateway-stripe-sepa.php
new file mode 100644
index 0000000..75ecbe0
--- /dev/null
+++ b/includes/payment-methods/class-wc-gateway-stripe-sepa.php
@@ -0,0 +1,413 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Class that handles SEPA payment method.
+ *
+ * @extends WC_Gateway_Stripe
+ *
+ * @since 4.0.0
+ */
+class WC_Gateway_Stripe_Sepa extends WC_Stripe_Payment_Gateway {
+
+ const ID = 'stripe_sepa';
+
+ /**
+ * Notices (array)
+ *
+ * @var array
+ */
+ public $notices = [];
+
+ /**
+ * Is test mode active?
+ *
+ * @var bool
+ */
+ public $testmode;
+
+ /**
+ * Alternate credit card statement name
+ *
+ * @var bool
+ */
+ public $statement_descriptor;
+
+ /**
+ * API access secret key
+ *
+ * @var string
+ */
+ public $secret_key;
+
+ /**
+ * Api access publishable key
+ *
+ * @var string
+ */
+ public $publishable_key;
+
+ /**
+ * Should we store the users credit cards?
+ *
+ * @var bool
+ */
+ public $saved_cards;
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ $this->id = self::ID;
+ $this->method_title = __( 'Stripe SEPA Direct Debit', 'woocommerce-gateway-stripe' );
+ $this->method_description = sprintf(
+ /* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+ __( 'All other general Stripe settings can be adjusted %1$shere%2$s.', 'woocommerce-gateway-stripe' ),
+ '<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=stripe' ) ) . '">',
+ '</a>'
+ );
+ $this->has_fields = true;
+ $this->supports = [
+ 'products',
+ 'refunds',
+ 'tokenization',
+ 'add_payment_method',
+ ];
+
+ // Load the form fields.
+ $this->init_form_fields();
+
+ // Load the settings.
+ $this->init_settings();
+
+ // Check if subscriptions are enabled and add support for them.
+ $this->maybe_init_subscriptions();
+
+ // Check if pre-orders are enabled and add support for them.
+ $this->maybe_init_pre_orders();
+
+ $main_settings = get_option( 'woocommerce_stripe_settings' );
+ $this->title = $this->get_option( 'title' );
+ $this->description = $this->get_option( 'description' );
+ $this->enabled = $this->get_option( 'enabled' );
+ $this->testmode = ( ! empty( $main_settings['testmode'] ) && 'yes' === $main_settings['testmode'] ) ? true : false;
+ $this->saved_cards = ( ! empty( $main_settings['saved_cards'] ) && 'yes' === $main_settings['saved_cards'] ) ? true : false;
+ $this->publishable_key = ! empty( $main_settings['publishable_key'] ) ? $main_settings['publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['secret_key'] ) ? $main_settings['secret_key'] : '';
+ $this->statement_descriptor = ! empty( $main_settings['statement_descriptor'] ) ? $main_settings['statement_descriptor'] : '';
+
+ if ( $this->testmode ) {
+ $this->publishable_key = ! empty( $main_settings['test_publishable_key'] ) ? $main_settings['test_publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['test_secret_key'] ) ? $main_settings['test_secret_key'] : '';
+ }
+
+ add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, [ $this, 'process_admin_options' ] );
+ add_action( 'wp_enqueue_scripts', [ $this, 'payment_scripts' ] );
+ }
+
+ /**
+ * Returns all supported currencies for this payment method.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return array
+ */
+ public function get_supported_currency() {
+ return apply_filters(
+ 'wc_stripe_sepa_supported_currencies',
+ [
+ 'EUR',
+ ]
+ );
+ }
+
+ /**
+ * Checks to see if all criteria is met before showing payment method.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return bool
+ */
+ public function is_available() {
+ if ( ! in_array( get_woocommerce_currency(), $this->get_supported_currency() ) ) {
+ return false;
+ }
+
+ if ( is_add_payment_method_page() && ! $this->saved_cards ) {
+ return false;
+ }
+
+ return parent::is_available();
+ }
+
+ /**
+ * Get_icon function.
+ *
+ * @since 1.0.0
+ * @version 4.0.0
+ * @return string
+ */
+ public function get_icon() {
+ $icons = $this->payment_icons();
+
+ $icons_str = '';
+
+ $icons_str .= isset( $icons['sepa'] ) ? $icons['sepa'] : '';
+
+ return apply_filters( 'woocommerce_gateway_icon', $icons_str, $this->id );
+ }
+
+ /**
+ * Outputs scripts used for stripe payment
+ */
+ public function payment_scripts() {
+ if ( ! is_cart() && ! is_checkout() && ! isset( $_GET['pay_for_order'] ) && ! is_add_payment_method_page() ) {
+ return;
+ }
+
+ wp_enqueue_style( 'stripe_styles' );
+ wp_enqueue_script( 'woocommerce_stripe' );
+ }
+
+ /**
+ * Initialize Gateway Settings Form Fields.
+ */
+ public function init_form_fields() {
+ $this->form_fields = require WC_STRIPE_PLUGIN_PATH . '/includes/admin/stripe-sepa-settings.php';
+ }
+
+ /**
+ * Displays the mandate acceptance notice to customer.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return string
+ */
+ public function mandate_display() {
+ /* translators: statement descriptor */
+ printf( __( 'By providing your IBAN and confirming this payment, you are authorizing %s and Stripe, our payment service provider, to send instructions to your bank to debit your account and your bank to debit your account in accordance with those instructions. You are entitled to a refund from your bank under the terms and conditions of your agreement with your bank. A refund must be claimed within 8 weeks starting from the date on which your account was debited.', 'woocommerce-gateway-stripe' ), WC_Stripe_Helper::clean_statement_descriptor( $this->statement_descriptor ) );
+ }
+
+ /**
+ * Renders the Stripe elements form.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ */
+ public function form() {
+ ?>
+ <fieldset id="wc-<?php echo esc_attr( $this->id ); ?>-form" class="wc-payment-form">
+ <?php do_action( 'woocommerce_credit_card_form_start', $this->id ); ?>
+ <p class="wc-stripe-sepa-mandate" style="margin-bottom:40px;"><?php $this->mandate_display(); ?></p>
+ <p class="form-row form-row-wide">
+ <label for="stripe-iban-element">
+ <?php esc_html_e( 'IBAN.', 'woocommerce-gateway-stripe' ); ?> <span class="required">*</span>
+ </label>
+ <div id="stripe-iban-element" class="wc-stripe-iban-element-field">
+ <!-- A Stripe Element will be inserted here. -->
+ </div>
+ </p>
+
+ <!-- Used to display form errors -->
+ <div class="stripe-source-errors" role="alert"></div>
+ <br />
+ <?php do_action( 'woocommerce_credit_card_form_end', $this->id ); ?>
+ <div class="clear"></div>
+ </fieldset>
+ <?php
+ }
+
+ /**
+ * Payment form on checkout page
+ */
+ public function payment_fields() {
+ global $wp;
+ $total = WC()->cart->total;
+ $display_tokenization = $this->supports( 'tokenization' ) && is_checkout() && $this->saved_cards;
+ $description = $this->get_description();
+ $description = ! empty( $description ) ? $description : '';
+
+ // If paying from order, we need to get total from order not cart.
+ if ( isset( $_GET['pay_for_order'] ) && ! empty( $_GET['key'] ) ) {
+ $order = wc_get_order( wc_clean( $wp->query_vars['order-pay'] ) );
+ $total = $order->get_total();
+ }
+
+ if ( is_add_payment_method_page() ) {
+ $total = '';
+ }
+
+ echo '<div
+ id="stripe-sepa_debit-payment-data"
+ data-amount="' . esc_attr( WC_Stripe_Helper::get_stripe_amount( $total ) ) . '"
+ data-currency="' . esc_attr( strtolower( get_woocommerce_currency() ) ) . '">';
+
+ if ( $this->testmode ) {
+ $description .= ' ' . __( 'TEST MODE ENABLED. In test mode, you can use IBAN number DE89370400440532013000.', 'woocommerce-gateway-stripe' );
+ }
+
+ $description = trim( $description );
+
+ echo apply_filters( 'wc_stripe_description', wpautop( wp_kses_post( $description ) ), $this->id );
+
+ if ( $display_tokenization ) {
+ $this->tokenization_script();
+ $this->saved_payment_methods();
+ }
+
+ $this->form();
+
+ if ( apply_filters( 'wc_stripe_display_save_payment_method_checkbox', $display_tokenization ) && ! is_add_payment_method_page() && ! isset( $_GET['change_payment_method'] ) ) {
+ $this->save_payment_method_checkbox();
+ }
+
+ do_action( 'wc_stripe_payment_fields_stripe_sepa', $this->id );
+
+ echo '</div>';
+ }
+
+ /**
+ * Process the payment
+ *
+ * @param int $order_id Reference.
+ * @param bool $retry Should we retry on fail.
+ * @param bool $force_save_source Force save the payment source.
+ *
+ * @throws Exception If payment will not be accepted.
+ *
+ * @return array|void
+ */
+ public function process_payment( $order_id, $retry = true, $force_save_source = false ) {
+ try {
+ $order = wc_get_order( $order_id );
+
+ if ( $this->has_subscription( $order_id ) ) {
+ $force_save_source = true;
+ }
+
+ if ( $this->maybe_change_subscription_payment_method( $order_id ) ) {
+ return $this->process_change_subscription_payment_method( $order_id );
+ }
+
+ if ( $this->maybe_process_pre_orders( $order_id ) ) {
+ return $this->process_pre_order( $order_id );
+ }
+
+ // This comes from the create account checkbox in the checkout page.
+ $create_account = ! empty( $_POST['createaccount'] ) ? true : false;
+
+ if ( $create_account ) {
+ $new_customer_id = $order->get_customer_id();
+ $new_stripe_customer = new WC_Stripe_Customer( $new_customer_id );
+ $new_stripe_customer->create_customer();
+ }
+
+ $prepared_source = $this->prepare_source( get_current_user_id(), $force_save_source );
+
+ $this->save_source_to_order( $order, $prepared_source );
+
+ // Result from Stripe API request.
+ $response = null;
+
+ if ( $order->get_total() > 0 ) {
+ // This will throw exception if not valid.
+ $this->validate_minimum_order_amount( $order );
+
+ WC_Stripe_Logger::log( "Info: Begin processing payment for order $order_id for the amount of {$order->get_total()}" );
+
+ // Make the request.
+ $response = WC_Stripe_API::request( $this->generate_payment_request( $order, $prepared_source ) );
+
+ if ( ! empty( $response->error ) ) {
+ // Customer param wrong? The user may have been deleted on stripe's end. Remove customer_id. Can be retried without.
+ if ( $this->is_no_such_customer_error( $response->error ) ) {
+ delete_user_option( $order->get_customer_id(), '_stripe_customer_id' );
+ $order->delete_meta_data( '_stripe_customer_id' );
+ $order->save();
+ }
+
+ if ( $this->is_no_such_token_error( $response->error ) && $prepared_source->token_id ) {
+ // Source param wrong? The CARD may have been deleted on stripe's end. Remove token and show message.
+ $wc_token = WC_Payment_Tokens::get( $prepared_source->token_id );
+ $wc_token->delete();
+ $localized_message = __( 'This card is no longer available and has been removed.', 'woocommerce-gateway-stripe' );
+ $order->add_order_note( $localized_message );
+ throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
+ }
+
+ // We want to retry.
+ if ( $this->is_retryable_error( $response->error ) ) {
+ if ( $retry ) {
+ // Don't do anymore retries after this.
+ if ( 5 <= $this->retry_interval ) {
+
+ return $this->process_payment( $order_id, false, $force_save_source );
+ }
+
+ sleep( $this->retry_interval );
+
+ $this->retry_interval++;
+
+ return $this->process_payment( $order_id, true, $force_save_source );
+ } else {
+ $localized_message = __( 'Sorry, we are unable to process your payment at this time. Please retry later.', 'woocommerce-gateway-stripe' );
+ $order->add_order_note( $localized_message );
+ throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
+ }
+ }
+
+ $localized_messages = WC_Stripe_Helper::get_localized_messages();
+
+ if ( 'card_error' === $response->error->type ) {
+ $localized_message = isset( $localized_messages[ $response->error->code ] ) ? $localized_messages[ $response->error->code ] : $response->error->message;
+ } else {
+ $localized_message = isset( $localized_messages[ $response->error->type ] ) ? $localized_messages[ $response->error->type ] : $response->error->message;
+ }
+
+ $order->add_order_note( $localized_message );
+
+ throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
+ }
+
+ do_action( 'wc_gateway_stripe_process_payment', $response, $order );
+
+ // Process valid response.
+ $this->process_response( $response, $order );
+ } else {
+ $order->payment_complete();
+ }
+
+ // Remove cart.
+ WC()->cart->empty_cart();
+
+ // Return thank you page redirect.
+ return [
+ 'result' => 'success',
+ 'redirect' => $this->get_return_url( $order ),
+ ];
+
+ } catch ( WC_Stripe_Exception $e ) {
+ wc_add_notice( $e->getLocalizedMessage(), 'error' );
+ WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
+
+ do_action( 'wc_gateway_stripe_process_payment_error', $e, $order );
+
+ if ( $order->has_status(
+ apply_filters(
+ 'wc_stripe_allowed_payment_processing_statuses',
+ [ 'pending', 'failed' ],
+ $order
+ )
+ ) ) {
+ $this->send_failed_order_email( $order_id );
+ }
+
+ return [
+ 'result' => 'fail',
+ 'redirect' => '',
+ ];
+ }
+ }
+}
diff --git a/includes/payment-methods/class-wc-gateway-stripe-sofort.php b/includes/payment-methods/class-wc-gateway-stripe-sofort.php
new file mode 100644
index 0000000..06744fd
--- /dev/null
+++ b/includes/payment-methods/class-wc-gateway-stripe-sofort.php
@@ -0,0 +1,308 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Class that handles Sofort payment method.
+ *
+ * @extends WC_Gateway_Stripe
+ *
+ * @since 4.0.0
+ */
+class WC_Gateway_Stripe_Sofort extends WC_Stripe_Payment_Gateway {
+
+ const ID = 'stripe_sofort';
+
+ /**
+ * Notices (array)
+ *
+ * @var array
+ */
+ public $notices = [];
+
+ /**
+ * Is test mode active?
+ *
+ * @var bool
+ */
+ public $testmode;
+
+ /**
+ * Alternate credit card statement name
+ *
+ * @var bool
+ */
+ public $statement_descriptor;
+
+ /**
+ * API access secret key
+ *
+ * @var string
+ */
+ public $secret_key;
+
+ /**
+ * Api access publishable key
+ *
+ * @var string
+ */
+ public $publishable_key;
+
+ /**
+ * Should we store the users credit cards?
+ *
+ * @var bool
+ */
+ public $saved_cards;
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ $this->id = 'stripe_sofort';
+ $this->method_title = __( 'Stripe Sofort', 'woocommerce-gateway-stripe' );
+ $this->method_description = sprintf(
+ /* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+ __( 'All other general Stripe settings can be adjusted %1$shere%2$s.', 'woocommerce-gateway-stripe' ),
+ '<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=stripe' ) ) . '">',
+ '</a>'
+ );
+ $this->supports = [
+ 'products',
+ 'refunds',
+ ];
+
+ // Load the form fields.
+ $this->init_form_fields();
+
+ // Load the settings.
+ $this->init_settings();
+
+ $main_settings = get_option( 'woocommerce_stripe_settings' );
+ $this->title = $this->get_option( 'title' );
+ $this->description = $this->get_option( 'description' );
+ $this->enabled = $this->get_option( 'enabled' );
+ $this->testmode = ( ! empty( $main_settings['testmode'] ) && 'yes' === $main_settings['testmode'] ) ? true : false;
+ $this->saved_cards = ( ! empty( $main_settings['saved_cards'] ) && 'yes' === $main_settings['saved_cards'] ) ? true : false;
+ $this->publishable_key = ! empty( $main_settings['publishable_key'] ) ? $main_settings['publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['secret_key'] ) ? $main_settings['secret_key'] : '';
+ $this->statement_descriptor = ! empty( $main_settings['statement_descriptor'] ) ? $main_settings['statement_descriptor'] : '';
+
+ if ( $this->testmode ) {
+ $this->publishable_key = ! empty( $main_settings['test_publishable_key'] ) ? $main_settings['test_publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['test_secret_key'] ) ? $main_settings['test_secret_key'] : '';
+ }
+
+ add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, [ $this, 'process_admin_options' ] );
+ add_action( 'wp_enqueue_scripts', [ $this, 'payment_scripts' ] );
+ }
+
+ /**
+ * Returns all supported currencies for this payment method.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return array
+ */
+ public function get_supported_currency() {
+ return apply_filters(
+ 'wc_stripe_sofort_supported_currencies',
+ [
+ 'EUR',
+ ]
+ );
+ }
+
+ /**
+ * Checks to see if all criteria is met before showing payment method.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return bool
+ */
+ public function is_available() {
+ if ( ! in_array( get_woocommerce_currency(), $this->get_supported_currency() ) ) {
+ return false;
+ }
+
+ return parent::is_available();
+ }
+
+ /**
+ * Get_icon function.
+ *
+ * @since 1.0.0
+ * @version 4.0.0
+ * @return string
+ */
+ public function get_icon() {
+ $icons = $this->payment_icons();
+
+ $icons_str = '';
+
+ $icons_str .= isset( $icons['sofort'] ) ? $icons['sofort'] : '';
+
+ return apply_filters( 'woocommerce_gateway_icon', $icons_str, $this->id );
+ }
+
+ /**
+ * Outputs scripts used for stripe payment
+ */
+ public function payment_scripts() {
+ if ( ! is_cart() && ! is_checkout() && ! isset( $_GET['pay_for_order'] ) && ! is_add_payment_method_page() ) {
+ return;
+ }
+
+ wp_enqueue_style( 'stripe_styles' );
+ wp_enqueue_script( 'woocommerce_stripe' );
+ }
+
+ /**
+ * Initialize Gateway Settings Form Fields.
+ */
+ public function init_form_fields() {
+ $this->form_fields = require WC_STRIPE_PLUGIN_PATH . '/includes/admin/stripe-sofort-settings.php';
+ }
+
+ /**
+ * Payment form on checkout page
+ */
+ public function payment_fields() {
+ global $wp;
+ $user = wp_get_current_user();
+ $total = WC()->cart->total;
+ $description = $this->get_description();
+
+ // If paying from order, we need to get total from order not cart.
+ if ( isset( $_GET['pay_for_order'] ) && ! empty( $_GET['key'] ) ) {
+ $order = wc_get_order( wc_clean( $wp->query_vars['order-pay'] ) );
+ $total = $order->get_total();
+ }
+
+ if ( is_add_payment_method_page() ) {
+ $pay_button_text = __( 'Add Payment', 'woocommerce-gateway-stripe' );
+ $total = '';
+ } else {
+ $pay_button_text = '';
+ }
+
+ echo '<div
+ id="stripe-sofort-payment-data"
+ data-amount="' . esc_attr( WC_Stripe_Helper::get_stripe_amount( $total ) ) . '"
+ data-currency="' . esc_attr( strtolower( get_woocommerce_currency() ) ) . '">';
+
+ if ( $description ) {
+ echo apply_filters( 'wc_stripe_description', wpautop( wp_kses_post( $description ) ), $this->id );
+ }
+
+ echo '</div>';
+ }
+
+ /**
+ * Creates the source for charge.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @param object $order
+ * @return mixed
+ */
+ public function create_source( $order ) {
+ $currency = $order->get_currency();
+ $bank_country = $order->get_billing_country();
+ $return_url = $this->get_stripe_return_url( $order );
+ $post_data = [];
+ $post_data['amount'] = WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $currency );
+ $post_data['currency'] = strtolower( $currency );
+ $post_data['type'] = 'sofort';
+ $post_data['owner'] = $this->get_owner_details( $order );
+ $post_data['redirect'] = [ 'return_url' => $return_url ];
+ $post_data['sofort'] = [
+ 'country' => $bank_country,
+ 'preferred_language' => $this->get_locale(),
+ ];
+
+ if ( ! empty( $this->statement_descriptor ) ) {
+ $post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $this->statement_descriptor );
+ }
+
+ WC_Stripe_Logger::log( 'Info: Begin creating Sofort source' );
+
+ return WC_Stripe_API::request( apply_filters( 'wc_stripe_sofort_source', $post_data, $order ), 'sources' );
+ }
+
+ /**
+ * Process the payment
+ *
+ * @param int $order_id Reference.
+ * @param bool $retry Should we retry on fail.
+ * @param bool $force_save_source Force payment source to be saved.
+ *
+ * @throws Exception If payment will not be accepted.
+ *
+ * @return array|void
+ */
+ public function process_payment( $order_id, $retry = true, $force_save_source = false ) {
+ try {
+ $order = wc_get_order( $order_id );
+
+ // This will throw exception if not valid.
+ $this->validate_minimum_order_amount( $order );
+
+ // This comes from the create account checkbox in the checkout page.
+ $create_account = ! empty( $_POST['createaccount'] ) ? true : false;
+
+ if ( $create_account ) {
+ $new_customer_id = $order->get_customer_id();
+ $new_stripe_customer = new WC_Stripe_Customer( $new_customer_id );
+ $new_stripe_customer->create_customer();
+ }
+
+ $response = $this->create_source( $order );
+
+ if ( ! empty( $response->error ) ) {
+ $order->add_order_note( $response->error->message );
+
+ $localized_messages = WC_Stripe_Helper::get_localized_messages();
+
+ if ( 'invalid_sofort_country' === $response->error->code ) {
+ $localized_message = isset( $localized_messages[ $response->error->code ] ) ? $localized_messages[ $response->error->code ] : $response->error->message;
+ } else {
+ $localized_message = isset( $localized_messages[ $response->error->type ] ) ? $localized_messages[ $response->error->type ] : $response->error->message;
+ }
+
+ throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
+ }
+
+ $order->update_meta_data( '_stripe_source_id', $response->id );
+ $order->save();
+
+ WC_Stripe_Logger::log( 'Info: Redirecting to Sofort...' );
+
+ return [
+ 'result' => 'success',
+ 'redirect' => esc_url_raw( $response->redirect->url ),
+ ];
+ } catch ( WC_Stripe_Exception $e ) {
+ wc_add_notice( $e->getLocalizedMessage(), 'error' );
+ WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
+
+ do_action( 'wc_gateway_stripe_process_payment_error', $e, $order );
+
+ if ( $order->has_status(
+ apply_filters(
+ 'wc_stripe_allowed_payment_processing_statuses',
+ [ 'pending', 'failed' ],
+ $order
+ )
+ ) ) {
+ $this->send_failed_order_email( $order_id );
+ }
+
+ return [
+ 'result' => 'fail',
+ 'redirect' => '',
+ ];
+ }
+ }
+}
diff --git a/includes/payment-methods/class-wc-stripe-payment-request.php b/includes/payment-methods/class-wc-stripe-payment-request.php
new file mode 100644
index 0000000..7ef7535
--- /dev/null
+++ b/includes/payment-methods/class-wc-stripe-payment-request.php
@@ -0,0 +1,1882 @@
+<?php
+/**
+ * Stripe Payment Request API
+ * Adds support for Apple Pay and Chrome Payment Request API buttons.
+ * Utilizes the Stripe Payment Request Button to support checkout from the product detail and cart pages.
+ *
+ * @package WooCommerce_Stripe/Classes/Payment_Request
+ * @since 4.0.0
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * WC_Stripe_Payment_Request class.
+ */
+class WC_Stripe_Payment_Request {
+
+ use WC_Stripe_Pre_Orders_Trait;
+
+ /**
+ * Enabled.
+ *
+ * @var
+ */
+ public $stripe_settings;
+
+ /**
+ * Total label
+ *
+ * @var
+ */
+ public $total_label;
+
+ /**
+ * Key
+ *
+ * @var
+ */
+ public $publishable_key;
+
+ /**
+ * Key
+ *
+ * @var
+ */
+ public $secret_key;
+
+ /**
+ * Is test mode active?
+ *
+ * @var bool
+ */
+ public $testmode;
+
+ /**
+ * This Instance.
+ *
+ * @var
+ */
+ private static $_this;
+
+ /**
+ * Initialize class actions.
+ *
+ * @since 3.0.0
+ * @version 4.0.0
+ */
+ public function __construct() {
+ self::$_this = $this;
+ $this->stripe_settings = get_option( 'woocommerce_stripe_settings', [] );
+ $this->testmode = ( ! empty( $this->stripe_settings['testmode'] ) && 'yes' === $this->stripe_settings['testmode'] ) ? true : false;
+ $this->publishable_key = ! empty( $this->stripe_settings['publishable_key'] ) ? $this->stripe_settings['publishable_key'] : '';
+ $this->secret_key = ! empty( $this->stripe_settings['secret_key'] ) ? $this->stripe_settings['secret_key'] : '';
+ $this->total_label = ! empty( $this->stripe_settings['statement_descriptor'] ) ? WC_Stripe_Helper::clean_statement_descriptor( $this->stripe_settings['statement_descriptor'] ) : '';
+
+ if ( $this->testmode ) {
+ $this->publishable_key = ! empty( $this->stripe_settings['test_publishable_key'] ) ? $this->stripe_settings['test_publishable_key'] : '';
+ $this->secret_key = ! empty( $this->stripe_settings['test_secret_key'] ) ? $this->stripe_settings['test_secret_key'] : '';
+ }
+
+ $this->total_label = str_replace( "'", '', $this->total_label ) . apply_filters( 'wc_stripe_payment_request_total_label_suffix', ' (via WooCommerce)' );
+
+ // Checks if Stripe Gateway is enabled.
+ if ( empty( $this->stripe_settings ) || ( isset( $this->stripe_settings['enabled'] ) && 'yes' !== $this->stripe_settings['enabled'] ) ) {
+ return;
+ }
+
+ // Checks if Payment Request is enabled.
+ if ( ! isset( $this->stripe_settings['payment_request'] ) || 'yes' !== $this->stripe_settings['payment_request'] ) {
+ return;
+ }
+
+ // Don't load for change payment method page.
+ if ( isset( $_GET['change_payment_method'] ) ) {
+ return;
+ }
+
+ $this->init();
+ }
+
+ /**
+ * Checks whether authentication is required for checkout.
+ *
+ * @since 5.1.0
+ * @version 5.3.0
+ *
+ * @return bool
+ */
+ public function is_authentication_required() {
+ // If guest checkout is disabled and account creation upon checkout is not possible, authentication is required.
+ if ( 'no' === get_option( 'woocommerce_enable_guest_checkout', 'yes' ) && ! $this->is_account_creation_possible() ) {
+ return true;
+ }
+ // If cart contains subscription and account creation upon checkout is not posible, authentication is required.
+ if ( $this->has_subscription_product() && ! $this->is_account_creation_possible() ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks whether account creation is possible upon checkout.
+ *
+ * @since 5.1.0
+ *
+ * @return bool
+ */
+ public function is_account_creation_possible() {
+ // If automatically generate username/password are disabled, the Payment Request API
+ // can't include any of those fields, so account creation is not possible.
+ return (
+ 'yes' === get_option( 'woocommerce_enable_signup_and_login_from_checkout', 'no' ) &&
+ 'yes' === get_option( 'woocommerce_registration_generate_username', 'yes' ) &&
+ 'yes' === get_option( 'woocommerce_registration_generate_password', 'yes' )
+ );
+ }
+
+ /**
+ * Checks if keys are set and valid.
+ *
+ * @since 4.0.6
+ * @return boolean True if the keys are set *and* valid, false otherwise (for example, if keys are empty or the secret key was pasted as publishable key).
+ */
+ public function are_keys_set() {
+ // NOTE: updates to this function should be added to are_keys_set()
+ // in includes/abstracts/abstract-wc-stripe-payment-gateway.php
+ if ( $this->testmode ) {
+ return preg_match( '/^pk_test_/', $this->publishable_key )
+ && preg_match( '/^[rs]k_test_/', $this->secret_key );
+ } else {
+ return preg_match( '/^pk_live_/', $this->publishable_key )
+ && preg_match( '/^[rs]k_live_/', $this->secret_key );
+ }
+ }
+
+ /**
+ * Get this instance.
+ *
+ * @since 4.0.6
+ * @return class
+ */
+ public static function instance() {
+ return self::$_this;
+ }
+
+ /**
+ * Sets the WC customer session if one is not set.
+ * This is needed so nonces can be verified by AJAX Request.
+ *
+ * @since 4.0.0
+ * @version 5.2.0
+ * @return void
+ */
+ public function set_session() {
+ if ( ! $this->is_product() || ( isset( WC()->session ) && WC()->session->has_session() ) ) {
+ return;
+ }
+
+ WC()->session->set_customer_session_cookie( true );
+ }
+
+ /**
+ * Handles payment request redirect when the redirect dialog "Continue" button is clicked.
+ *
+ * @since 5.3.0
+ */
+ public function handle_payment_request_redirect() {
+ if (
+ ! empty( $_GET['wc_stripe_payment_request_redirect_url'] )
+ && ! empty( $_GET['_wpnonce'] )
+ && wp_verify_nonce( $_GET['_wpnonce'], 'wc-stripe-set-redirect-url' ) // @codingStandardsIgnoreLine
+ ) {
+ $url = rawurldecode( esc_url_raw( wp_unslash( $_GET['wc_stripe_payment_request_redirect_url'] ) ) );
+ // Sets a redirect URL cookie for 10 minutes, which we will redirect to after authentication.
+ // Users will have a 10 minute timeout to login/create account, otherwise redirect URL expires.
+ wc_setcookie( 'wc_stripe_payment_request_redirect_url', $url, time() + MINUTE_IN_SECONDS * 10 );
+ // Redirects to "my-account" page.
+ wp_safe_redirect( get_permalink( get_option( 'woocommerce_myaccount_page_id' ) ) );
+ exit;
+ }
+ }
+
+ /**
+ * Initialize hooks.
+ *
+ * @since 4.0.0
+ * @version 5.3.0
+ * @return void
+ */
+ public function init() {
+
+ add_action( 'template_redirect', [ $this, 'set_session' ] );
+ add_action( 'template_redirect', [ $this, 'handle_payment_request_redirect' ] );
+
+ add_action( 'wp_enqueue_scripts', [ $this, 'scripts' ] );
+
+ add_action( 'woocommerce_after_add_to_cart_quantity', [ $this, 'display_payment_request_button_html' ], 1 );
+ add_action( 'woocommerce_after_add_to_cart_quantity', [ $this, 'display_payment_request_button_separator_html' ], 2 );
+
+ add_action( 'woocommerce_proceed_to_checkout', [ $this, 'display_payment_request_button_html' ], 1 );
+ add_action( 'woocommerce_proceed_to_checkout', [ $this, 'display_payment_request_button_separator_html' ], 2 );
+
+ add_action( 'woocommerce_checkout_before_customer_details', [ $this, 'display_payment_request_button_html' ], 1 );
+ add_action( 'woocommerce_checkout_before_customer_details', [ $this, 'display_payment_request_button_separator_html' ], 2 );
+
+ add_action( 'wc_ajax_wc_stripe_get_cart_details', [ $this, 'ajax_get_cart_details' ] );
+ add_action( 'wc_ajax_wc_stripe_get_shipping_options', [ $this, 'ajax_get_shipping_options' ] );
+ add_action( 'wc_ajax_wc_stripe_update_shipping_method', [ $this, 'ajax_update_shipping_method' ] );
+ add_action( 'wc_ajax_wc_stripe_create_order', [ $this, 'ajax_create_order' ] );
+ add_action( 'wc_ajax_wc_stripe_add_to_cart', [ $this, 'ajax_add_to_cart' ] );
+ add_action( 'wc_ajax_wc_stripe_get_selected_product_data', [ $this, 'ajax_get_selected_product_data' ] );
+ add_action( 'wc_ajax_wc_stripe_clear_cart', [ $this, 'ajax_clear_cart' ] );
+ add_action( 'wc_ajax_wc_stripe_log_errors', [ $this, 'ajax_log_errors' ] );
+
+ add_filter( 'woocommerce_gateway_title', [ $this, 'filter_gateway_title' ], 10, 2 );
+ add_action( 'woocommerce_checkout_order_processed', [ $this, 'add_order_meta' ], 10, 2 );
+ add_filter( 'woocommerce_login_redirect', [ $this, 'get_login_redirect_url' ], 10, 3 );
+ add_filter( 'woocommerce_registration_redirect', [ $this, 'get_login_redirect_url' ], 10, 3 );
+ }
+
+ /**
+ * Gets the button type.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return string
+ */
+ public function get_button_type() {
+ return isset( $this->stripe_settings['payment_request_button_type'] ) ? $this->stripe_settings['payment_request_button_type'] : 'default';
+ }
+
+ /**
+ * Gets the button theme.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return string
+ */
+ public function get_button_theme() {
+ return isset( $this->stripe_settings['payment_request_button_theme'] ) ? $this->stripe_settings['payment_request_button_theme'] : 'dark';
+ }
+
+ /**
+ * Gets the button height.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return string
+ */
+ public function get_button_height() {
+ if ( ! WC_Stripe_Feature_Flags::is_upe_preview_enabled() ) {
+ return isset( $this->stripe_settings['payment_request_button_height'] ) ? str_replace( 'px', '', $this->stripe_settings['payment_request_button_height'] ) : '64';
+ }
+
+ $height = isset( $this->stripe_settings['payment_request_button_size'] ) ? $this->stripe_settings['payment_request_button_size'] : 'default';
+ if ( 'medium' === $height ) {
+ return '48';
+ }
+
+ if ( 'large' === $height ) {
+ return '56';
+ }
+
+ // for the "default" and "catch-all" scenarios.
+ return '40';
+ }
+
+ /**
+ * Checks if the button is branded.
+ *
+ * @since 4.4.0
+ * @version 4.4.0
+ * @return boolean
+ */
+ public function is_branded_button() {
+ return 'branded' === $this->get_button_type();
+ }
+
+ /**
+ * Gets the branded button type.
+ *
+ * @since 4.4.0
+ * @version 4.4.0
+ * @return string
+ */
+ public function get_button_branded_type() {
+ return isset( $this->stripe_settings['payment_request_button_branded_type'] ) ? $this->stripe_settings['payment_request_button_branded_type'] : 'default';
+ }
+
+ /**
+ * Checks if the button is custom.
+ *
+ * @since 4.4.0
+ * @version 4.4.0
+ * @return boolean
+ */
+ public function is_custom_button() {
+ // no longer a valid option
+ if ( WC_Stripe_Feature_Flags::is_upe_preview_enabled() ) {
+ return false;
+ }
+
+ return 'custom' === $this->get_button_type();
+ }
+
+ /**
+ * Returns custom button css selector.
+ *
+ * @since 4.4.0
+ * @version 4.4.0
+ * @return string
+ */
+ public function custom_button_selector() {
+ return $this->is_custom_button() ? '#wc-stripe-custom-button' : '';
+ }
+
+ /**
+ * Gets the custom button label.
+ *
+ * @since 4.4.0
+ * @version 4.4.0
+ * @return string
+ */
+ public function get_button_label() {
+ // no longer a valid option
+ if ( WC_Stripe_Feature_Flags::is_upe_preview_enabled() ) {
+ return '';
+ }
+
+ return isset( $this->stripe_settings['payment_request_button_label'] ) ? $this->stripe_settings['payment_request_button_label'] : 'Buy now';
+ }
+
+ /**
+ * Gets the product total price.
+ *
+ * @since 5.2.0
+ *
+ * @param object $product WC_Product_* object.
+ * @return integer Total price.
+ */
+ public function get_product_price( $product ) {
+ $product_price = $product->get_price();
+ // Add subscription sign-up fees to product price.
+ if ( 'subscription' === $product->get_type() && class_exists( 'WC_Subscriptions_Product' ) ) {
+ $product_price = $product->get_price() + WC_Subscriptions_Product::get_sign_up_fee( $product );
+ }
+
+ return $product_price;
+ }
+
+ /**
+ * Gets the product data for the currently viewed page
+ *
+ * @since 4.0.0
+ * @version 5.2.0
+ * @return mixed Returns false if not on a product page, the product information otherwise.
+ */
+ public function get_product_data() {
+ if ( ! $this->is_product() ) {
+ return false;
+ }
+
+ $product = $this->get_product();
+
+ if ( 'variable' === $product->get_type() ) {
+ $variation_attributes = $product->get_variation_attributes();
+ $attributes = [];
+
+ foreach ( $variation_attributes as $attribute_name => $attribute_values ) {
+ $attribute_key = 'attribute_' . sanitize_title( $attribute_name );
+
+ // Passed value via GET takes precedence. Otherwise get the default value for given attribute
+ $attributes[ $attribute_key ] = isset( $_GET[ $attribute_key ] )
+ ? wc_clean( wp_unslash( $_GET[ $attribute_key ] ) )
+ : $product->get_variation_default_attribute( $attribute_name );
+ }
+
+ $data_store = WC_Data_Store::load( 'product' );
+ $variation_id = $data_store->find_matching_product_variation( $product, $attributes );
+
+ if ( ! empty( $variation_id ) ) {
+ $product = wc_get_product( $variation_id );
+ }
+ }
+
+ $data = [];
+ $items = [];
+
+ $items[] = [
+ 'label' => $product->get_name(),
+ 'amount' => WC_Stripe_Helper::get_stripe_amount( $this->get_product_price( $product ) ),
+ ];
+
+ if ( wc_tax_enabled() ) {
+ $items[] = [
+ 'label' => __( 'Tax', 'woocommerce-gateway-stripe' ),
+ 'amount' => 0,
+ 'pending' => true,
+ ];
+ }
+
+ if ( wc_shipping_enabled() && $product->needs_shipping() ) {
+ $items[] = [
+ 'label' => __( 'Shipping', 'woocommerce-gateway-stripe' ),
+ 'amount' => 0,
+ 'pending' => true,
+ ];
+
+ $data['shippingOptions'] = [
+ 'id' => 'pending',
+ 'label' => __( 'Pending', 'woocommerce-gateway-stripe' ),
+ 'detail' => '',
+ 'amount' => 0,
+ ];
+ }
+
+ $data['displayItems'] = $items;
+ $data['total'] = [
+ 'label' => apply_filters( 'wc_stripe_payment_request_total_label', $this->total_label ),
+ 'amount' => WC_Stripe_Helper::get_stripe_amount( $this->get_product_price( $product ) ),
+ ];
+
+ $data['requestShipping'] = ( wc_shipping_enabled() && $product->needs_shipping() && 0 !== wc_get_shipping_method_count( true ) );
+ $data['currency'] = strtolower( get_woocommerce_currency() );
+ $data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 );
+
+ return apply_filters( 'wc_stripe_payment_request_product_data', $data, $product );
+ }
+
+ /**
+ * Filters the gateway title to reflect Payment Request type
+ */
+ public function filter_gateway_title( $title, $id ) {
+ global $post;
+
+ if ( ! is_object( $post ) ) {
+ return $title;
+ }
+
+ $order = wc_get_order( $post->ID );
+ $method_title = is_object( $order ) ? $order->get_payment_method_title() : '';
+
+ if ( 'stripe' === $id && ! empty( $method_title ) ) {
+ if ( 'Apple Pay (Stripe)' === $method_title
+ || 'Google Pay (Stripe)' === $method_title
+ || 'Payment Request (Stripe)' === $method_title
+ ) {
+ return $method_title;
+ }
+
+ // We renamed 'Chrome Payment Request' to just 'Payment Request' since Payment Requests
+ // are supported by other browsers besides Chrome. As such, we need to check for the
+ // old title to make sure older orders still reflect that they were paid via Payment
+ // Request Buttons.
+ if ( 'Chrome Payment Request (Stripe)' === $method_title ) {
+ return 'Payment Request (Stripe)';
+ }
+
+ return $method_title;
+ }
+
+ return $title;
+ }
+
+ /**
+ * Normalizes postal code in case of redacted data from Apple Pay.
+ *
+ * @since 5.2.0
+ *
+ * @param string $postcode Postal code.
+ * @param string $country Country.
+ */
+ public function get_normalized_postal_code( $postcode, $country ) {
+ /**
+ * Currently, Apple Pay truncates the UK and Canadian postal codes to the first 4 and 3 characters respectively
+ * when passing it back from the shippingcontactselected object. This causes WC to invalidate
+ * the postal code and not calculate shipping zones correctly.
+ */
+ if ( 'GB' === $country ) {
+ // Replaces a redacted string with something like LN10***.
+ return str_pad( preg_replace( '/\s+/', '', $postcode ), 7, '*' );
+ }
+ if ( 'CA' === $country ) {
+ // Replaces a redacted string with something like L4Y***.
+ return str_pad( preg_replace( '/\s+/', '', $postcode ), 6, '*' );
+ }
+
+ return $postcode;
+ }
+
+ /**
+ * Add needed order meta
+ *
+ * @param integer $order_id The order ID.
+ * @param array $posted_data The posted data from checkout form.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return void
+ */
+ public function add_order_meta( $order_id, $posted_data ) {
+ if ( empty( $_POST['payment_request_type'] ) || ! isset( $_POST['payment_method'] ) || 'stripe' !== $_POST['payment_method'] ) {
+ return;
+ }
+
+ $order = wc_get_order( $order_id );
+
+ $payment_request_type = wc_clean( wp_unslash( $_POST['payment_request_type'] ) );
+
+ if ( 'apple_pay' === $payment_request_type ) {
+ $order->set_payment_method_title( 'Apple Pay (Stripe)' );
+ $order->save();
+ } elseif ( 'google_pay' === $payment_request_type ) {
+ $order->set_payment_method_title( 'Google Pay (Stripe)' );
+ $order->save();
+ } elseif ( 'payment_request_api' === $payment_request_type ) {
+ $order->set_payment_method_title( 'Payment Request (Stripe)' );
+ $order->save();
+ }
+ }
+
+ /**
+ * Checks to make sure product type is supported.
+ *
+ * @since 3.1.0
+ * @version 4.0.0
+ * @return array
+ */
+ public function supported_product_types() {
+ return apply_filters(
+ 'wc_stripe_payment_request_supported_types',
+ [
+ 'simple',
+ 'variable',
+ 'variation',
+ 'subscription',
+ 'variable-subscription',
+ 'subscription_variation',
+ 'booking',
+ 'bundle',
+ 'composite',
+ ]
+ );
+ }
+
+ /**
+ * Checks the cart to see if all items are allowed to be used.
+ *
+ * @since 3.1.4
+ * @version 4.0.0
+ * @return boolean
+ */
+ public function allowed_items_in_cart() {
+ // Pre Orders compatibility where we don't support charge upon release.
+ if ( $this->is_pre_order_item_in_cart() && $this->is_pre_order_product_charged_upon_release( $this->get_pre_order_product_from_cart() ) ) {
+ return false;
+ }
+
+ // If the cart is not available we don't have any unsupported products in the cart, so we
+ // return true. This can happen e.g. when loading the cart or checkout blocks in Gutenberg.
+ if ( is_null( WC()->cart ) ) {
+ return true;
+ }
+
+ foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
+ $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
+
+ if ( ! in_array( $_product->get_type(), $this->supported_product_types() ) ) {
+ return false;
+ }
+
+ // Trial subscriptions with shipping are not supported.
+ if ( class_exists( 'WC_Subscriptions_Product' ) && WC_Subscriptions_Product::is_subscription( $_product ) && $_product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $_product ) > 0 ) {
+ return false;
+ }
+ }
+
+ // We don't support multiple packages with Payment Request Buttons because we can't offer
+ // a good UX.
+ $packages = WC()->cart->get_shipping_packages();
+ if ( 1 < count( $packages ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks whether cart contains a subscription product or this is a subscription product page.
+ *
+ * @since 5.3.0
+ * @version 5.3.0
+ * @return boolean
+ */
+ public function has_subscription_product() {
+ if ( ! class_exists( 'WC_Subscriptions_Product' ) ) {
+ return false;
+ }
+
+ if ( $this->is_product() ) {
+ $product = $this->get_product();
+ if ( WC_Subscriptions_Product::is_subscription( $product ) ) {
+ return true;
+ }
+ } elseif ( WC_Stripe_Helper::has_cart_or_checkout_on_current_page() ) {
+ foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
+ $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
+ if ( WC_Subscriptions_Product::is_subscription( $_product ) ) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks if this is a product page or content contains a product_page shortcode.
+ *
+ * @since 5.2.0
+ * @return boolean
+ */
+ public function is_product() {
+ return is_product() || wc_post_content_has_shortcode( 'product_page' );
+ }
+
+ /**
+ * Get product from product page or product_page shortcode.
+ *
+ * @since 5.2.0
+ * @return WC_Product Product object.
+ */
+ public function get_product() {
+ global $post;
+
+ if ( is_product() ) {
+ return wc_get_product( $post->ID );
+ } elseif ( wc_post_content_has_shortcode( 'product_page' ) ) {
+ // Get id from product_page shortcode.
+ preg_match( '/\[product_page id="(?<id>\d+)"\]/', $post->post_content, $shortcode_match );
+
+ if ( ! isset( $shortcode_match['id'] ) ) {
+ return false;
+ }
+
+ return wc_get_product( $shortcode_match['id'] );
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the login redirect URL.
+ *
+ * @since 5.3.0
+ *
+ * @param string $redirect Default redirect URL.
+ * @return string Redirect URL.
+ */
+ public function get_login_redirect_url( $redirect ) {
+ $url = esc_url_raw( wp_unslash( isset( $_COOKIE['wc_stripe_payment_request_redirect_url'] ) ? $_COOKIE['wc_stripe_payment_request_redirect_url'] : '' ) );
+
+ if ( empty( $url ) ) {
+ return $redirect;
+ }
+ wc_setcookie( 'wc_stripe_payment_request_redirect_url', null );
+
+ return $url;
+ }
+
+ /**
+ * Returns the JavaScript configuration object used for any pages with a payment request button.
+ *
+ * @return array The settings used for the payment request button in JavaScript.
+ */
+ public function javascript_params() {
+ $needs_shipping = 'no';
+ if ( ! is_null( WC()->cart ) && WC()->cart->needs_shipping() ) {
+ $needs_shipping = 'yes';
+ }
+
+ return [
+ 'ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ),
+ 'stripe' => [
+ 'key' => $this->publishable_key,
+ 'allow_prepaid_card' => apply_filters( 'wc_stripe_allow_prepaid_card', true ) ? 'yes' : 'no',
+ 'locale' => WC_Stripe_Helper::convert_wc_locale_to_stripe_locale( get_locale() ),
+ ],
+ 'nonce' => [
+ 'payment' => wp_create_nonce( 'wc-stripe-payment-request' ),
+ 'shipping' => wp_create_nonce( 'wc-stripe-payment-request-shipping' ),
+ 'update_shipping' => wp_create_nonce( 'wc-stripe-update-shipping-method' ),
+ 'checkout' => wp_create_nonce( 'woocommerce-process_checkout' ),
+ 'add_to_cart' => wp_create_nonce( 'wc-stripe-add-to-cart' ),
+ 'get_selected_product_data' => wp_create_nonce( 'wc-stripe-get-selected-product-data' ),
+ 'log_errors' => wp_create_nonce( 'wc-stripe-log-errors' ),
+ 'clear_cart' => wp_create_nonce( 'wc-stripe-clear-cart' ),
+ ],
+ 'i18n' => [
+ 'no_prepaid_card' => __( 'Sorry, we\'re not accepting prepaid cards at this time.', 'woocommerce-gateway-stripe' ),
+ /* translators: Do not translate the [option] placeholder */
+ 'unknown_shipping' => __( 'Unknown shipping option "[option]".', 'woocommerce-gateway-stripe' ),
+ ],
+ 'checkout' => [
+ 'url' => wc_get_checkout_url(),
+ 'currency_code' => strtolower( get_woocommerce_currency() ),
+ 'country_code' => substr( get_option( 'woocommerce_default_country' ), 0, 2 ),
+ 'needs_shipping' => $needs_shipping,
+ // Defaults to 'required' to match how core initializes this option.
+ 'needs_payer_phone' => 'required' === get_option( 'woocommerce_checkout_phone_field', 'required' ),
+ ],
+ 'button' => $this->get_button_settings(),
+ 'login_confirmation' => $this->get_login_confirmation_settings(),
+ 'is_product_page' => $this->is_product(),
+ 'product' => $this->get_product_data(),
+ ];
+ }
+
+ /**
+ * Load public scripts and styles.
+ *
+ * @since 3.1.0
+ * @version 5.2.0
+ */
+ public function scripts() {
+ // If page is not supported, bail.
+ // Note: This check is not in `should_show_payment_request_button()` because that function is
+ // also called by the blocks support class, and this check would fail *incorrectly* when
+ // called from there.
+ if ( ! $this->is_page_supported() ) {
+ return;
+ }
+
+ if ( ! $this->should_show_payment_request_button() ) {
+ return;
+ }
+
+ $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
+
+ wp_register_script( 'stripe', 'https://js.stripe.com/v3/', '', '3.0', true );
+ wp_register_script( 'wc_stripe_payment_request', plugins_url( 'assets/js/stripe-payment-request' . $suffix . '.js', WC_STRIPE_MAIN_FILE ), [ 'jquery', 'stripe' ], WC_STRIPE_VERSION, true );
+
+ wp_localize_script(
+ 'wc_stripe_payment_request',
+ 'wc_stripe_payment_request_params',
+ apply_filters(
+ 'wc_stripe_payment_request_params',
+ $this->javascript_params()
+ )
+ );
+
+ wp_enqueue_script( 'wc_stripe_payment_request' );
+ }
+
+ /**
+ * Returns true if the current page supports Payment Request Buttons, false otherwise.
+ *
+ * @since 5.3.0
+ * @version 5.3.0
+ * @return boolean True if the current page is supported, false otherwise.
+ */
+ private function is_page_supported() {
+ return $this->is_product()
+ || WC_Stripe_Helper::has_cart_or_checkout_on_current_page()
+ || isset( $_GET['pay_for_order'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ }
+
+ /**
+ * Display the payment request button.
+ *
+ * @since 4.0.0
+ * @version 5.2.0
+ */
+ public function display_payment_request_button_html() {
+ $gateways = WC()->payment_gateways->get_available_payment_gateways();
+
+ if ( ! isset( $gateways['stripe'] ) ) {
+ return;
+ }
+
+ if ( ! $this->is_page_supported() ) {
+ return;
+ }
+
+ if ( ! $this->should_show_payment_request_button() ) {
+ return;
+ }
+
+ ?>
+ <div id="wc-stripe-payment-request-wrapper" style="clear:both;padding-top:1.5em;display:none;">
+ <div id="wc-stripe-payment-request-button">
+ <?php
+ if ( $this->is_custom_button() ) {
+ $label = esc_html( $this->get_button_label() );
+ $class_name = esc_attr( 'button ' . $this->get_button_theme() );
+ $style = esc_attr( 'height:' . $this->get_button_height() . 'px;' );
+ echo "<button id=\"wc-stripe-custom-button\" class=\"$class_name\" style=\"$style\"> $label </button>";
+ }
+ ?>
+ <!-- A Stripe Element will be inserted here. -->
+ </div>
+ </div>
+ <?php
+ }
+
+ /**
+ * Display payment request button separator.
+ *
+ * @since 4.0.0
+ * @version 5.2.0
+ */
+ public function display_payment_request_button_separator_html() {
+ $gateways = WC()->payment_gateways->get_available_payment_gateways();
+
+ if ( ! isset( $gateways['stripe'] ) ) {
+ return;
+ }
+
+ if ( ! is_cart() && ! is_checkout() && ! $this->is_product() && ! isset( $_GET['pay_for_order'] ) ) {
+ return;
+ }
+
+ if ( is_checkout() && ! in_array( 'checkout', $this->get_button_locations(), true ) ) {
+ return;
+ }
+ ?>
+ <p id="wc-stripe-payment-request-button-separator" style="margin-top:1.5em;text-align:center;display:none;">— <?php esc_html_e( 'OR', 'woocommerce-gateway-stripe' ); ?> —</p>
+ <?php
+ }
+
+ /**
+ * Returns true if Payment Request Buttons are supported on the current page, false
+ * otherwise.
+ *
+ * @since 5.3.0
+ * @version 5.3.0
+ * @return boolean True if PRBs are supported on current page, false otherwise
+ */
+ public function should_show_payment_request_button() {
+ // If keys are not set bail.
+ if ( ! $this->are_keys_set() ) {
+ WC_Stripe_Logger::log( 'Keys are not set correctly.' );
+ return false;
+ }
+
+ // If no SSL bail.
+ if ( ! $this->testmode && ! is_ssl() ) {
+ WC_Stripe_Logger::log( 'Stripe Payment Request live mode requires SSL.' );
+ return false;
+ }
+
+ // Don't show if on the cart or checkout page, or if page contains the cart or checkout
+ // shortcodes, with items in the cart that aren't supported.
+ if (
+ WC_Stripe_Helper::has_cart_or_checkout_on_current_page()
+ && ! $this->allowed_items_in_cart()
+ ) {
+ return false;
+ }
+
+ // Don't show on cart if disabled.
+ if ( is_cart() && ! $this->should_show_prb_on_cart_page() ) {
+ return false;
+ }
+
+ // Don't show on checkout if disabled.
+ if ( is_checkout() && ! $this->should_show_prb_on_checkout_page() ) {
+ return false;
+ }
+
+ // Don't show if product page PRB is disabled.
+ if ( $this->is_product() && ! $this->should_show_prb_on_product_pages() ) {
+ return false;
+ }
+
+ // Don't show if product on current page is not supported.
+ if ( $this->is_product() && ! $this->is_product_supported( $this->get_product() ) ) {
+ return false;
+ }
+
+ if ( $this->is_product() && in_array( $this->get_product()->get_type(), [ 'variable', 'variable-subscription' ], true ) ) {
+ $stock_availability = array_column( $this->get_product()->get_available_variations(), 'is_in_stock' );
+ // Don't show if all product variations are out-of-stock.
+ if ( ! in_array( true, $stock_availability, true ) ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns true if Payment Request Buttons are enabled on the cart page, false
+ * otherwise.
+ *
+ * @since 5.5.0
+ * @version 5.5.0
+ * @return boolean True if PRBs are enabled on the cart page, false otherwise
+ */
+ public function should_show_prb_on_cart_page() {
+ $should_show_on_cart_page = in_array( 'cart', $this->get_button_locations(), true );
+
+ return apply_filters(
+ 'wc_stripe_show_payment_request_on_cart',
+ $should_show_on_cart_page
+ );
+ }
+
+ /**
+ * Returns true if Payment Request Buttons are enabled on the checkout page, false
+ * otherwise.
+ *
+ * @since 5.5.0
+ * @version 5.5.0
+ * @return boolean True if PRBs are enabled on the checkout page, false otherwise
+ */
+ public function should_show_prb_on_checkout_page() {
+ global $post;
+
+ $should_show_on_checkout_page = in_array( 'checkout', $this->get_button_locations(), true );
+
+ return apply_filters(
+ 'wc_stripe_show_payment_request_on_checkout',
+ $should_show_on_checkout_page,
+ $post
+ );
+ }
+
+ /**
+ * Returns true if Payment Request Buttons are enabled on product pages, false
+ * otherwise.
+ *
+ * @since 5.5.0
+ * @version 5.5.0
+ * @return boolean True if PRBs are enabled on product pages, false otherwise
+ */
+ public function should_show_prb_on_product_pages() {
+ global $post;
+
+ $should_show_on_product_page = in_array( 'product', $this->get_button_locations(), true );
+
+ // Note the negation because if the filter returns `true` that means we should hide the PRB.
+ return ! apply_filters(
+ 'wc_stripe_hide_payment_request_on_product_page',
+ ! $should_show_on_product_page,
+ $post
+ );
+ }
+
+ /**
+ * Returns true if a the provided product is supported, false otherwise.
+ *
+ * @param WC_Product $param The product that's being checked for support.
+ *
+ * @since 5.3.0
+ * @version 5.3.0
+ * @return boolean True if the provided product is supported, false otherwise.
+ */
+ private function is_product_supported( $product ) {
+ if ( ! is_object( $product ) || ! in_array( $product->get_type(), $this->supported_product_types() ) ) {
+ return false;
+ }
+
+ // Trial subscriptions with shipping are not supported.
+ if ( class_exists( 'WC_Subscriptions_Product' ) && $product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $product ) > 0 ) {
+ return false;
+ }
+
+ // Pre Orders charge upon release not supported.
+ if ( $this->is_pre_order_product_charged_upon_release( $product ) ) {
+ return false;
+ }
+
+ // Composite products are not supported on the product page.
+ if ( class_exists( 'WC_Composite_Products' ) && function_exists( 'is_composite_product' ) && is_composite_product() ) {
+ return false;
+ }
+
+ // File upload addon not supported
+ if ( class_exists( 'WC_Product_Addons_Helper' ) ) {
+ $product_addons = WC_Product_Addons_Helper::get_product_addons( $product->get_id() );
+ foreach ( $product_addons as $addon ) {
+ if ( 'file_upload' === $addon['type'] ) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Log errors coming from Payment Request
+ *
+ * @since 3.1.4
+ * @version 4.0.0
+ */
+ public function ajax_log_errors() {
+ check_ajax_referer( 'wc-stripe-log-errors', 'security' );
+
+ $errors = isset( $_POST['errors'] ) ? wc_clean( wp_unslash( $_POST['errors'] ) ) : '';
+
+ WC_Stripe_Logger::log( $errors );
+
+ exit;
+ }
+
+ /**
+ * Clears cart.
+ *
+ * @since 3.1.4
+ * @version 4.0.0
+ */
+ public function ajax_clear_cart() {
+ check_ajax_referer( 'wc-stripe-clear-cart', 'security' );
+
+ WC()->cart->empty_cart();
+ exit;
+ }
+
+ /**
+ * Get cart details.
+ */
+ public function ajax_get_cart_details() {
+ check_ajax_referer( 'wc-stripe-payment-request', 'security' );
+
+ if ( ! defined( 'WOOCOMMERCE_CART' ) ) {
+ define( 'WOOCOMMERCE_CART', true );
+ }
+
+ WC()->cart->calculate_totals();
+
+ $currency = get_woocommerce_currency();
+
+ // Set mandatory payment details.
+ $data = [
+ 'shipping_required' => WC()->cart->needs_shipping(),
+ 'order_data' => [
+ 'currency' => strtolower( $currency ),
+ 'country_code' => substr( get_option( 'woocommerce_default_country' ), 0, 2 ),
+ ],
+ ];
+
+ $data['order_data'] += $this->build_display_items();
+
+ wp_send_json( $data );
+ }
+
+ /**
+ * Get shipping options.
+ *
+ * @see WC_Cart::get_shipping_packages().
+ * @see WC_Shipping::calculate_shipping().
+ * @see WC_Shipping::get_packages().
+ */
+ public function ajax_get_shipping_options() {
+ check_ajax_referer( 'wc-stripe-payment-request-shipping', 'security' );
+
+ $shipping_address = filter_input_array(
+ INPUT_POST,
+ [
+ 'country' => FILTER_SANITIZE_STRING,
+ 'state' => FILTER_SANITIZE_STRING,
+ 'postcode' => FILTER_SANITIZE_STRING,
+ 'city' => FILTER_SANITIZE_STRING,
+ 'address' => FILTER_SANITIZE_STRING,
+ 'address_2' => FILTER_SANITIZE_STRING,
+ ]
+ );
+ $product_view_options = filter_input_array( INPUT_POST, [ 'is_product_page' => FILTER_SANITIZE_STRING ] );
+ $should_show_itemized_view = ! isset( $product_view_options['is_product_page'] ) ? true : filter_var( $product_view_options['is_product_page'], FILTER_VALIDATE_BOOLEAN );
+
+ $data = $this->get_shipping_options( $shipping_address, $should_show_itemized_view );
+ wp_send_json( $data );
+ }
+
+ /**
+ * Gets shipping options available for specified shipping address
+ *
+ * @param array $shipping_address Shipping address.
+ * @param boolean $itemized_display_items Indicates whether to show subtotals or itemized views.
+ *
+ * @return array Shipping options data.
+ * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag
+ */
+ public function get_shipping_options( $shipping_address, $itemized_display_items = false ) {
+ try {
+ // Set the shipping options.
+ $data = [];
+
+ // Remember current shipping method before resetting.
+ $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
+ $this->calculate_shipping( apply_filters( 'wc_stripe_payment_request_shipping_posted_values', $shipping_address ) );
+
+ $packages = WC()->shipping->get_packages();
+ $shipping_rate_ids = [];
+
+ if ( ! empty( $packages ) && WC()->customer->has_calculated_shipping() ) {
+ foreach ( $packages as $package_key => $package ) {
+ if ( empty( $package['rates'] ) ) {
+ throw new Exception( __( 'Unable to find shipping method for address.', 'woocommerce-gateway-stripe' ) );
+ }
+
+ foreach ( $package['rates'] as $key => $rate ) {
+ if ( in_array( $rate->id, $shipping_rate_ids, true ) ) {
+ // The Payment Requests will try to load indefinitely if there are duplicate shipping
+ // option IDs.
+ throw new Exception( __( 'Unable to provide shipping options for Payment Requests.', 'woocommerce-gateway-stripe' ) );
+ }
+ $shipping_rate_ids[] = $rate->id;
+ $data['shipping_options'][] = [
+ 'id' => $rate->id,
+ 'label' => $rate->label,
+ 'detail' => '',
+ 'amount' => WC_Stripe_Helper::get_stripe_amount( $rate->cost ),
+ ];
+ }
+ }
+ } else {
+ throw new Exception( __( 'Unable to find shipping method for address.', 'woocommerce-gateway-stripe' ) );
+ }
+
+ // The first shipping option is automatically applied on the client.
+ // Keep chosen shipping method by sorting shipping options if the method still available for new address.
+ // Fallback to the first available shipping method.
+ if ( isset( $data['shipping_options'][0] ) ) {
+ if ( isset( $chosen_shipping_methods[0] ) ) {
+ $chosen_method_id = $chosen_shipping_methods[0];
+ $compare_shipping_options = function ( $a, $b ) use ( $chosen_method_id ) {
+ if ( $a['id'] === $chosen_method_id ) {
+ return -1;
+ }
+
+ if ( $b['id'] === $chosen_method_id ) {
+ return 1;
+ }
+
+ return 0;
+ };
+ usort( $data['shipping_options'], $compare_shipping_options );
+ }
+
+ $first_shipping_method_id = $data['shipping_options'][0]['id'];
+ $this->update_shipping_method( [ $first_shipping_method_id ] );
+ }
+
+ WC()->cart->calculate_totals();
+
+ $data += $this->build_display_items( $itemized_display_items );
+ $data['result'] = 'success';
+ } catch ( Exception $e ) {
+ $data += $this->build_display_items( $itemized_display_items );
+ $data['result'] = 'invalid_shipping_address';
+ }
+
+ return $data;
+ }
+
+ /**
+ * Update shipping method.
+ */
+ public function ajax_update_shipping_method() {
+ check_ajax_referer( 'wc-stripe-update-shipping-method', 'security' );
+
+ if ( ! defined( 'WOOCOMMERCE_CART' ) ) {
+ define( 'WOOCOMMERCE_CART', true );
+ }
+
+ $shipping_methods = filter_input( INPUT_POST, 'shipping_method', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
+ $this->update_shipping_method( $shipping_methods );
+
+ WC()->cart->calculate_totals();
+
+ $product_view_options = filter_input_array( INPUT_POST, [ 'is_product_page' => FILTER_SANITIZE_STRING ] );
+ $should_show_itemized_view = ! isset( $product_view_options['is_product_page'] ) ? true : filter_var( $product_view_options['is_product_page'], FILTER_VALIDATE_BOOLEAN );
+
+ $data = [];
+ $data += $this->build_display_items( $should_show_itemized_view );
+ $data['result'] = 'success';
+
+ wp_send_json( $data );
+ }
+
+ /**
+ * Updates shipping method in WC session
+ *
+ * @param array $shipping_methods Array of selected shipping methods ids.
+ */
+ public function update_shipping_method( $shipping_methods ) {
+ $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
+
+ if ( is_array( $shipping_methods ) ) {
+ foreach ( $shipping_methods as $i => $value ) {
+ $chosen_shipping_methods[ $i ] = wc_clean( $value );
+ }
+ }
+
+ WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
+ }
+
+ /**
+ * Gets the selected product data.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return array $data
+ */
+ public function ajax_get_selected_product_data() {
+ check_ajax_referer( 'wc-stripe-get-selected-product-data', 'security' );
+
+ try {
+ $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0;
+ $qty = ! isset( $_POST['qty'] ) ? 1 : apply_filters( 'woocommerce_add_to_cart_quantity', absint( $_POST['qty'] ), $product_id );
+ $addon_value = isset( $_POST['addon_value'] ) ? max( floatval( $_POST['addon_value'] ), 0 ) : 0;
+ $product = wc_get_product( $product_id );
+ $variation_id = null;
+
+ if ( ! is_a( $product, 'WC_Product' ) ) {
+ /* translators: 1) The product Id */
+ throw new Exception( sprintf( __( 'Product with the ID (%1$s) cannot be found.', 'woocommerce-gateway-stripe' ), $product_id ) );
+ }
+
+ if ( 'variable' === $product->get_type() && isset( $_POST['attributes'] ) ) {
+ $attributes = wc_clean( wp_unslash( $_POST['attributes'] ) );
+
+ $data_store = WC_Data_Store::load( 'product' );
+ $variation_id = $data_store->find_matching_product_variation( $product, $attributes );
+
+ if ( ! empty( $variation_id ) ) {
+ $product = wc_get_product( $variation_id );
+ }
+ }
+
+ // Force quantity to 1 if sold individually and check for existing item in cart.
+ if ( $product->is_sold_individually() ) {
+ $qty = apply_filters( 'wc_stripe_payment_request_add_to_cart_sold_individually_quantity', 1, $qty, $product_id, $variation_id );
+ }
+
+ if ( ! $product->has_enough_stock( $qty ) ) {
+ /* translators: 1) product name 2) quantity in stock */
+ throw new Exception( sprintf( __( 'You cannot add that amount of "%1$s"; to the cart because there is not enough stock (%2$s remaining).', 'woocommerce-gateway-stripe' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity(), $product ) ) );
+ }
+
+ $total = $qty * $this->get_product_price( $product ) + $addon_value;
+
+ $quantity_label = 1 < $qty ? ' (x' . $qty . ')' : '';
+
+ $data = [];
+ $items = [];
+
+ $items[] = [
+ 'label' => $product->get_name() . $quantity_label,
+ 'amount' => WC_Stripe_Helper::get_stripe_amount( $total ),
+ ];
+
+ if ( wc_tax_enabled() ) {
+ $items[] = [
+ 'label' => __( 'Tax', 'woocommerce-gateway-stripe' ),
+ 'amount' => 0,
+ 'pending' => true,
+ ];
+ }
+
+ if ( wc_shipping_enabled() && $product->needs_shipping() ) {
+ $items[] = [
+ 'label' => __( 'Shipping', 'woocommerce-gateway-stripe' ),
+ 'amount' => 0,
+ 'pending' => true,
+ ];
+
+ $data['shippingOptions'] = [
+ 'id' => 'pending',
+ 'label' => __( 'Pending', 'woocommerce-gateway-stripe' ),
+ 'detail' => '',
+ 'amount' => 0,
+ ];
+ }
+
+ $data['displayItems'] = $items;
+ $data['total'] = [
+ 'label' => $this->total_label,
+ 'amount' => WC_Stripe_Helper::get_stripe_amount( $total ),
+ ];
+
+ $data['requestShipping'] = ( wc_shipping_enabled() && $product->needs_shipping() );
+ $data['currency'] = strtolower( get_woocommerce_currency() );
+ $data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 );
+
+ wp_send_json( $data );
+ } catch ( Exception $e ) {
+ wp_send_json( [ 'error' => wp_strip_all_tags( $e->getMessage() ) ] );
+ }
+ }
+
+ /**
+ * Adds the current product to the cart. Used on product detail page.
+ *
+ * @since 4.0.0
+ * @version 4.0.0
+ * @return array $data
+ */
+ public function ajax_add_to_cart() {
+ check_ajax_referer( 'wc-stripe-add-to-cart', 'security' );
+
+ if ( ! defined( 'WOOCOMMERCE_CART' ) ) {
+ define( 'WOOCOMMERCE_CART', true );
+ }
+
+ WC()->shipping->reset_shipping();
+
+ $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0;
+ $qty = ! isset( $_POST['qty'] ) ? 1 : absint( $_POST['qty'] );
+ $product = wc_get_product( $product_id );
+ $product_type = $product->get_type();
+
+ // First empty the cart to prevent wrong calculation.
+ WC()->cart->empty_cart();
+
+ if ( ( 'variable' === $product_type || 'variable-subscription' === $product_type ) && isset( $_POST['attributes'] ) ) {
+ $attributes = wc_clean( wp_unslash( $_POST['attributes'] ) );
+
+ $data_store = WC_Data_Store::load( 'product' );
+ $variation_id = $data_store->find_matching_product_variation( $product, $attributes );
+
+ WC()->cart->add_to_cart( $product->get_id(), $qty, $variation_id, $attributes );
+ }
+
+ if ( 'simple' === $product_type || 'subscription' === $product_type ) {
+ WC()->cart->add_to_cart( $product->get_id(), $qty );
+ }
+
+ WC()->cart->calculate_totals();
+
+ $data = [];
+ $data += $this->build_display_items();
+ $data['result'] = 'success';
+
+ wp_send_json( $data );
+ }
+
+ /**
+ * Normalizes billing and shipping state fields.
+ *
+ * @since 4.0.0
+ * @version 5.1.0
+ */
+ public function normalize_state() {
+ $billing_country = ! empty( $_POST['billing_country'] ) ? wc_clean( wp_unslash( $_POST['billing_country'] ) ) : '';
+ $shipping_country = ! empty( $_POST['shipping_country'] ) ? wc_clean( wp_unslash( $_POST['shipping_country'] ) ) : '';
+ $billing_state = ! empty( $_POST['billing_state'] ) ? wc_clean( wp_unslash( $_POST['billing_state'] ) ) : '';
+ $shipping_state = ! empty( $_POST['shipping_state'] ) ? wc_clean( wp_unslash( $_POST['shipping_state'] ) ) : '';
+
+ // Due to a bug in Apple Pay, the "Region" part of a Hong Kong address is delivered in
+ // `shipping_postcode`, so we need some special case handling for that. According to
+ // our sources at Apple Pay people will sometimes use the district or even sub-district
+ // for this value. As such we check against all regions, districts, and sub-districts
+ // with both English and Mandarin spelling.
+ //
+ // @reykjalin: The check here is quite elaborate in an attempt to make sure this doesn't break once
+ // Apple Pay fixes the bug that causes address values to be in the wrong place. Because of that the
+ // algorithm becomes:
+ // 1. Use the supplied state if it's valid (in case Apple Pay bug is fixed)
+ // 2. Use the value supplied in the postcode if it's a valid HK region (equivalent to a WC state).
+ // 3. Fall back to the value supplied in the state. This will likely cause a validation error, in
+ // which case a merchant can reach out to us so we can either: 1) add whatever the customer used
+ // as a state to our list of valid states; or 2) let them know the customer must spell the state
+ // in some way that matches our list of valid states.
+ //
+ // @reykjalin: This HK specific sanitazation *should be removed* once Apple Pay fix
+ // the address bug. More info on that in pc4etw-bY-p2.
+ if ( 'HK' === $billing_country ) {
+ include_once WC_STRIPE_PLUGIN_PATH . '/includes/constants/class-wc-stripe-hong-kong-states.php';
+
+ if ( ! WC_Stripe_Hong_Kong_States::is_valid_state( strtolower( $billing_state ) ) ) {
+ $billing_postcode = ! empty( $_POST['billing_postcode'] ) ? wc_clean( wp_unslash( $_POST['billing_postcode'] ) ) : '';
+ if ( WC_Stripe_Hong_Kong_States::is_valid_state( strtolower( $billing_postcode ) ) ) {
+ $billing_state = $billing_postcode;
+ }
+ }
+ }
+ if ( 'HK' === $shipping_country ) {
+ include_once WC_STRIPE_PLUGIN_PATH . '/includes/constants/class-wc-stripe-hong-kong-states.php';
+
+ if ( ! WC_Stripe_Hong_Kong_States::is_valid_state( strtolower( $shipping_state ) ) ) {
+ $shipping_postcode = ! empty( $_POST['shipping_postcode'] ) ? wc_clean( wp_unslash( $_POST['shipping_postcode'] ) ) : '';
+ if ( WC_Stripe_Hong_Kong_States::is_valid_state( strtolower( $shipping_postcode ) ) ) {
+ $shipping_state = $shipping_postcode;
+ }
+ }
+ }
+
+ // Finally we normalize the state value we want to process.
+ if ( $billing_state && $billing_country ) {
+ $_POST['billing_state'] = $this->get_normalized_state( $billing_state, $billing_country );
+ }
+
+ if ( $shipping_state && $shipping_country ) {
+ $_POST['shipping_state'] = $this->get_normalized_state( $shipping_state, $shipping_country );
+ }
+ }
+
+ /**
+ * Checks if given state is normalized.
+ *
+ * @since 5.1.0
+ *
+ * @param string $state State.
+ * @param string $country Two-letter country code.
+ *
+ * @return bool Whether state is normalized or not.
+ */
+ public function is_normalized_state( $state, $country ) {
+ $wc_states = WC()->countries->get_states( $country );
+ return (
+ is_array( $wc_states ) &&
+ in_array( $state, array_keys( $wc_states ), true )
+ );
+ }
+
+ /**
+ * Sanitize string for comparison.
+ *
+ * @since 5.1.0
+ *
+ * @param string $string String to be sanitized.
+ *
+ * @return string The sanitized string.
+ */
+ public function sanitize_string( $string ) {
+ return trim( wc_strtolower( remove_accents( $string ) ) );
+ }
+
+ /**
+ * Get normalized state from Payment Request API dropdown list of states.
+ *
+ * @since 5.1.0
+ *
+ * @param string $state Full state name or state code.
+ * @param string $country Two-letter country code.
+ *
+ * @return string Normalized state or original state input value.
+ */
+ public function get_normalized_state_from_pr_states( $state, $country ) {
+ // Include Payment Request API State list for compatibility with WC countries/states.
+ include_once WC_STRIPE_PLUGIN_PATH . '/includes/constants/class-wc-stripe-payment-request-button-states.php';
+ $pr_states = WC_Stripe_Payment_Request_Button_States::STATES;
+
+ if ( ! isset( $pr_states[ $country ] ) ) {
+ return $state;
+ }
+
+ foreach ( $pr_states[ $country ] as $wc_state_abbr => $pr_state ) {
+ $sanitized_state_string = $this->sanitize_string( $state );
+ // Checks if input state matches with Payment Request state code (0), name (1) or localName (2).
+ if (
+ ( ! empty( $pr_state[0] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[0] ) ) ||
+ ( ! empty( $pr_state[1] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[1] ) ) ||
+ ( ! empty( $pr_state[2] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[2] ) )
+ ) {
+ return $wc_state_abbr;
+ }
+ }
+
+ return $state;
+ }
+
+ /**
+ * Get normalized state from WooCommerce list of translated states.
+ *
+ * @since 5.1.0
+ *
+ * @param string $state Full state name or state code.
+ * @param string $country Two-letter country code.
+ *
+ * @return string Normalized state or original state input value.
+ */
+ public function get_normalized_state_from_wc_states( $state, $country ) {
+ $wc_states = WC()->countries->get_states( $country );
+
+ if ( is_array( $wc_states ) ) {
+ foreach ( $wc_states as $wc_state_abbr => $wc_state_value ) {
+ if ( preg_match( '/' . preg_quote( $wc_state_value, '/' ) . '/i', $state ) ) {
+ return $wc_state_abbr;
+ }
+ }
+ }
+
+ return $state;
+ }
+
+ /**
+ * Gets the normalized state/county field because in some
+ * cases, the state/county field is formatted differently from
+ * what WC is expecting and throws an error. An example
+ * for Ireland, the county dropdown in Chrome shows "Co. Clare" format.
+ *
+ * @since 5.0.0
+ * @version 5.1.0
+ *
+ * @param string $state Full state name or an already normalized abbreviation.
+ * @param string $country Two-letter country code.
+ *
+ * @return string Normalized state abbreviation.
+ */
+ public function get_normalized_state( $state, $country ) {
+ // If it's empty or already normalized, skip.
+ if ( ! $state || $this->is_normalized_state( $state, $country ) ) {
+ return $state;
+ }
+
+ // Try to match state from the Payment Request API list of states.
+ $state = $this->get_normalized_state_from_pr_states( $state, $country );
+
+ // If it's normalized, return.
+ if ( $this->is_normalized_state( $state, $country ) ) {
+ return $state;
+ }
+
+ // If the above doesn't work, fallback to matching against the list of translated
+ // states from WooCommerce.
+ return $this->get_normalized_state_from_wc_states( $state, $country );
+ }
+
+ /**
+ * The Payment Request API provides its own validation for the address form.
+ * For some countries, it might not provide a state field, so we need to return a more descriptive
+ * error message, indicating that the Payment Request button is not supported for that country.
+ *
+ * @since 5.1.0
+ */
+ public function validate_state() {
+ $wc_checkout = WC_Checkout::instance();
+ $posted_data = $wc_checkout->get_posted_data();
+ $checkout_fields = $wc_checkout->get_checkout_fields();
+ $countries = WC()->countries->get_countries();
+
+ $is_supported = true;
+ // Checks if billing state is missing and is required.
+ if ( ! empty( $checkout_fields['billing']['billing_state']['required'] ) && '' === $posted_data['billing_state'] ) {
+ $is_supported = false;
+ }
+
+ // Checks if shipping state is missing and is required.
+ if ( WC()->cart->needs_shipping_address() && ! empty( $checkout_fields['shipping']['shipping_state']['required'] ) && '' === $posted_data['shipping_state'] ) {
+ $is_supported = false;
+ }
+
+ if ( ! $is_supported ) {
+ wc_add_notice(
+ sprintf(
+ /* translators: 1) country. */
+ __( 'The Payment Request button is not supported in %1$s because some required fields couldn\'t be verified. Please proceed to the checkout page and try again.', 'woocommerce-gateway-stripe' ),
+ isset( $countries[ $posted_data['billing_country'] ] ) ? $countries[ $posted_data['billing_country'] ] : $posted_data['billing_country']
+ ),
+ 'error'
+ );
+ }
+ }
+
+ /**
+ * Create order. Security is handled by WC.
+ *
+ * @since 3.1.0
+ * @version 5.1.0
+ */
+ public function ajax_create_order() {
+ if ( WC()->cart->is_empty() ) {
+ wp_send_json_error( __( 'Empty cart', 'woocommerce-gateway-stripe' ) );
+ }
+
+ if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) {
+ define( 'WOOCOMMERCE_CHECKOUT', true );
+ }
+
+ // Normalizes billing and shipping state values.
+ $this->normalize_state();
+
+ // In case the state is required, but is missing, add a more descriptive error notice.
+ $this->validate_state();
+
+ WC()->checkout()->process_checkout();
+
+ die( 0 );
+ }
+
+ /**
+ * Calculate and set shipping method.
+ *
+ * @param array $address Shipping address.
+ *
+ * @since 3.1.0
+ * @version 5.0.0
+ */
+ protected function calculate_shipping( $address = [] ) {
+ $country = $address['country'];
+ $state = $address['state'];
+ $postcode = $address['postcode'];
+ $city = $address['city'];
+ $address_1 = $address['address'];
+ $address_2 = $address['address_2'];
+
+ // Normalizes state to calculate shipping zones.
+ $state = $this->get_normalized_state( $state, $country );
+
+ // Normalizes postal code in case of redacted data from Apple Pay.
+ $postcode = $this->get_normalized_postal_code( $postcode, $country );
+
+ WC()->shipping->reset_shipping();
+
+ if ( $postcode && WC_Validation::is_postcode( $postcode, $country ) ) {
+ $postcode = wc_format_postcode( $postcode, $country );
+ }
+
+ if ( $country ) {
+ WC()->customer->set_location( $country, $state, $postcode, $city );
+ WC()->customer->set_shipping_location( $country, $state, $postcode, $city );
+ } else {
+ WC()->customer->set_billing_address_to_base();
+ WC()->customer->set_shipping_address_to_base();
+ }
+
+ WC()->customer->set_calculated_shipping( true );
+ WC()->customer->save();
+
+ $packages = [];
+
+ $packages[0]['contents'] = WC()->cart->get_cart();
+ $packages[0]['contents_cost'] = 0;
+ $packages[0]['applied_coupons'] = WC()->cart->applied_coupons;
+ $packages[0]['user']['ID'] = get_current_user_id();
+ $packages[0]['destination']['country'] = $country;
+ $packages[0]['destination']['state'] = $state;
+ $packages[0]['destination']['postcode'] = $postcode;
+ $packages[0]['destination']['city'] = $city;
+ $packages[0]['destination']['address'] = $address_1;
+ $packages[0]['destination']['address_2'] = $address_2;
+
+ foreach ( WC()->cart->get_cart() as $item ) {
+ if ( $item['data']->needs_shipping() ) {
+ if ( isset( $item['line_total'] ) ) {
+ $packages[0]['contents_cost'] += $item['line_total'];
+ }
+ }
+ }
+
+ $packages = apply_filters( 'woocommerce_cart_shipping_packages', $packages );
+
+ WC()->shipping->calculate_shipping( $packages );
+ }
+
+ /**
+ * The settings for the `button` attribute - they depend on the "settings redesign" flag value.
+ *
+ * @return array
+ */
+ public function get_button_settings() {
+ // it would be DRYer to use `array_merge`,
+ // but I thought that this approach might be more straightforward to clean up when we remove the feature flag code.
+ $button_type = $this->get_button_type();
+ if ( WC_Stripe_Feature_Flags::is_upe_preview_enabled() ) {
+ return [
+ 'type' => $button_type,
+ 'theme' => $this->get_button_theme(),
+ 'height' => $this->get_button_height(),
+ // Default format is en_US.
+ 'locale' => apply_filters( 'wc_stripe_payment_request_button_locale', substr( get_locale(), 0, 2 ) ),
+ 'branded_type' => 'default' === $button_type ? 'short' : 'long',
+ // these values are no longer applicable - all the JS relying on them can be removed.
+ 'css_selector' => '',
+ 'label' => '',
+ 'is_custom' => false,
+ 'is_branded' => false,
+ ];
+ }
+
+ return [
+ 'type' => $button_type,
+ 'theme' => $this->get_button_theme(),
+ 'height' => $this->get_button_height(),
+ 'locale' => apply_filters( 'wc_stripe_payment_request_button_locale', substr( get_locale(), 0, 2 ) ),
+ // Default format is en_US.
+ 'is_custom' => $this->is_custom_button(),
+ 'is_branded' => $this->is_branded_button(),
+ 'css_selector' => $this->custom_button_selector(),
+ 'branded_type' => $this->get_button_branded_type(),
+ ];
+ }
+
+ /**
+ * Builds the shippings methods to pass to Payment Request
+ *
+ * @since 3.1.0
+ * @version 4.0.0
+ */
+ protected function build_shipping_methods( $shipping_methods ) {
+ if ( empty( $shipping_methods ) ) {
+ return [];
+ }
+
+ $shipping = [];
+
+ foreach ( $shipping_methods as $method ) {
+ $shipping[] = [
+ 'id' => $method['id'],
+ 'label' => $method['label'],
+ 'detail' => '',
+ 'amount' => WC_Stripe_Helper::get_stripe_amount( $method['amount']['value'] ),
+ ];
+ }
+
+ return $shipping;
+ }
+
+ /**
+ * Builds the line items to pass to Payment Request
+ *
+ * @since 3.1.0
+ * @version 4.0.0
+ */
+ protected function build_display_items( $itemized_display_items = false ) {
+ if ( ! defined( 'WOOCOMMERCE_CART' ) ) {
+ define( 'WOOCOMMERCE_CART', true );
+ }
+
+ $items = [];
+ $lines = [];
+ $subtotal = 0;
+ $discounts = 0;
+ $display_items = ! apply_filters( 'wc_stripe_payment_request_hide_itemization', true ) || $itemized_display_items;
+
+ foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
+ $subtotal += $cart_item['line_subtotal'];
+ $amount = $cart_item['line_subtotal'];
+ $quantity_label = 1 < $cart_item['quantity'] ? ' (x' . $cart_item['quantity'] . ')' : '';
+ $product_name = $cart_item['data']->get_name();
+
+ $lines[] = [
+ 'label' => $product_name . $quantity_label,
+ 'amount' => WC_Stripe_Helper::get_stripe_amount( $amount ),
+ ];
+ }
+
+ if ( $display_items ) {
+ $items = array_merge( $items, $lines );
+ } else {
+ // Default show only subtotal instead of itemization.
+
+ $items[] = [
+ 'label' => 'Subtotal',
+ 'amount' => WC_Stripe_Helper::get_stripe_amount( $subtotal ),
+ ];
+ }
+
+ if ( version_compare( WC_VERSION, '3.2', '<' ) ) {
+ $discounts = wc_format_decimal( WC()->cart->get_cart_discount_total(), WC()->cart->dp );
+ } else {
+ $applied_coupons = array_values( WC()->cart->get_coupon_discount_totals() );
+
+ foreach ( $applied_coupons as $amount ) {
+ $discounts += (float) $amount;
+ }
+ }
+
+ $discounts = wc_format_decimal( $discounts, WC()->cart->dp );
+ $tax = wc_format_decimal( WC()->cart->tax_total + WC()->cart->shipping_tax_total, WC()->cart->dp );
+ $shipping = wc_format_decimal( WC()->cart->shipping_total, WC()->cart->dp );
+ $items_total = wc_format_decimal( WC()->cart->cart_contents_total, WC()->cart->dp ) + $discounts;
+ $order_total = version_compare( WC_VERSION, '3.2', '<' ) ? wc_format_decimal( $items_total + $tax + $shipping - $discounts, WC()->cart->dp ) : WC()->cart->get_total( false );
+
+ if ( wc_tax_enabled() ) {
+ $items[] = [
+ 'label' => esc_html( __( 'Tax', 'woocommerce-gateway-stripe' ) ),
+ 'amount' => WC_Stripe_Helper::get_stripe_amount( $tax ),
+ ];
+ }
+
+ if ( WC()->cart->needs_shipping() ) {
+ $items[] = [
+ 'label' => esc_html( __( 'Shipping', 'woocommerce-gateway-stripe' ) ),
+ 'amount' => WC_Stripe_Helper::get_stripe_amount( $shipping ),
+ ];
+ }
+
+ if ( WC()->cart->has_discount() ) {
+ $items[] = [
+ 'label' => esc_html( __( 'Discount', 'woocommerce-gateway-stripe' ) ),
+ 'amount' => WC_Stripe_Helper::get_stripe_amount( $discounts ),
+ ];
+ }
+
+ if ( version_compare( WC_VERSION, '3.2', '<' ) ) {
+ $cart_fees = WC()->cart->fees;
+ } else {
+ $cart_fees = WC()->cart->get_fees();
+ }
+
+ // Include fees and taxes as display items.
+ foreach ( $cart_fees as $key => $fee ) {
+ $items[] = [
+ 'label' => $fee->name,
+ 'amount' => WC_Stripe_Helper::get_stripe_amount( $fee->amount ),
+ ];
+ }
+
+ return [
+ 'displayItems' => $items,
+ 'total' => [
+ 'label' => $this->total_label,
+ 'amount' => max( 0, apply_filters( 'woocommerce_stripe_calculated_total', WC_Stripe_Helper::get_stripe_amount( $order_total ), $order_total, WC()->cart ) ),
+ 'pending' => false,
+ ],
+ ];
+ }
+
+ /**
+ * Settings array for the user authentication dialog and redirection.
+ *
+ * @since 5.3.0
+ * @version 5.3.0
+ *
+ * @return array
+ */
+ public function get_login_confirmation_settings() {
+ if ( is_user_logged_in() || ! $this->is_authentication_required() ) {
+ return false;
+ }
+
+ /* translators: The text encapsulated in `**` can be replaced with "Apple Pay" or "Google Pay". Please translate this text, but don't remove the `**`. */
+ $message = __( 'To complete your transaction with **the selected payment method**, you must log in or create an account with our site.', 'woocommerce-gateway-stripe' );
+ $redirect_url = add_query_arg(
+ [
+ '_wpnonce' => wp_create_nonce( 'wc-stripe-set-redirect-url' ),
+ 'wc_stripe_payment_request_redirect_url' => rawurlencode( home_url( add_query_arg( [] ) ) ), // Current URL to redirect to after login.
+ ],
+ home_url()
+ );
+
+ return [
+ 'message' => $message,
+ 'redirect_url' => $redirect_url,
+ ];
+ }
+
+ public function get_button_locations() {
+ // If the locations have not been set return the default setting.
+ if ( ! isset( $this->stripe_settings['payment_request_button_locations'] ) ) {
+ return [ 'product', 'cart' ];
+ }
+
+ // If all locations are removed through the settings UI the location config will be set to
+ // an empty string "". If that's the case (and if the settings are not an array for any
+ // other reason) we should return an empty array.
+ if ( ! is_array( $this->stripe_settings['payment_request_button_locations'] ) ) {
+ return [];
+ }
+
+ return $this->stripe_settings['payment_request_button_locations'];
+ }
+}
diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php
new file mode 100644
index 0000000..d188589
--- /dev/null
+++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php
@@ -0,0 +1,1355 @@
+<?php
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+* Class that handles UPE payment method.
+*
+* @extends WC_Gateway_Stripe
+*
+* @since 5.5.0
+*/
+class WC_Stripe_UPE_Payment_Gateway extends WC_Gateway_Stripe {
+
+ const ID = 'stripe';
+
+ /**
+ * Upe Available Methods
+ *
+ * @type WC_Stripe_UPE_Payment_Method[]
+ */
+ const UPE_AVAILABLE_METHODS = [
+ WC_Stripe_UPE_Payment_Method_CC::class,
+ WC_Stripe_UPE_Payment_Method_Giropay::class,
+ WC_Stripe_UPE_Payment_Method_Eps::class,
+ WC_Stripe_UPE_Payment_Method_Bancontact::class,
+ WC_Stripe_UPE_Payment_Method_Boleto::class,
+ WC_Stripe_UPE_Payment_Method_Ideal::class,
+ WC_Stripe_UPE_Payment_Method_Oxxo::class,
+ WC_Stripe_UPE_Payment_Method_Sepa::class,
+ WC_Stripe_UPE_Payment_Method_P24::class,
+ WC_Stripe_UPE_Payment_Method_Sofort::class,
+ WC_Stripe_UPE_Payment_Method_Link::class,
+ ];
+
+ const UPE_APPEARANCE_TRANSIENT = 'wc_stripe_upe_appearance';
+
+ const WC_BLOCKS_UPE_APPEARANCE_TRANSIENT = 'wc_stripe_wc_blocks_upe_appearance';
+
+ /**
+ * Stripe intents that are treated as successfully created.
+ *
+ * @type array
+ */
+ const SUCCESSFUL_INTENT_STATUS = [ 'succeeded', 'requires_capture', 'processing' ];
+
+ /**
+ * Notices (array)
+ *
+ * @var array
+ */
+ public $notices = [];
+
+ /**
+ * Is test mode active?
+ *
+ * @var bool
+ */
+ public $testmode;
+
+ /**
+ * Alternate credit card statement name
+ *
+ * @var bool
+ */
+ public $statement_descriptor;
+
+ /**
+ * Are saved cards enabled
+ *
+ * @var bool
+ */
+ public $saved_cards;
+
+ /**
+ * API access secret key
+ *
+ * @var string
+ */
+ public $secret_key;
+
+ /**
+ * Api access publishable key
+ *
+ * @var string
+ */
+ public $publishable_key;
+
+ /**
+ * Array mapping payment method string IDs to classes
+ *
+ * @var WC_Stripe_UPE_Payment_Method[]
+ */
+ public $payment_methods = [];
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ $this->id = self::ID;
+ $this->method_title = __( 'Stripe', 'woocommerce-gateway-stripe' );
+ /* translators: link */
+ $this->method_description = __( 'Accept debit and credit cards in 135+ currencies, methods such as SEPA, and one-touch checkout with Apple Pay.', 'woocommerce-gateway-stripe' );
+ $this->has_fields = true;
+ $this->supports = [
+ 'products',
+ 'refunds',
+ 'tokenization',
+ 'add_payment_method',
+ ];
+
+ $this->payment_methods = [];
+ foreach ( self::UPE_AVAILABLE_METHODS as $payment_method_class ) {
+ $payment_method = new $payment_method_class();
+ $this->payment_methods[ $payment_method->get_id() ] = $payment_method;
+ }
+
+ // Load the form fields.
+ $this->init_form_fields();
+
+ // Load the settings.
+ $this->init_settings();
+
+ // Check if subscriptions are enabled and add support for them.
+ $this->maybe_init_subscriptions();
+
+ // Check if pre-orders are enabled and add support for them.
+ $this->maybe_init_pre_orders();
+
+ $main_settings = get_option( 'woocommerce_stripe_settings' );
+ $this->title = ! empty( $this->get_option( 'title_upe' ) ) ? $this->get_option( 'title_upe' ) : $this->form_fields['title_upe']['default'];
+ $this->description = '';
+ $this->enabled = $this->get_option( 'enabled' );
+ $this->saved_cards = 'yes' === $this->get_option( 'saved_cards' );
+ $this->testmode = ! empty( $main_settings['testmode'] ) && 'yes' === $main_settings['testmode'];
+ $this->publishable_key = ! empty( $main_settings['publishable_key'] ) ? $main_settings['publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['secret_key'] ) ? $main_settings['secret_key'] : '';
+ $this->statement_descriptor = ! empty( $main_settings['statement_descriptor'] ) ? $main_settings['statement_descriptor'] : '';
+
+ $enabled_at_checkout_payment_methods = $this->get_upe_enabled_at_checkout_payment_method_ids();
+ if ( count( $enabled_at_checkout_payment_methods ) === 1 ) {
+ $this->title = $this->payment_methods[ $enabled_at_checkout_payment_methods[0] ]->get_title();
+ }
+
+ // When feature flags are enabled, title shows the count of enabled payment methods in settings page only.
+ if ( WC_Stripe_Feature_Flags::is_upe_checkout_enabled() && WC_Stripe_Feature_Flags::is_upe_preview_enabled() && isset( $_GET['page'] ) && 'wc-settings' === $_GET['page'] ) {
+ $enabled_payment_methods_count = count( $this->get_upe_enabled_payment_method_ids() );
+ $this->title = $enabled_payment_methods_count ?
+ /* translators: $1. Count of enabled payment methods. */
+ sprintf( _n( '%d payment method', '%d payment methods', $enabled_payment_methods_count, 'woocommerce-gateway-stripe' ), $enabled_payment_methods_count )
+ : $this->method_title;
+ }
+
+ if ( $this->testmode ) {
+ $this->publishable_key = ! empty( $main_settings['test_publishable_key'] ) ? $main_settings['test_publishable_key'] : '';
+ $this->secret_key = ! empty( $main_settings['test_secret_key'] ) ? $main_settings['test_secret_key'] : '';
+ }
+
+ add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, [ $this, 'process_admin_options' ] );
+ add_action( 'wp_enqueue_scripts', [ $this, 'payment_scripts' ] );
+
+ // Needed for 3DS compatibility when checking out with PRBs..
+ // Copied from WC_Gateway_Stripe::__construct().
+ add_filter( 'woocommerce_payment_successful_result', [ $this, 'modify_successful_payment_result' ], 99999, 2 );
+ }
+
+ /**
+ * Hides refund through stripe when payment method does not allow refund
+ *
+ * @param WC_Order $order
+ *
+ * @return array|bool
+ */
+ public function can_refund_order( $order ) {
+ $upe_payment_type = $order->get_meta( '_stripe_upe_payment_type' );
+
+ if ( ! $upe_payment_type ) {
+ return true;
+ }
+
+ return $this->payment_methods[ $upe_payment_type ]->can_refund_via_stripe();
+ }
+
+ /**
+ * Return the gateway icon - None for UPE.
+ */
+ public function get_icon() {
+ return apply_filters( 'woocommerce_gateway_icon', null, $this->id );
+ }
+
+ /**
+ * Initialize Gateway Settings Form Fields.
+ */
+ public function init_form_fields() {
+ $this->form_fields = require WC_STRIPE_PLUGIN_PATH . '/includes/admin/stripe-settings.php';
+ unset( $this->form_fields['inline_cc_form'] );
+ unset( $this->form_fields['title'] );
+ unset( $this->form_fields['description'] );
+ }
+
+ /**
+ * Outputs scripts used for stripe payment
+ */
+ public function payment_scripts() {
+ if (
+ ! is_product()
+ && ! WC_Stripe_Helper::has_cart_or_checkout_on_current_page()
+ && ! isset( $_GET['pay_for_order'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ && ! is_add_payment_method_page() ) {
+ return;
+ }
+
+ if ( is_product() && ! WC_Stripe_Helper::should_load_scripts_on_product_page() ) {
+ return;
+ }
+
+ if ( is_cart() && ! WC_Stripe_Helper::should_load_scripts_on_cart_page() ) {
+ return;
+ }
+
+ $asset_path = WC_STRIPE_PLUGIN_PATH . '/build/checkout_upe.asset.php';
+ $version = WC_STRIPE_VERSION;
+ $dependencies = [];
+ if ( file_exists( $asset_path ) ) {
+ $asset = require $asset_path;
+ $version = is_array( $asset ) && isset( $asset['version'] )
+ ? $asset['version']
+ : $version;
+ $dependencies = is_array( $asset ) && isset( $asset['dependencies'] )
+ ? $asset['dependencies']
+ : $dependencies;
+ }
+
+ wp_register_script(
+ 'stripe',
+ 'https://js.stripe.com/v3/',
+ [],
+ '3.0',
+ true
+ );
+
+ wp_register_script(
+ 'wc-stripe-upe-classic',
+ WC_STRIPE_PLUGIN_URL . '/build/upe_classic.js',
+ array_merge( [ 'stripe', 'wc-checkout' ], $dependencies ),
+ $version,
+ true
+ );
+ wp_set_script_translations(
+ 'wc-stripe-upe-classic',
+ 'woocommerce-gateway-stripe'
+ );
+
+ wp_localize_script(
+ 'wc-stripe-upe-classic',
+ 'wc_stripe_upe_params',
+ apply_filters( 'wc_stripe_upe_params', $this->javascript_params() )
+ );
+
+ wp_register_style(
+ 'wc-stripe-upe-classic',
+ WC_STRIPE_PLUGIN_URL . '/build/upe_classic.css',
+ [],
+ $version
+ );
+
+ wp_enqueue_script( 'wc-stripe-upe-classic' );
+ wp_enqueue_style( 'wc-stripe-upe-classic' );
+
+ wp_register_style( 'stripelink_styles', plugins_url( 'assets/css/stripe-link.css', WC_STRIPE_MAIN_FILE ), [], WC_STRIPE_VERSION );
+ wp_enqueue_style( 'stripelink_styles' );
+ }
+
+ /**
+ * Returns the JavaScript configuration object used on the product, cart, and checkout pages.
+ *
+ * @return array The configuration object to be loaded to JS.
+ */
+ public function javascript_params() {
+ global $wp;
+
+ $stripe_params = [
+ 'title' => $this->title,
+ 'isUPEEnabled' => true,
+ 'key' => $this->publishable_key,
+ 'locale' => WC_Stripe_Helper::convert_wc_locale_to_stripe_locale( get_locale() ),
+ ];
+
+ $enabled_billing_fields = [];
+ foreach ( WC()->checkout()->get_checkout_fields( 'billing' ) as $billing_field => $billing_field_options ) {
+ if ( ! isset( $billing_field_options['enabled'] ) || $billing_field_options['enabled'] ) {
+ $enabled_billing_fields[] = $billing_field;
+ }
+ }
+
+ $stripe_params['isCheckout'] = is_checkout() && empty( $_GET['pay_for_order'] ); // wpcs: csrf ok.
+ $stripe_params['return_url'] = $this->get_stripe_return_url();
+ $stripe_params['ajax_url'] = WC_AJAX::get_endpoint( '%%endpoint%%' );
+ $stripe_params['createPaymentIntentNonce'] = wp_create_nonce( 'wc_stripe_create_payment_intent_nonce' );
+ $stripe_params['updatePaymentIntentNonce'] = wp_create_nonce( 'wc_stripe_update_payment_intent_nonce' );
+ $stripe_params['createSetupIntentNonce'] = wp_create_nonce( 'wc_stripe_create_setup_intent_nonce' );
+ $stripe_params['updateFailedOrderNonce'] = wp_create_nonce( 'wc_stripe_update_failed_order_nonce' );
+ $stripe_params['upeAppearance'] = get_transient( self::UPE_APPEARANCE_TRANSIENT );
+ $stripe_params['wcBlocksUPEAppearance'] = get_transient( self::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT );
+ $stripe_params['saveUPEAppearanceNonce'] = wp_create_nonce( 'wc_stripe_save_upe_appearance_nonce' );
+ $stripe_params['paymentMethodsConfig'] = $this->get_enabled_payment_method_config();
+ $stripe_params['genericErrorMessage'] = __( 'There was a problem processing the payment. Please check your email inbox and refresh the page to try again.', 'woocommerce-gateway-stripe' );
+ $stripe_params['accountDescriptor'] = $this->statement_descriptor;
+ $stripe_params['addPaymentReturnURL'] = wc_get_account_endpoint_url( 'payment-methods' );
+ $stripe_params['enabledBillingFields'] = $enabled_billing_fields;
+
+ if ( is_wc_endpoint_url( 'order-pay' ) ) {
+ if ( $this->is_subscriptions_enabled() && $this->is_changing_payment_method_for_subscription() ) {
+ $stripe_params['isChangingPayment'] = true;
+ $stripe_params['addPaymentReturnURL'] = esc_url_raw( home_url( add_query_arg( [] ) ) );
+
+ if ( $this->is_setup_intent_success_creation_redirection() && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wpnonce'] ) ) ) ) {
+ $setup_intent_id = isset( $_GET['setup_intent'] ) ? wc_clean( wp_unslash( $_GET['setup_intent'] ) ) : '';
+ $token = $this->create_token_from_setup_intent( $setup_intent_id, wp_get_current_user() );
+ $stripe_params['newTokenFormId'] = '#wc-' . $token->get_gateway_id() . '-payment-token-' . $token->get_id();
+ }
+ return $stripe_params;
+ }
+
+ $order_id = absint( get_query_var( 'order-pay' ) );
+ $stripe_params['orderId'] = $order_id;
+ $stripe_params['isOrderPay'] = true;
+ $order = wc_get_order( $order_id );
+
+ if ( is_a( $order, 'WC_Order' ) ) {
+ $stripe_params['orderReturnURL'] = esc_url_raw(
+ add_query_arg(
+ [
+ 'order_id' => $order_id,
+ 'wc_payment_method' => self::ID,
+ '_wpnonce' => wp_create_nonce( 'wc_stripe_process_redirect_order_nonce' ),
+ ],
+ $this->get_return_url( $order )
+ )
+ );
+ }
+ }
+
+ // Pre-orders and free trial subscriptions don't require payments.
+ $stripe_params['isPaymentNeeded'] = $this->is_payment_needed( isset( $order_id ) ? $order_id : null );
+
+ return array_merge( $stripe_params, WC_Stripe_Helper::get_localized_messages() );
+ }
+
+ /**
+ * Gets payment method settings to pass to client scripts
+ *
+ * @return array
+ */
+ private function get_enabled_payment_method_config() {
+ $settings = [];
+ $enabled_payment_methods = $this->get_upe_enabled_at_checkout_payment_method_ids();
+
+ foreach ( $enabled_payment_methods as $payment_method ) {
+ $settings[ $payment_method ] = [
+ 'isReusable' => $this->payment_methods[ $payment_method ]->is_reusable(),
+ ];
+ }
+
+ return $settings;
+ }
+
+ /**
+ * Returns the list of enabled payment method types for UPE.
+ *
+ * @return string[]
+ */
+ public function get_upe_enabled_payment_method_ids() {
+ return $this->get_option( 'upe_checkout_experience_accepted_payments', [ 'card' ] );
+ }
+
+ /**
+ * Returns the list of enabled payment method types that will function with the current checkout.
+ *
+ * @param int|null $order_id
+ * @return string[]
+ */
+ public function get_upe_enabled_at_checkout_payment_method_ids( $order_id = null ) {
+ $is_automatic_capture_enabled = $this->is_automatic_capture_enabled();
+ $available_method_ids = [];
+ foreach ( $this->get_upe_enabled_payment_method_ids() as $payment_method_id ) {
+ if ( ! isset( $this->payment_methods[ $payment_method_id ] ) ) {
+ continue;
+ }
+
+ $method = $this->payment_methods[ $payment_method_id ];
+ if ( $method->is_enabled_at_checkout( $order_id ) === false ) {
+ continue;
+ }
+
+ if ( ! $is_automatic_capture_enabled && $method->requires_automatic_capture() ) {
+ continue;
+ }
+
+ $available_method_ids[] = $payment_method_id;
+ }
+
+ return $available_method_ids;
+ }
+
+ /**
+ * Returns the list of available payment method types for UPE.
+ * See https://stripe.com/docs/stripe-js/payment-element#web-create-payment-intent for a complete list.
+ *
+ * @return string[]
+ */
+ public function get_upe_available_payment_methods() {
+ $available_payment_methods = [];
+
+ foreach ( $this->payment_methods as $payment_method ) {
+ if ( ! $payment_method->is_available() ) {
+ continue;
+ }
+ $available_payment_methods[] = $payment_method->get_id();
+ }
+ return $available_payment_methods;
+ }
+
+ /**
+ * Renders the UPE input fields needed to get the user's payment information on the checkout page
+ */
+ public function payment_fields() {
+ try {
+ $display_tokenization = $this->supports( 'tokenization' ) && is_checkout();
+
+ // Output the form HTML.
+ ?>
+ <?php if ( ! empty( $this->get_description() ) ) : ?>
+ <p><?php echo wp_kses_post( $this->get_description() ); ?></p>
+ <?php endif; ?>
+
+ <?php if ( $this->testmode ) : ?>
+ <p class="testmode-info">
+ <?php
+ printf(
+ /* translators: 1) HTML strong open tag 2) HTML strong closing tag 3) HTML anchor open tag 2) HTML anchor closing tag */
+ esc_html__( '%1$sTest mode:%2$s use the test VISA card 4242424242424242 with any expiry date and CVC. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed %3$shere%4$s.', 'woocommerce-gateway-stripe' ),
+ '<strong>',
+ '</strong>',
+ '<a href="https://stripe.com/docs/testing" target="_blank">',
+ '</a>'
+ );
+ ?>
+ </p>
+ <?php endif; ?>
+
+ <?php
+ if ( $display_tokenization ) {
+ $this->tokenization_script();
+ $this->saved_payment_methods();
+ }
+ ?>
+
+ <fieldset id="wc-stripe-upe-form" class="wc-upe-form wc-payment-form">
+ <div id="wc-stripe-upe-element"></div>
+ <div id="wc-stripe-upe-errors" role="alert"></div>
+ <input id="wc-stripe-payment-method-upe" type="hidden" name="wc-stripe-payment-method-upe" />
+ <input id="wc_stripe_selected_upe_payment_type" type="hidden" name="wc_stripe_selected_upe_payment_type" />
+ </fieldset>
+ <?php
+ $methods_enabled_for_saved_payments = array_filter( $this->get_upe_enabled_payment_method_ids(), [ $this, 'is_enabled_for_saved_payments' ] );
+ if ( $this->is_saved_cards_enabled() && ! empty( $methods_enabled_for_saved_payments ) ) {
+ $force_save_payment = ( $display_tokenization && ! apply_filters( 'wc_stripe_display_save_payment_method_checkbox', $display_tokenization ) ) || is_add_payment_method_page();
+ if ( is_user_logged_in() ) {
+ $this->save_payment_method_checkbox( $force_save_payment );
+ }
+ }
+ } catch ( Exception $e ) {
+ // Output the error message.
+ WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
+ ?>
+ <div>
+ <?php
+ echo esc_html__( 'An error was encountered when preparing the payment form. Please try again later.', 'woocommerce-gateway-stripe' );
+ ?>
+ </div>
+ <?php
+ }
+ }
+
+ /**
+ * Process the payment for a given order.
+ *
+ * @param int $order_id Reference.
+ * @param bool $retry Should we retry on fail.
+ * @param bool $force_save_source Force save the payment source.
+ * @param mix $previous_error Any error message from previous request.
+ * @param bool $use_order_source Whether to use the source, which should already be attached to the order.
+ *
+ * @return array|null An array with result of payment and redirect URL, or nothing.
+ */
+ public function process_payment( $order_id, $retry = true, $force_save_source = false, $previous_error = false, $use_order_source = false ) {
+ if ( $this->maybe_change_subscription_payment_method( $order_id ) ) {
+ return $this->process_change_subscription_payment_method( $order_id );
+ }
+
+ if ( $this->is_using_saved_payment_method() ) {
+ return $this->process_payment_with_saved_payment_method( $order_id );
+ }
+
+ $payment_intent_id = isset( $_POST['wc_payment_intent_id'] ) ? wc_clean( wp_unslash( $_POST['wc_payment_intent_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ $order = wc_get_order( $order_id );
+ $payment_needed = $this->is_payment_needed( $order_id );
+ $save_payment_method = $this->has_subscription( $order_id ) || ! empty( $_POST[ 'wc-' . self::ID . '-new-payment-method' ] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ $selected_upe_payment_type = ! empty( $_POST['wc_stripe_selected_upe_payment_type'] ) ? wc_clean( wp_unslash( $_POST['wc_stripe_selected_upe_payment_type'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
+
+ $statement_descriptor = ! empty( $this->get_option( 'statement_descriptor' ) ) ? str_replace( "'", '', $this->get_option( 'statement_descriptor' ) ) : '';
+ $short_statement_descriptor = ! empty( $this->get_option( 'short_statement_descriptor' ) ) ? str_replace( "'", '', $this->get_option( 'short_statement_descriptor' ) ) : '';
+ $is_short_statement_descriptor_enabled = ! empty( $this->get_option( 'is_short_statement_descriptor_enabled' ) ) && 'yes' === $this->get_option( 'is_short_statement_descriptor_enabled' );
+ $descriptor = null;
+ if ( 'card' === $selected_upe_payment_type && $is_short_statement_descriptor_enabled && ! ( empty( $short_statement_descriptor ) && empty( $statement_descriptor ) ) ) {
+ // Use the shortened statement descriptor for card transactions only
+ $descriptor = WC_Stripe_Helper::get_dynamic_statement_descriptor( $short_statement_descriptor, $order, $statement_descriptor );
+ } elseif ( ! empty( $statement_descriptor ) ) {
+ $descriptor = WC_Stripe_Helper::clean_statement_descriptor( $statement_descriptor );
+ }
+
+ if ( $payment_intent_id ) {
+ if ( $payment_needed ) {
+ $amount = $order->get_total();
+ $currency = $order->get_currency();
+ $converted_amount = WC_Stripe_Helper::get_stripe_amount( $amount, $currency );
+
+ $request = [
+ 'amount' => $converted_amount,
+ 'currency' => $currency,
+ 'statement_descriptor' => $descriptor,
+ /* translators: 1) blog name 2) order number */
+ 'description' => sprintf( __( '%1$s - Order %2$s', 'woocommerce-gateway-stripe' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), $order->get_order_number() ),
+ ];
+
+ // Get user/customer for order.
+ $customer_id = $this->get_stripe_customer_id( $order );
+ if ( ! empty( $customer_id ) ) {
+ $request['customer'] = $customer_id;
+ } else {
+ $user = $this->get_user_from_order( $order );
+ $customer = new WC_Stripe_Customer( $user->ID );
+ $request['customer'] = $customer->update_or_create_customer();// Update customer or create customer if customer does not exist.
+ }
+
+ if ( '' !== $selected_upe_payment_type ) {
+ // Only update the payment_method_types if we have a reference to the payment type the customer selected.
+ $request['payment_method_types'] = [ $selected_upe_payment_type ];
+ if ( WC_Stripe_UPE_Payment_Method_CC::STRIPE_ID === $selected_upe_payment_type ) {
+ if ( in_array(
+ WC_Stripe_UPE_Payment_Method_Link::STRIPE_ID,
+ $this->get_upe_enabled_payment_method_ids(),
+ true
+ ) ) {
+ $request['payment_method_types'] = [
+ WC_Stripe_UPE_Payment_Method_CC::STRIPE_ID,
+ WC_Stripe_UPE_Payment_Method_Link::STRIPE_ID,
+ ];
+ }
+ }
+ $this->set_payment_method_title_for_order( $order, $selected_upe_payment_type );
+ if ( ! $this->payment_methods[ $selected_upe_payment_type ]->is_allowed_on_country( $order->get_billing_country() ) ) {
+ throw new \Exception( __( 'This payment method is not available on the selected country', 'woocommerce-gateway-stripe' ) );
+ }
+ }
+
+ if ( $save_payment_method ) {
+ $request['setup_future_usage'] = 'off_session';
+ }
+
+ $request['metadata'] = $this->get_metadata_from_order( $order );
+
+ WC_Stripe_Helper::add_payment_intent_to_order( $payment_intent_id, $order );
+ $order->update_status( 'pending', __( 'Awaiting payment.', 'woocommerce-gateway-stripe' ) );
+ $order->update_meta_data( '_stripe_upe_payment_type', $selected_upe_payment_type );
+ $order->save();
+
+ $this->stripe_request(
+ "payment_intents/$payment_intent_id",
+ $request,
+ $order
+ );
+ }
+ } else {
+ return parent::process_payment( $order_id, $retry, $force_save_source, $previous_error, $use_order_source );
+ }
+
+ return [
+ 'result' => 'success',
+ 'payment_needed' => $payment_needed,
+ 'order_id' => $order_id,
+ 'redirect_url' => wp_sanitize_redirect(
+ esc_url_raw(
+ add_query_arg(
+ [
+ 'order_id' => $order_id,
+ 'wc_payment_method' => self::ID,
+ '_wpnonce' => wp_create_nonce( 'wc_stripe_process_redirect_order_nonce' ),
+ 'save_payment_method' => $save_payment_method ? 'yes' : 'no',
+ ],
+ $this->get_return_url( $order )
+ )
+ )
+ ),
+ ];
+ }
+
+ /**
+ * Process payment using saved payment method.
+ * This follows WC_Gateway_Stripe::process_payment,
+ * but uses Payment Methods instead of Sources.
+ *
+ * @param int $order_id The order ID being processed.
+ * @param bool $can_retry Should we retry on fail.
+ */
+ public function process_payment_with_saved_payment_method( $order_id, $can_retry = true ) {
+ try {
+ $order = wc_get_order( $order_id );
+
+ if ( $this->maybe_process_pre_orders( $order_id ) ) {
+ return $this->process_pre_order( $order_id );
+ }
+
+ $token = WC_Stripe_Payment_Tokens::get_token_from_request( $_POST );
+ $payment_method = $this->stripe_request( 'payment_methods/' . $token->get_token(), [], null, 'GET' );
+ $prepared_payment_method = $this->prepare_payment_method( $payment_method );
+
+ $this->maybe_disallow_prepaid_card( $payment_method );
+ $this->save_payment_method_to_order( $order, $prepared_payment_method );
+
+ WC_Stripe_Logger::log( "Info: Begin processing payment with saved payment method for order $order_id for the amount of {$order->get_total()}" );
+
+ // If we are retrying request, maybe intent has been saved to order.
+ $intent = $this->get_intent_from_order( $order );
+
+ $enabled_payment_methods = array_filter( $this->get_upe_enabled_payment_method_ids(), [ $this, 'is_enabled_at_checkout' ] );
+ $payment_needed = $this->is_payment_needed( $order_id );
+
+ if ( $payment_needed ) {
+ // This will throw exception if not valid.
+ $this->validate_minimum_order_amount( $order );
+
+ $request_details = $this->generate_payment_request( $order, $prepared_payment_method );
+ $endpoint = false !== $intent ? "payment_intents/$intent->id" : 'payment_intents';
+ $request = [
+ 'payment_method' => $payment_method->id,
+ 'payment_method_types' => array_values( $enabled_payment_methods ),
+ 'amount' => WC_Stripe_Helper::get_stripe_amount( $order->get_total() ),
+ 'currency' => strtolower( $order->get_currency() ),
+ 'description' => $request_details['description'],
+ 'metadata' => $request_details['metadata'],
+ 'customer' => $payment_method->customer,
+ ];
+ if ( false === $intent ) {
+ $request['capture_method'] = ( 'true' === $request_details['capture'] ) ? 'automatic' : 'manual';
+ $request['confirm'] = 'true';
+ }
+
+ $intent = $this->stripe_request(
+ $endpoint,
+ $request,
+ $order
+ );
+ } else {
+ $endpoint = false !== $intent ? "setup_intents/$intent->id" : 'setup_intents';
+ $request = [
+ 'payment_method' => $payment_method->id,
+ 'payment_method_types' => array_values( $enabled_payment_methods ),
+ 'customer' => $payment_method->customer,
+ ];
+ if ( false === $intent ) {
+ $request['confirm'] = 'true';
+ // SEPA setup intents require mandate data.
+ if ( in_array( 'sepa_debit', array_values( $enabled_payment_methods ), true ) ) {
+ $request['mandate_data'] = [
+ 'customer_acceptance' => [
+ 'type' => 'online',
+ 'online' => [
+ 'ip_address' => WC_Geolocation::get_ip_address(),
+ 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '', // @codingStandardsIgnoreLine
+ ],
+ ],
+ ];
+ }
+ }
+
+ $intent = $this->stripe_request( $endpoint, $request );
+ }
+ $this->save_intent_to_order( $order, $intent );
+
+ if ( ! empty( $intent->error ) ) {
+ $this->maybe_remove_non_existent_customer( $intent->error, $order );
+
+ // We want to retry (apparently).
+ if ( $this->is_retryable_error( $intent->error ) ) {
+ return $this->retry_after_error( $intent, $order, $can_retry );
+ }
+
+ $this->throw_localized_message( $intent, $order );
+ }
+
+ if ( 'requires_action' === $intent->status || 'requires_confirmation' === $intent->status ) {
+ if ( isset( $intent->next_action->type ) && 'redirect_to_url' === $intent->next_action->type && ! empty( $intent->next_action->redirect_to_url->url ) ) {
+ return [
+ 'result' => 'success',
+ 'redirect' => $intent->next_action->redirect_to_url->url,
+ ];
+ } else {
+ return [
+ 'result' => 'success',
+ // Include a new nonce for update_order_status to ensure the update order
+ // status call works when a guest user creates an account during checkout.
+ 'redirect' => sprintf(
+ '#wc-stripe-confirm-%s:%s:%s:%s',
+ $payment_needed ? 'pi' : 'si',
+ $order_id,
+ $intent->client_secret,
+ wp_create_nonce( 'wc_stripe_update_order_status_nonce' )
+ ),
+ ];
+ }
+ }
+
+ list( $payment_method_type, $payment_method_details ) = $this->get_payment_method_data_from_intent( $intent );
+
+ if ( $payment_needed ) {
+ // Use the last charge within the intent to proceed.
+ $this->process_response( end( $intent->charges->data ), $order );
+ } else {
+ $order->payment_complete();
+ }
+ $this->set_payment_method_title_for_order( $order, $payment_method_type );
+
+ // Remove cart.
+ if ( isset( WC()->cart ) ) {
+ WC()->cart->empty_cart();
+ }
+
+ // Return thank you page redirect.
+ return [
+ 'result' => 'success',
+ 'redirect' => $this->get_return_url( $order ),
+ ];
+
+ } catch ( WC_Stripe_Exception $e ) {
+ wc_add_notice( $e->getLocalizedMessage(), 'error' );
+ WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
+
+ do_action( 'wc_gateway_stripe_process_payment_error', $e, $order );
+
+ /* translators: error message */
+ $order->update_status( 'failed' );
+
+ return [
+ 'result' => 'fail',
+ 'redirect' => '',
+ ];
+ }
+ }
+
+ /**
+ * Check for a UPE redirect payment method on order received page or setup intent on payment methods page.
+ *
+ * @since 5.6.0
+ * @version 5.6.0
+ */
+ public function maybe_process_upe_redirect() {
+ if ( $this->is_payment_methods_page() ) {
+ if ( $this->is_setup_intent_success_creation_redirection() ) {
+ if ( isset( $_GET['redirect_status'] ) && 'succeeded' === $_GET['redirect_status'] ) {
+ $user_id = wp_get_current_user()->ID;
+ $customer = new WC_Stripe_Customer( $user_id );
+ $customer->clear_cache();
+ wc_add_notice( __( 'Payment method successfully added.', 'woocommerce-gateway-stripe' ) );
+
+ // The newly created payment method does not inherit the customers' billing info, so we manually
+ // trigger an update; in case of failure we log the error and continue because the payment method's
+ // billing info will be updated when the customer makes a purchase anyway.
+ try {
+ $setup_intent_id = isset( $_GET['setup_intent'] ) ? wc_clean( wp_unslash( $_GET['setup_intent'] ) ) : '';
+ $setup_intent = $this->stripe_request( 'setup_intents/' . $setup_intent_id, [], null, 'GET' );
+
+ $customer_data = WC_Stripe_Customer::map_customer_data( null, new WC_Customer( $user_id ) );
+ $payment_method_object = $this->stripe_request(
+ 'payment_methods/' . $setup_intent->payment_method,
+ [
+ 'billing_details' => [
+ 'name' => $customer_data['name'],
+ 'email' => $customer_data['email'],
+ 'phone' => $customer_data['phone'],
+ 'address' => $customer_data['address'],
+ ],
+ ]
+ );
+
+ do_action( 'woocommerce_stripe_add_payment_method', $user_id, $payment_method_object );
+ } catch ( Exception $e ) {
+ WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
+ }
+ } else {
+ wc_add_notice( __( 'Failed to add payment method.', 'woocommerce-gateway-stripe' ), 'error', [ 'icon' => 'error' ] );
+ }
+ }
+ return;
+ }
+
+ if ( ! is_order_received_page() ) {
+ return;
+ }
+
+ $payment_method = isset( $_GET['wc_payment_method'] ) ? wc_clean( wp_unslash( $_GET['wc_payment_method'] ) ) : '';
+ if ( self::ID !== $payment_method ) {
+ return;
+ }
+
+ $is_nonce_valid = isset( $_GET['_wpnonce'] ) && wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wpnonce'] ) ), 'wc_stripe_process_redirect_order_nonce' );
+ if ( ! $is_nonce_valid || empty( $_GET['wc_payment_method'] ) ) {
+ return;
+ }
+
+ if ( ! empty( $_GET['payment_intent_client_secret'] ) ) {
+ $intent_id = isset( $_GET['payment_intent'] ) ? wc_clean( wp_unslash( $_GET['payment_intent'] ) ) : '';
+ } elseif ( ! empty( $_GET['setup_intent_client_secret'] ) ) {
+ $intent_id = isset( $_GET['setup_intent'] ) ? wc_clean( wp_unslash( $_GET['setup_intent'] ) ) : '';
+ } else {
+ return;
+ }
+
+ $order_id = isset( $_GET['order_id'] ) ? wc_clean( wp_unslash( $_GET['order_id'] ) ) : '';
+ $save_payment_method = isset( $_GET['save_payment_method'] ) ? 'yes' === wc_clean( wp_unslash( $_GET['save_payment_method'] ) ) : false;
+
+ if ( empty( $intent_id ) || empty( $order_id ) ) {
+ return;
+ }
+
+ $this->process_upe_redirect_payment( $order_id, $intent_id, $save_payment_method );
+ }
+
+ /**
+ * Processes UPE redirect payments.
+ *
+ * @param int $order_id The order ID being processed.
+ * @param string $intent_id The Stripe setup/payment intent ID for the order payment.
+ * @param bool $save_payment_method Boolean representing whether payment method for order should be saved.
+ *
+ * @since 5.5.0
+ * @version 5.5.0
+ */
+ public function process_upe_redirect_payment( $order_id, $intent_id, $save_payment_method ) {
+ $order = wc_get_order( $order_id );
+
+ if ( ! is_object( $order ) ) {
+ return;
+ }
+
+ if ( $order->has_status( [ 'processing', 'completed', 'on-hold' ] ) ) {
+ return;
+ }
+
+ if ( $order->get_meta( '_stripe_upe_redirect_processed', true ) ) {
+ return;
+ }
+
+ WC_Stripe_Logger::log( "Begin processing UPE redirect payment for order $order_id for the amount of {$order->get_total()}" );
+
+ try {
+ $this->process_order_for_confirmed_intent( $order, $intent_id, $save_payment_method );
+ } catch ( Exception $e ) {
+ WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
+
+ /* translators: localized exception message */
+ $order->update_status( 'failed', sprintf( __( 'UPE payment failed: %s', 'woocommerce-gateway-stripe' ), $e->getMessage() ) );
+
+ wc_add_notice( $e->getMessage(), 'error' );
+ wp_safe_redirect( wc_get_checkout_url() );
+ exit;
+ }
+ }
+
+ /**
+ * Update order and maybe save payment method for an order after an intent has been created and confirmed.
+ *
+ * @param WC_Order $order Order being processed.
+ * @param string $intent_id Stripe setup/payment ID.
+ * @param bool $save_payment_method Boolean representing whether payment method for order should be saved.
+ */
+ public function process_order_for_confirmed_intent( $order, $intent_id, $save_payment_method ) {
+ $payment_needed = $this->is_payment_needed( $order->get_id() );
+
+ // Get payment intent to confirm status.
+ if ( $payment_needed ) {
+ $intent = $this->stripe_request( 'payment_intents/' . $intent_id . '?expand[]=payment_method' );
+ $error = isset( $intent->last_payment_error ) ? $intent->last_payment_error : false;
+ } else {
+ $intent = $this->stripe_request( 'setup_intents/' . $intent_id . '?expand[]=payment_method&expand[]=latest_attempt' );
+ $error = isset( $intent->last_setup_error ) ? $intent->last_setup_error : false;
+ }
+
+ if ( ! empty( $error ) ) {
+ WC_Stripe_Logger::log( 'Error when processing payment: ' . $error->message );
+ throw new WC_Stripe_Exception( __( "We're not able to process this payment. Please try again later.", 'woocommerce-gateway-stripe' ) );
+ }
+
+ list( $payment_method_type, $payment_method_details ) = $this->get_payment_method_data_from_intent( $intent );
+
+ if ( ! isset( $this->payment_methods[ $payment_method_type ] ) ) {
+ return;
+ }
+ $payment_method = $this->payment_methods[ $payment_method_type ];
+
+ if ( $this->maybe_process_pre_orders( $order->get_id() ) ) {
+ // If this is a pre-order, simply mark the order as pre-ordered and allow
+ // the subsequent logic to save the payment method and proceed to complete the order.
+ $this->mark_order_as_pre_ordered( $order->get_id() );
+ $save_payment_method = true;
+ }
+
+ if ( $save_payment_method && $payment_method->is_reusable() ) {
+ $payment_method_object = null;
+ if ( $payment_method->get_id() !== $payment_method->get_retrievable_type() ) {
+ $generated_payment_method_id = $payment_method_details[ $payment_method_type ]->generated_sepa_debit;
+ $payment_method_object = $this->stripe_request( "payment_methods/$generated_payment_method_id", [], null, 'GET' );
+ } else {
+ $payment_method_object = $intent->payment_method;
+ }
+ $user = $this->get_user_from_order( $order );
+ $customer = new WC_Stripe_Customer( $user->ID );
+ $prepared_payment_method = $this->prepare_payment_method( $payment_method_object );
+
+ $customer->clear_cache();
+ $this->save_payment_method_to_order( $order, $prepared_payment_method );
+ do_action( 'woocommerce_stripe_add_payment_method', $user->get_id(), $payment_method_object );
+ }
+
+ if ( $payment_needed ) {
+ // Use the last charge within the intent to proceed.
+ $this->process_response( end( $intent->charges->data ), $order );
+ } else {
+ $order->payment_complete();
+ }
+ $this->save_intent_to_order( $order, $intent );
+ $this->set_payment_method_title_for_order( $order, $payment_method_type );
+ $order->update_meta_data( '_stripe_upe_redirect_processed', true );
+ $order->save();
+ }
+
+ /**
+ * Converts payment method into object similar to prepared source
+ * compatible with wc_stripe_payment_metadata and wc_stripe_generate_payment_request filters.
+ *
+ * @param object $payment_method Stripe payment method object response.
+ *
+ * @return object
+ */
+ public function prepare_payment_method( $payment_method ) {
+ return (object) [
+ 'customer' => $payment_method->customer,
+ 'source' => null,
+ 'source_object' => null,
+ 'payment_method' => $payment_method->id,
+ 'payment_method_object' => $payment_method,
+ ];
+ }
+
+ /**
+ * Save payment method to order.
+ *
+ * @param WC_Order $order For to which the source applies.
+ * @param stdClass $payment_method Stripe Payment Method.
+ */
+ public function save_payment_method_to_order( $order, $payment_method ) {
+ if ( $payment_method->customer ) {
+ $order->update_meta_data( '_stripe_customer_id', $payment_method->customer );
+ }
+ // Save the payment method id as `source_id`, because we use both `sources` and `payment_methods` APIs.
+ $order->update_meta_data( '_stripe_source_id', $payment_method->payment_method );
+
+ if ( is_callable( [ $order, 'save' ] ) ) {
+ $order->save();
+ }
+
+ $this->maybe_update_source_on_subscription_order( $order, $payment_method );
+ }
+
+ /**
+ * Retries the payment process once an error occured.
+ *
+ * @param object $intent The Payment Intent response from the Stripe API.
+ * @param WC_Order $order An order that is being paid for.
+ * @param bool $retry A flag that indicates whether another retry should be attempted.
+ * @param bool $force_save_source Force save the payment source.
+ * @param mixed $previous_error Any error message from previous request.
+ * @param bool $use_order_source Whether to use the source, which should already be attached to the order.
+ * @throws WC_Stripe_Exception If the payment is not accepted.
+ * @return array|void
+ */
+ public function retry_after_error( $intent, $order, $retry, $force_save_source = false, $previous_error = false, $use_order_source = false ) {
+ if ( ! $retry ) {
+ $localized_message = __( 'Sorry, we are unable to process your payment at this time. Please retry later.', 'woocommerce-gateway-stripe' );
+ $order->add_order_note( $localized_message );
+ throw new WC_Stripe_Exception( print_r( $intent, true ), $localized_message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.
+ }
+
+ // Don't do anymore retries after this.
+ if ( 5 <= $this->retry_interval ) {
+ return $this->process_payment_with_saved_payment_method( $order->get_id(), false );
+ }
+
+ sleep( $this->retry_interval );
+ $this->retry_interval++;
+
+ return $this->process_payment_with_saved_payment_method( $order->get_id(), true );
+ }
+
+ /**
+ * Returns true if a payment is needed for the current cart or order.
+ * Pre-Orders and Subscriptions may not require an upfront payment, so we need to check whether
+ * or not the payment is necessary to decide for either a setup intent or a payment intent.
+ *
+ * @since 5.8.0
+ *
+ * @param int $order_id The order ID being processed.
+ *
+ * @return bool Whether a payment is necessary.
+ */
+ public function is_payment_needed( $order_id = null ) {
+ if ( $this->is_pre_order_item_in_cart() || ( ! empty( $order_id ) && $this->has_pre_order( $order_id ) ) ) {
+ $pre_order_product = ( ! empty( $order_id ) ) ? $this->get_pre_order_product_from_order( $order_id ) : $this->get_pre_order_product_from_cart();
+ // Only one pre-order product is allowed per cart,
+ // so we can return if it's charged upfront.
+ return $this->is_pre_order_product_charged_upfront( $pre_order_product );
+ }
+
+ // Free trial subscriptions without a sign up fee, or any other type
+ // of order with a `0` amount should fall into the logic below.
+ $amount = is_null( WC()->cart ) ? 0 : WC()->cart->get_total( false );
+ $order = isset( $order_id ) ? wc_get_order( $order_id ) : null;
+ if ( is_a( $order, 'WC_Order' ) ) {
+ $amount = $order->get_total();
+ }
+
+ $converted_amount = WC_Stripe_Helper::get_stripe_amount( $amount, strtolower( get_woocommerce_currency() ) );
+
+ return 0 < $converted_amount;
+ }
+
+ /**
+ * Checks if card on Payment Method is a prepaid card.
+ *
+ * @since 4.0.6
+ * @param object $payment_method
+ * @return bool
+ */
+ public function is_prepaid_card( $payment_method ) {
+ return (
+ $payment_method
+ && ( 'card' === $payment_method->type )
+ && 'prepaid' === $payment_method->card->funding
+ );
+ }
+
+ /**
+ * Get WC User from WC Order.
+ *
+ * @param WC_Order $order
+ *
+ * @return WP_User
+ */
+ public function get_user_from_order( $order ) {
+ $user = $order->get_user();
+ if ( false === $user ) {
+ $user = wp_get_current_user();
+ }
+ return $user;
+ }
+
+ /**
+ * Checks if gateway should be available to use.
+ *
+ * @since 5.6.0
+ */
+ public function is_available() {
+ $methods_enabled_for_saved_payments = array_filter( $this->get_upe_enabled_payment_method_ids(), [ $this, 'is_enabled_for_saved_payments' ] );
+ if ( is_add_payment_method_page() && count( $methods_enabled_for_saved_payments ) === 0 ) {
+ return false;
+ }
+
+ return parent::is_available();
+ }
+
+ /**
+ * Function to be used with array_filter
+ * to filter UPE payment methods supported with current checkout
+ *
+ * @param string $payment_method_id Stripe payment method.
+ *
+ * @return bool
+ */
+ public function is_enabled_at_checkout( $payment_method_id ) {
+ if ( ! isset( $this->payment_methods[ $payment_method_id ] ) ) {
+ return false;
+ }
+ return $this->payment_methods[ $payment_method_id ]->is_enabled_at_checkout();
+ }
+
+ /**
+ * Function to be used with array_filter
+ * to filter UPE payment methods that support saved payments
+ *
+ * @param string $payment_method_id Stripe payment method.
+ *
+ * @return bool
+ */
+ public function is_enabled_for_saved_payments( $payment_method_id ) {
+ if ( ! isset( $this->payment_methods[ $payment_method_id ] ) ) {
+ return false;
+ }
+ return $this->payment_methods[ $payment_method_id ]->is_reusable();
+ }
+
+ // TODO: Actually validate.
+ public function validate_upe_checkout_experience_accepted_payments_field( $key, $value ) {
+ return $value;
+ }
+
+ /**
+ * Checks if the setting to allow the user to save cards is enabled.
+ *
+ * @return bool Whether the setting to allow saved cards is enabled or not.
+ */
+ public function is_saved_cards_enabled() {
+ return $this->saved_cards;
+ }
+
+ /**
+ * Set formatted readable payment method title for order,
+ * using payment method details from accompanying charge.
+ *
+ * @param WC_Order $order WC Order being processed.
+ * @param string $payment_method_type Stripe payment method key.
+ *
+ * @since 5.5.0
+ * @version 5.5.0
+ */
+ public function set_payment_method_title_for_order( $order, $payment_method_type ) {
+ if ( ! isset( $this->payment_methods[ $payment_method_type ] ) ) {
+ return;
+ }
+
+ $payment_method_title = $this->payment_methods[ $payment_method_type ]->get_label();
+
+ $order->set_payment_method( self::ID );
+ $order->set_payment_method_title( $payment_method_title );
+ $order->save();
+ }
+
+ /**
+ * This is overloading the upe checkout experience type on the settings page.
+ *
+ * @param string $key Field key.
+ * @param array $data Field data.
+ * @return string
+ */
+ public function generate_upe_checkout_experience_accepted_payments_html( $key, $data ) {
+ $stripe_account = $this->stripe_request( 'account' );
+ $stripe_capabilities = isset( $stripe_account->capabilities ) ? (array) $stripe_account->capabilities : [];
+ $data['description'] = '<p>' . __( "Select payments available to customers at checkout. We'll only show your customers the most relevant payment methods based on their currency and location.", 'woocommerce-gateway-stripe' ) . '</p>
+ <table class="wc_gateways widefat form-table wc-stripe-upe-method-selection" cellspacing="0" aria-describedby="wc_stripe_upe_method_selection">
+ <thead>
+ <tr>
+ <th class="name wc-stripe-upe-method-selection__name">' . esc_html__( 'Method', 'woocommerce-gateway-stripe' ) . '</th>
+ <th class="status wc-stripe-upe-method-selection__status">' . esc_html__( 'Enabled', 'woocommerce-gateway-stripe' ) . '</th>
+ <th class="description wc-stripe-upe-method-selection__description">' . esc_html__( 'Description', 'woocommerce-gateway-stripe' ) . '</th>
+ </tr>
+ </thead>
+ <tbody>';
+
+ $is_automatic_capture_enabled = $this->is_automatic_capture_enabled();
+
+ foreach ( $this->payment_methods as $method_id => $method ) {
+ $method_enabled = in_array( $method_id, $this->get_upe_enabled_payment_method_ids(), true ) && ( $is_automatic_capture_enabled || ! $method->requires_automatic_capture() ) ? 'enabled' : 'disabled';
+ $method_enabled_label = 'enabled' === $method_enabled ? __( 'enabled', 'woocommerce-gateway-stripe' ) : __( 'disabled', 'woocommerce-gateway-stripe' );
+ $capability_id = "{$method_id}_payments"; // "_payments" is a suffix that comes from Stripe API, except when it is "transfers", which does not apply here
+ $method_status = isset( $stripe_capabilities[ $capability_id ] ) ? $stripe_capabilities[ $capability_id ] : 'inactive';
+ $subtext_messages = $method->get_subtext_messages( $method_status );
+ $aria_label = sprintf(
+ /* translators: $1%s payment method ID, $2%s "enabled" or "disabled" */
+ esc_attr__( 'The "%1$s" payment method is currently %2$s', 'woocommerce-gateway-stripe' ),
+ $method_id,
+ $method_enabled_label
+ );
+ $manual_capture_tip = sprintf(
+ /* translators: $1%s payment method label */
+ __( '%1$s is not available to your customers when manual capture is enabled.', 'woocommerce-gateway-stripe' ),
+ $method->get_label()
+ );
+ $data['description'] .= '<tr data-upe_method_id="' . $method_id . '">
+ <td class="name wc-stripe-upe-method-selection__name" width="">
+ ' . $method->get_label() . '
+ ' . ( empty( $subtext_messages ) ? '' : '<span class="wc-payment-gateway-method-name"> – ' . $subtext_messages . '</span>' ) . '
+ </td>
+ <td class="status wc-stripe-upe-method-selection__status" width="1%">
+ <a class="wc-payment-upe-method-toggle-' . $method_enabled . '" href="#">
+ <span class="woocommerce-input-toggle woocommerce-input-toggle--' . $method_enabled . '" aria-label="' . $aria_label . '">
+ ' . ( 'enabled' === $method_enabled ? __( 'Yes', 'woocommerce-gateway-stripe' ) : __( 'No', 'woocommerce-gateway-stripe' ) ) . '
+ </span>
+ </a>'
+ . ( ! $is_automatic_capture_enabled && $method->requires_automatic_capture() ? '<span class="tips dashicons dashicons-warning" style="margin-top: 1px; margin-right: -25px; margin-left: 5px; color: red" data-tip="' . $manual_capture_tip . '" />' : '' ) .
+ '</td>
+ <td class="description wc-stripe-upe-method-selection__description" width="">' . $method->get_description() . '</td>
+ </tr>';
+ }
+
+ $data['description'] .= '</tbody>
+ </table>
+ <p><a class="button" target="_blank" href="https://dashboard.stripe.com/account/payments/settings">' . __( 'Get more payment methods', 'woocommerce-gateway-stripe' ) . '</a></p>
+ <span id="wc_stripe_upe_change_notice" class="hidden">' . __( 'You must save your changes.', 'woocommerce-gateway-stripe' ) . '</span>';
+
+ return $this->generate_title_html( $key, $data );
+ }
+
+ /**
+ * Extacts the Stripe intent's payment_method_type and payment_method_details values.
+ *
+ * @param $intent Stripe's intent response.
+ * @return string[] List with 2 values: payment_method_type and payment_method_details.
+ */
+ private function get_payment_method_data_from_intent( $intent ) {
+ $payment_method_type = '';
+ $payment_method_details = false;
+
+ if ( 'payment_intent' === $intent->object ) {
+ if ( ! empty( $intent->charges ) && 0 < $intent->charges->total_count ) {
+ $charge = end( $intent->charges->data );
+ $payment_method_details = (array) $charge->payment_method_details;
+ $payment_method_type = ! empty( $payment_method_details ) ? $payment_method_details['type'] : '';
+ }
+ } elseif ( 'setup_intent' === $intent->object ) {
+ if ( ! empty( $intent->latest_attempt ) && ! empty( $intent->latest_attempt->payment_method_details ) ) {
+ $payment_method_details = (array) $intent->latest_attempt->payment_method_details;
+ $payment_method_type = $payment_method_details['type'];
+ } elseif ( ! empty( $intent->payment_method ) ) {
+ $payment_method_details = $intent->payment_method;
+ $payment_method_type = $payment_method_details->type;
+ }
+ // Setup intents don't have details, keep the false value.
+ }
+
+ return [ $payment_method_type, $payment_method_details ];
+ }
+
+ /**
+ * Prepares Stripe metadata for a given order.
+ *
+ * @param WC_Order $order Order being processed.
+ *
+ * @return array Array of keyed metadata values.
+ */
+ public function get_metadata_from_order( $order ) {
+ $payment_type = $this->is_payment_recurring( $order->get_id() ) ? 'recurring' : 'single';
+ $name = sanitize_text_field( $order->get_billing_first_name() ) . ' ' . sanitize_text_field( $order->get_billing_last_name() );
+ $email = sanitize_email( $order->get_billing_email() );
+
+ return [
+ 'customer_name' => $name,
+ 'customer_email' => $email,
+ 'site_url' => esc_url( get_site_url() ),
+ 'order_id' => $order->get_id(),
+ 'order_key' => $order->get_order_key(),
+ 'payment_type' => $payment_type,
+ ];
+ }
+
+ /**
+ * Returns true when viewing payment methods page.
+ *
+ * @return bool
+ */
+ private function is_payment_methods_page() {
+ global $wp;
+
+ $page_id = wc_get_page_id( 'myaccount' );
+
+ return ( $page_id && is_page( $page_id ) && ( isset( $wp->query_vars['payment-methods'] ) ) );
+ }
+
+ /**
+ * True if the request contains the values that indicates a redirection after a successful setup intent creation.
+ *
+ * @return bool
+ */
+ private function is_setup_intent_success_creation_redirection() {
+ return ( ! empty( $_GET['setup_intent_client_secret'] ) & ! empty( $_GET['setup_intent'] ) & ! empty( $_GET['redirect_status'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ }
+
+ /**
+ * Adds a token to current user from a setup intent id.
+ *
+ * @param string $setup_intent_id ID of the setup intent.
+ * @param WP_User $user User to add token to.
+ *
+ * @return WC_Payment_Token_CC|WC_Payment_Token_WCPay_SEPA The added token.
+ *
+ * @since 5.8.0
+ * @version 5.8.0
+ */
+ public function create_token_from_setup_intent( $setup_intent_id, $user ) {
+ try {
+ $setup_intent = $this->stripe_request( 'setup_intents/' . $setup_intent_id );
+ if ( ! empty( $setup_intent->last_payment_error ) ) {
+ throw new WC_Stripe_Exception( __( "We're not able to add this payment method. Please try again later.", 'woocommerce-gateway-stripe' ) );
+ }
+
+ $payment_method_id = $setup_intent->payment_method;
+ $payment_method_object = $this->stripe_request( 'payment_methods/' . $payment_method_id );
+
+ $payment_method = $this->payment_methods[ $payment_method_object->type ];
+
+ $customer = new WC_Stripe_Customer( wp_get_current_user()->ID );
+ $customer->clear_cache();
+
+ return $payment_method->create_payment_token_for_user( $user->ID, $payment_method_object );
+ } catch ( Exception $e ) {
+ wc_add_notice( $e->getMessage(), 'error', [ 'icon' => 'error' ] );
+ WC_Stripe_Logger::log( 'Error when adding payment method: ' . $e->getMessage() );
+ return [
+ 'result' => 'error',
+ ];
+ }
+ }
+
+ /**
+ * Wrapper function to manage requests to WC_Stripe_API.
+ *
+ * @param string $path Stripe API endpoint path to query.
+ * @param string $params Parameters for request body.
+ * @param WC_Order $order WC Order for request.
+ * @param string $method HTTP method for request.
+ *
+ * @return object JSON response object.
+ */
+ protected function stripe_request( $path, $params = null, $order = null, $method = 'POST' ) {
+ if ( is_null( $params ) ) {
+ return WC_Stripe_API::retrieve( $path );
+ }
+ if ( ! is_null( $order ) ) {
+ $level3_data = $this->get_level3_data_from_order( $order );
+ return WC_Stripe_API::request_with_level3_data( $params, $path, $level3_data, $order );
+ }
+ return WC_Stripe_API::request( $params, $path, $method );
+ }
+
+}
diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-bancontact.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-bancontact.php
new file mode 100644
index 0000000..89c04e0
--- /dev/null
+++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-bancontact.php
@@ -0,0 +1,30 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Bancontact Payment Method class extending UPE base class
+ */
+class WC_Stripe_UPE_Payment_Method_Bancontact extends WC_Stripe_UPE_Payment_Method {
+
+ const STRIPE_ID = 'bancontact';
+
+ const LPM_GATEWAY_CLASS = WC_Gateway_Stripe_Bancontact::class;
+
+ /**
+ * Constructor for Bancontact payment method
+ */
+ public function __construct() {
+ parent::__construct();
+ $this->stripe_id = self::STRIPE_ID;
+ $this->title = 'Pay with Bancontact';
+ $this->is_reusable = true;
+ $this->supported_currencies = [ 'EUR' ];
+ $this->label = __( 'Bancontact', 'woocommerce-gateway-stripe' );
+ $this->description = __(
+ 'Bancontact is the most popular online payment method in Belgium, with over 15 million cards in circulation.',
+ 'woocommerce-gateway-stripe'
+ );
+ }
+}
diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-boleto.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-boleto.php
new file mode 100644
index 0000000..ee5b3fc
--- /dev/null
+++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-boleto.php
@@ -0,0 +1,52 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Boleto Payment Method class extending UPE base class
+ */
+class WC_Stripe_UPE_Payment_Method_Boleto extends WC_Stripe_UPE_Payment_Method {
+
+ const STRIPE_ID = 'boleto';
+
+ const LPM_GATEWAY_CLASS = WC_Gateway_Stripe_Boleto::class;
+
+ /**
+ * Constructor for Boleto payment method
+ *
+ * @since 5.8.0
+ */
+ public function __construct() {
+ parent::__construct();
+ $this->stripe_id = self::STRIPE_ID;
+ $this->can_refund = false;
+ $this->title = 'Pay with Boleto';
+ $this->is_reusable = false;
+ $this->supported_currencies = [ 'BRL' ];
+ $this->supported_countries = [ 'BR' ];
+ $this->label = __( 'Boleto', 'woocommerce-gateway-stripe' );
+ $this->description = __(
+ 'Boleto is an official payment method in Brazil. Customers receive a voucher that can be paid at authorized agencies or banks, ATMs, or online bank portals.',
+ 'woocommerce-gateway-stripe'
+ );
+
+ add_filter( 'wc_stripe_allowed_payment_processing_statuses', [ $this, 'add_allowed_payment_processing_statuses' ], 10, 2 );
+ }
+
+ /**
+ * Adds on-hold as accepted status during webhook handling on orders paid with Boleto
+ *
+ * @param $allowed_statuses
+ * @param $order
+ *
+ * @return mixed
+ */
+ public function add_allowed_payment_processing_statuses( $allowed_statuses, $order ) {
+ if ( 'boleto' === $order->get_meta( '_stripe_upe_payment_type' ) && ! in_array( 'on-hold', $allowed_statuses ) ) {
+ $allowed_statuses[] = 'on-hold';
+ }
+
+ return $allowed_statuses;
+ }
+}
diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-cc.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-cc.php
new file mode 100644
index 0000000..31bbb48
--- /dev/null
+++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-cc.php
@@ -0,0 +1,112 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Class WC_Stripe_UPE_Payment_Method_CC
+ */
+
+/**
+ * Credit card Payment Method class extending UPE base class
+ */
+class WC_Stripe_UPE_Payment_Method_CC extends WC_Stripe_UPE_Payment_Method {
+
+ const STRIPE_ID = 'card';
+
+ const LPM_GATEWAY_CLASS = WC_Gateway_Stripe::class;
+
+ /**
+ * Constructor for card payment method
+ */
+ public function __construct() {
+ parent::__construct();
+ $this->stripe_id = self::STRIPE_ID;
+ $this->title = __( 'Pay with credit card / debit card', 'woocommerce-gateway-stripe' );
+ $this->is_reusable = true;
+ $this->label = __( 'Credit card / debit card', 'woocommerce-gateway-stripe' );
+ $this->description = __(
+ 'Let your customers pay with major credit and debit cards without leaving your store.',
+ 'woocommerce-gateway-stripe'
+ );
+ }
+
+ /**
+ * Returns payment method title
+ *
+ * @param array|bool $payment_details Optional payment details from charge object.
+ *
+ * @return string
+ */
+ public function get_title( $payment_details = false ) {
+ if ( ! $payment_details ) {
+ return $this->title;
+ }
+
+ $details = $payment_details[ $this->stripe_id ];
+ $funding_types = [
+ 'credit' => __( 'credit', 'woocommerce-gateway-stripe' ),
+ 'debit' => __( 'debit', 'woocommerce-gateway-stripe' ),
+ 'prepaid' => __( 'prepaid', 'woocommerce-gateway-stripe' ),
+ 'unknown' => __( 'unknown', 'woocommerce-gateway-stripe' ),
+ ];
+
+ return sprintf(
+ // Translators: %1$s card brand, %2$s card funding (prepaid, credit, etc.).
+ __( '%1$s %2$s card', 'woocommerce-gateway-stripe' ),
+ ucfirst( $details->network ),
+ $funding_types[ $details->funding ]
+ );
+ }
+
+ /**
+ * Returns string representing payment method type
+ * to query to retrieve saved payment methods from Stripe.
+ */
+ public function get_retrievable_type() {
+ return $this->get_id();
+ }
+
+ /**
+ * Create and return WC payment token for user.
+ *
+ * This will be used from the WC_Stripe_Payment_Tokens service
+ * as opposed to WC_Stripe_UPE_Payment_Gateway.
+ *
+ * @param string $user_id WP_User ID
+ * @param object $payment_method Stripe payment method object
+ *
+ * @return WC_Payment_Token_CC
+ */
+ public function create_payment_token_for_user( $user_id, $payment_method ) {
+ $token = new WC_Payment_Token_CC();
+ $token->set_expiry_month( $payment_method->card->exp_month );
+ $token->set_expiry_year( $payment_method->card->exp_year );
+ $token->set_card_type( strtolower( $payment_method->card->brand ) );
+ $token->set_last4( $payment_method->card->last4 );
+ $token->set_gateway_id( WC_Stripe_UPE_Payment_Gateway::ID );
+ $token->set_token( $payment_method->id );
+ $token->set_user_id( $user_id );
+ $token->save();
+ return $token;
+ }
+
+ /**
+ * Returns boolean dependent on whether capability
+ * for site account is enabled for payment method.
+ *
+ * @return bool
+ */
+ public function is_capability_active() {
+ return true;
+ }
+
+ /**
+ * The Credit Card method allows automatic capture.
+ *
+ * @return bool
+ */
+ public function requires_automatic_capture() {
+ return false;
+ }
+}
diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-eps.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-eps.php
new file mode 100644
index 0000000..65844aa
--- /dev/null
+++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-eps.php
@@ -0,0 +1,30 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * EPS Payment Method class extending UPE base class
+ */
+class WC_Stripe_UPE_Payment_Method_Eps extends WC_Stripe_UPE_Payment_Method {
+
+ const STRIPE_ID = 'eps';
+
+ const LPM_GATEWAY_CLASS = WC_Gateway_Stripe_Eps::class;
+
+ /**
+ * Constructor for EPS payment method
+ */
+ public function __construct() {
+ parent::__construct();
+ $this->stripe_id = self::STRIPE_ID;
+ $this->title = __( 'Pay with EPS', 'woocommerce-gateway-stripe' );
+ $this->is_reusable = false;
+ $this->supported_currencies = [ 'EUR' ];
+ $this->label = __( 'EPS', 'woocommerce-gateway-stripe' );
+ $this->description = __(
+ 'EPS is an Austria-based payment method that allows customers to complete transactions online using their bank credentials.',
+ 'woocommerce-gateway-stripe'
+ );
+ }
+}
diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-giropay.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-giropay.php
new file mode 100644
index 0000000..708f794
--- /dev/null
+++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-giropay.php
@@ -0,0 +1,30 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * The giropay Payment Method class extending UPE base class
+ */
+class WC_Stripe_UPE_Payment_Method_Giropay extends WC_Stripe_UPE_Payment_Method {
+
+ const STRIPE_ID = 'giropay';
+
+ const LPM_GATEWAY_CLASS = WC_Gateway_Stripe_Giropay::class;
+
+ /**
+ * Constructor for giropay payment method
+ */
+ public function __construct() {
+ parent::__construct();
+ $this->stripe_id = self::STRIPE_ID;
+ $this->title = __( 'Pay with giropay', 'woocommerce-gateway-stripe' );
+ $this->is_reusable = false;
+ $this->supported_currencies = [ 'EUR' ];
+ $this->label = __( 'giropay', 'woocommerce-gateway-stripe' );
+ $this->description = __(
+ 'Expand your business with giropay — Germany’s second most popular payment system.',
+ 'woocommerce-gateway-stripe'
+ );
+ }
+}
diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-ideal.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-ideal.php
new file mode 100644
index 0000000..503aadf
--- /dev/null
+++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-ideal.php
@@ -0,0 +1,30 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * The iDEAL Payment Method class extending UPE base class
+ */
+class WC_Stripe_UPE_Payment_Method_Ideal extends WC_Stripe_UPE_Payment_Method {
+
+ const STRIPE_ID = 'ideal';
+
+ const LPM_GATEWAY_CLASS = WC_Gateway_Stripe_Ideal::class;
+
+ /**
+ * Constructor for iDEAL payment method
+ */
+ public function __construct() {
+ parent::__construct();
+ $this->stripe_id = self::STRIPE_ID;
+ $this->title = __( 'Pay with iDEAL', 'woocommerce-gateway-stripe' );
+ $this->is_reusable = true;
+ $this->supported_currencies = [ 'EUR' ];
+ $this->label = __( 'iDEAL', 'woocommerce-gateway-stripe' );
+ $this->description = __(
+ 'iDEAL is a Netherlands-based payment method that allows customers to complete transactions online using their bank credentials.',
+ 'woocommerce-gateway-stripe'
+ );
+ }
+}
diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-link.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-link.php
new file mode 100644
index 0000000..a088c46
--- /dev/null
+++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-link.php
@@ -0,0 +1,88 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Link Payment Method class extending UPE base class
+ */
+class WC_Stripe_UPE_Payment_Method_Link extends WC_Stripe_UPE_Payment_Method {
+
+ const STRIPE_ID = 'link';
+
+ /**
+ * Constructor for Link payment method
+ */
+ public function __construct() {
+ parent::__construct();
+ $this->stripe_id = self::STRIPE_ID;
+ $this->title = __( 'Pay with Link', 'woocommerce-gateway-stripe' );
+ $this->is_reusable = true;
+ $this->supported_currencies = [ 'USD' ];
+ $this->label = __( 'Stripe Link', 'woocommerce-gateway-stripe' );
+ $this->description = __(
+ 'Link is a payment method that allows customers to save payment information and use the payment details
+ for further payments.',
+ 'woocommerce-gateway-stripe'
+ );
+ }
+
+ /**
+ * Return if Stripe Link is enabled
+ *
+ * @return bool
+ */
+ public static function is_link_enabled() {
+
+ // Assume Link is disabled if UPE is disabled.
+ if ( ! WC_Stripe_Feature_Flags::is_upe_checkout_enabled() ) {
+ return false;
+ }
+
+ return in_array(
+ self::STRIPE_ID,
+ woocommerce_gateway_stripe()->get_main_stripe_gateway()->get_upe_enabled_payment_method_ids(),
+ true
+ );
+ }
+
+ /**
+ * Returns string representing payment method type
+ * to query to retrieve saved payment methods from Stripe.
+ */
+ public function get_retrievable_type() {
+ return $this->get_id();
+ }
+
+ /**
+ * Create new WC payment token and add to user.
+ *
+ * @param int $user_id WP_User ID
+ * @param object $payment_method Stripe payment method object
+ *
+ * @return WC_Payment_Token_Link
+ */
+ public function create_payment_token_for_user( $user_id, $payment_method ) {
+ $token = new WC_Payment_Token_Link();
+ $token->set_email( $payment_method->link->email );
+ $token->set_gateway_id( WC_Stripe_UPE_Payment_Gateway::ID );
+ $token->set_token( $payment_method->id );
+ $token->set_payment_method_type( $this->get_id() );
+ $token->set_user_id( $user_id );
+ $token->save();
+ return $token;
+ }
+
+ /**
+ * Returns true if the UPE method is available.
+ *
+ * @return bool
+ */
+ public function is_available() {
+ //if merchant is outside US, Link payment method should not be available
+ $cached_account_data = WC_Stripe::get_instance()->account->get_cached_account_data();
+ $account_country = $cached_account_data['country'] ?? null;
+
+ return 'US' === $account_country && parent::is_available();
+ }
+}
diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-oxxo.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-oxxo.php
new file mode 100644
index 0000000..074f80e
--- /dev/null
+++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-oxxo.php
@@ -0,0 +1,52 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * OXXO Payment Method class extending UPE base class
+ */
+class WC_Stripe_UPE_Payment_Method_Oxxo extends WC_Stripe_UPE_Payment_Method {
+
+ const STRIPE_ID = 'oxxo';
+
+ const LPM_GATEWAY_CLASS = WC_Gateway_Stripe_Oxxo::class;
+
+ /**
+ * Constructor for OXXO payment method
+ *
+ * @since 5.8.0
+ */
+ public function __construct() {
+ parent::__construct();
+ $this->stripe_id = self::STRIPE_ID;
+ $this->can_refund = false;
+ $this->title = 'Pay with OXXO';
+ $this->is_reusable = false;
+ $this->supported_currencies = [ 'MXN' ];
+ $this->supported_countries = [ 'MX' ];
+ $this->label = __( 'OXXO', 'woocommerce-gateway-stripe' );
+ $this->description = __(
+ 'OXXO is a Mexican chain of convenience stores that allows customers to pay bills and online purchases in-store with cash.',
+ 'woocommerce-gateway-stripe'
+ );
+
+ add_filter( 'wc_stripe_allowed_payment_processing_statuses', [ $this, 'add_allowed_payment_processing_statuses' ], 10, 2 );
+ }
+
+ /**
+ * Adds on-hold as accepted status during webhook handling on orders paid with OXXO
+ *
+ * @param $allowed_statuses
+ * @param $order
+ *
+ * @return mixed
+ */
+ public function add_allowed_payment_processing_statuses( $allowed_statuses, $order ) {
+ if ( 'oxxo' === $order->get_meta( '_stripe_upe_payment_type' ) && ! in_array( 'on-hold', $allowed_statuses ) ) {
+ $allowed_statuses[] = 'on-hold';
+ }
+
+ return $allowed_statuses;
+ }
+}
diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-p24.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-p24.php
new file mode 100644
index 0000000..731061b
--- /dev/null
+++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-p24.php
@@ -0,0 +1,30 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Przelewy24 Payment Method class extending UPE base class
+ */
+class WC_Stripe_UPE_Payment_Method_P24 extends WC_Stripe_UPE_Payment_Method {
+
+ const STRIPE_ID = 'p24';
+
+ const LPM_GATEWAY_CLASS = WC_Gateway_Stripe_P24::class;
+
+ /**
+ * Constructor for Przelewy24 payment method
+ */
+ public function __construct() {
+ parent::__construct();
+ $this->stripe_id = self::STRIPE_ID;
+ $this->title = __( 'Pay with Przelewy24', 'woocommerce-gateway-stripe' );
+ $this->is_reusable = false;
+ $this->supported_currencies = [ 'EUR', 'PLN' ];
+ $this->label = __( 'Przelewy24', 'woocommerce-gateway-stripe' );
+ $this->description = __(
+ 'Przelewy24 is a Poland-based payment method aggregator that allows customers to complete transactions online using bank transfers and other methods.',
+ 'woocommerce-gateway-stripe'
+ );
+ }
+}
diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-sepa.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-sepa.php
new file mode 100644
index 0000000..a8dd752
--- /dev/null
+++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-sepa.php
@@ -0,0 +1,40 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * SEPA Payment Method class extending UPE base class
+ */
+class WC_Stripe_UPE_Payment_Method_Sepa extends WC_Stripe_UPE_Payment_Method {
+
+ const STRIPE_ID = 'sepa_debit';
+
+ const LPM_GATEWAY_CLASS = WC_Gateway_Stripe_Sepa::class;
+
+ /**
+ * Constructor for SEPA payment method
+ *
+ * @param WC_Payments_Token_Service $token_service Token class instance.
+ */
+ public function __construct() {
+ parent::__construct();
+ $this->stripe_id = self::STRIPE_ID;
+ $this->title = __( 'Pay with SEPA Direct Debit', 'woocommerce-gateway-stripe' );
+ $this->is_reusable = true;
+ $this->supported_currencies = [ 'EUR' ];
+ $this->label = __( 'SEPA Direct Debit', 'woocommerce-gateway-stripe' );
+ $this->description = __(
+ 'Reach 500 million customers and over 20 million businesses across the European Union.',
+ 'woocommerce-gateway-stripe'
+ );
+ }
+
+ /**
+ * Returns string representing payment method type
+ * to query to retrieve saved payment methods from Stripe.
+ */
+ public function get_retrievable_type() {
+ return $this->get_id();
+ }
+}
diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-sofort.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-sofort.php
new file mode 100644
index 0000000..1188f33
--- /dev/null
+++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-sofort.php
@@ -0,0 +1,30 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Sofort Payment Method class extending UPE base class
+ */
+class WC_Stripe_UPE_Payment_Method_Sofort extends WC_Stripe_UPE_Payment_Method {
+
+ const STRIPE_ID = 'sofort';
+
+ const LPM_GATEWAY_CLASS = WC_Gateway_Stripe_Sofort::class;
+
+ /**
+ * Constructor for Sofort payment method
+ */
+ public function __construct() {
+ parent::__construct();
+ $this->stripe_id = self::STRIPE_ID;
+ $this->title = __( 'Pay with Sofort', 'woocommerce-gateway-stripe' );
+ $this->is_reusable = true;
+ $this->supported_currencies = [ 'EUR' ];
+ $this->label = __( 'Sofort', 'woocommerce-gateway-stripe' );
+ $this->description = __(
+ 'Accept secure bank transfers from Austria, Belgium, Germany, Italy, Netherlands, and Spain.',
+ 'woocommerce-gateway-stripe'
+ );
+ }
+}
diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method.php b/includes/payment-methods/class-wc-stripe-upe-payment-method.php
new file mode 100644
index 0000000..c681f17
--- /dev/null
+++ b/includes/payment-methods/class-wc-stripe-upe-payment-method.php
@@ -0,0 +1,346 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Abstract UPE Payment Method class
+ *
+ * Handles general functionality for UPE payment methods
+ */
+
+
+/**
+ * Extendable abstract class for payment methods.
+ */
+abstract class WC_Stripe_UPE_Payment_Method {
+
+ use WC_Stripe_Subscriptions_Utilities_Trait;
+ use WC_Stripe_Pre_Orders_Trait;
+
+ /**
+ * Stripe key name
+ *
+ * @var string
+ */
+ protected $stripe_id;
+
+ /**
+ * Display title
+ *
+ * @var string
+ */
+ protected $title;
+
+ /**
+ * Method label
+ *
+ * @var string
+ */
+ protected $label;
+
+ /**
+ * Method description
+ *
+ * @var string
+ */
+ protected $description;
+
+ /**
+ * Can payment method be saved or reused?
+ *
+ * @var bool
+ */
+ protected $is_reusable;
+
+ /**
+ * Array of currencies supported by this UPE method
+ *
+ * @var array
+ */
+ protected $supported_currencies;
+
+ /**
+ * Can this payment method be refunded?
+ *
+ * @var array
+ */
+ protected $can_refund = true;
+
+ /**
+ * Wether this UPE method is enabled
+ *
+ * @var bool
+ */
+ protected $enabled;
+
+ /**
+ * List of supported countries
+ *
+ * @var array
+ */
+ protected $supported_countries;
+
+ /**
+ * Create instance of payment method
+ */
+ public function __construct() {
+ $main_settings = get_option( 'woocommerce_stripe_settings' );
+
+ if ( isset( $main_settings['upe_checkout_experience_accepted_payments'] ) ) {
+ $enabled_upe_methods = $main_settings['upe_checkout_experience_accepted_payments'];
+ } else {
+ $enabled_upe_methods = [ WC_Stripe_UPE_Payment_Method_CC::STRIPE_ID ];
+ }
+
+ $this->enabled = in_array( static::STRIPE_ID, $enabled_upe_methods, true );
+ }
+
+ /**
+ * Returns payment method ID
+ *
+ * @return string
+ */
+ public function get_id() {
+ return $this->stripe_id;
+ }
+
+ /**
+ * Returns true if the UPE method is enabled.
+ *
+ * @return bool
+ */
+ public function is_enabled() {
+ return $this->enabled;
+ }
+
+ /**
+ * Returns true if the UPE method is available.
+ *
+ * @return bool
+ */
+ public function is_available() {
+ return true;
+ }
+
+ /**
+ * Returns payment method title
+ *
+ * @param array|bool $payment_details Optional payment details from charge object.
+ *
+ * @return string
+ */
+ public function get_title( $payment_details = false ) {
+ return $this->title;
+ }
+
+ /**
+ * Returns payment method label
+ *
+ * @return string
+ */
+ public function get_label() {
+ return $this->label;
+ }
+
+ /**
+ * Returns payment method description
+ *
+ * @return string
+ */
+ public function get_description() {
+ return $this->description;
+ }
+
+ /**
+ * Returns boolean dependent on whether payment method
+ * can be used at checkout
+ *
+ * @param int|null $order_id
+ * @return bool
+ */
+ public function is_enabled_at_checkout( $order_id = null ) {
+ // Check capabilities first.
+ if ( ! $this->is_capability_active() ) {
+ return false;
+ }
+
+ // Check currency compatibility.
+ $currencies = $this->get_supported_currencies();
+ if ( ! empty( $currencies ) && ! in_array( $this->get_woocommerce_currency(), $currencies, true ) ) {
+ return false;
+ }
+
+ // If cart or order contains subscription, enable payment method if it's reusable.
+ if ( $this->is_subscription_item_in_cart() || ( ! empty( $order_id ) && $this->has_subscription( $order_id ) ) ) {
+ return $this->is_reusable();
+ }
+
+ // If cart or order contains pre-order, enable payment method if it's reusable.
+ if ( $this->is_pre_order_item_in_cart() || ( ! empty( $order_id ) && $this->has_pre_order( $order_id ) ) ) {
+ return $this->is_reusable();
+ }
+
+ return true;
+ }
+
+ /**
+ * Validates if a payment method is available on a given country
+ *
+ * @param string $country a two-letter country code
+ *
+ * @return bool Will return true if supported_countries is empty on payment method
+ */
+ public function is_allowed_on_country( $country ) {
+ if ( ! empty( $this->supported_countries ) ) {
+ return in_array( $country, $this->supported_countries );
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns boolean dependent on whether payment method
+ * will support saved payments/subscription payments
+ *
+ * @return bool
+ */
+ public function is_reusable() {
+ return $this->is_reusable;
+ }
+
+ /**
+ * Returns boolean dependent on whether capability
+ * for site account is enabled for payment method.
+ *
+ * @return bool
+ */
+ public function is_capability_active() {
+ // Treat all capabilities as active when in test mode.
+ $plugin_settings = get_option( 'woocommerce_stripe_settings' );
+ $test_mode_setting = ! empty( $plugin_settings['testmode'] ) ? $plugin_settings['testmode'] : 'no';
+
+ if ( 'yes' === $test_mode_setting ) {
+ return true;
+ }
+
+ // Otherwise, make sure the capability is available.
+ $capabilities = $this->get_capabilities_response();
+ if ( empty( $capabilities ) ) {
+ return false;
+ }
+ $key = $this->get_id() . '_payments';
+ return isset( $capabilities[ $key ] ) && 'active' === $capabilities[ $key ];
+ }
+
+ /**
+ * Returns capabilities response object for site account.
+ *
+ * @return object
+ */
+ public function get_capabilities_response() {
+ $account = WC_Stripe::get_instance()->account;
+ $data = $account->get_cached_account_data();
+ if ( empty( $data ) || ! isset( $data['capabilities'] ) ) {
+ return [];
+ }
+ return $data['capabilities'];
+ }
+
+ /**
+ * Returns string representing payment method type
+ * to query to retrieve saved payment methods from Stripe.
+ */
+ public function get_retrievable_type() {
+ return $this->is_reusable() ? WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID : null;
+ }
+
+ /**
+ * Create new WC payment token and add to user.
+ *
+ * @param int $user_id WP_User ID
+ * @param object $payment_method Stripe payment method object
+ *
+ * @return WC_Payment_Token_SEPA
+ */
+ public function create_payment_token_for_user( $user_id, $payment_method ) {
+ $token = new WC_Payment_Token_SEPA();
+ $token->set_last4( $payment_method->sepa_debit->last4 );
+ $token->set_gateway_id( WC_Stripe_UPE_Payment_Gateway::ID );
+ $token->set_token( $payment_method->id );
+ $token->set_payment_method_type( $this->get_id() );
+ $token->set_user_id( $user_id );
+ $token->save();
+ return $token;
+ }
+
+ /**
+ * Returns the currencies this UPE method supports.
+ *
+ * @return array|null
+ */
+ public function get_supported_currencies() {
+ return apply_filters(
+ 'wc_stripe_' . static::STRIPE_ID . '_upe_supported_currencies',
+ $this->supported_currencies
+ );
+ }
+
+ /**
+ * Wrapper function for get_woocommerce_currency global function
+ */
+ public function get_woocommerce_currency() {
+ return get_woocommerce_currency();
+ }
+
+ /**
+ * Returns whether the payment method requires automatic capture.
+ * By default all the UPE payment methods require automatic capture, except for "card".
+ *
+ * @return bool
+ */
+ public function requires_automatic_capture() {
+ return true;
+ }
+
+ /**
+ * Returns the HTML for the subtext messaging in the old settings UI.
+ *
+ * @param string $stripe_method_status (optional) Status of this payment method based on the Stripe's account capabilities
+ * @return string
+ */
+ public function get_subtext_messages( $stripe_method_status ) {
+ // can be either a `currency` or `activation` messaging, to be displayed in the old settings UI.
+ $messages = [];
+
+ if ( ! empty( $stripe_method_status ) && 'active' !== $stripe_method_status ) {
+ $text = __( 'Pending activation', 'woocommerce-gateway-stripe' );
+ $tooltip_content = sprintf(
+ /* translators: %1: Payment method name */
+ esc_attr__( '%1$s won\'t be visible to your customers until you provide the required information. Follow the instructions Stripe has sent to your e-mail address.', 'woocommerce-gateway-stripe' ),
+ $this->get_label()
+ );
+ $messages[] = $text . '<span class="tips" data-tip="' . $tooltip_content . '"><span class="woocommerce-help-tip" style="margin-top: 0;"></span></span>';
+ }
+
+ $currencies = $this->get_supported_currencies();
+ if ( ! empty( $currencies ) && ! in_array( get_woocommerce_currency(), $currencies, true ) ) {
+ /* translators: %s: List of comma-separated currencies. */
+ $tooltip_content = sprintf( esc_attr__( 'In order to be used at checkout, the payment method requires the store currency to be set to one of: %s', 'woocommerce-gateway-stripe' ), implode( ', ', $currencies ) );
+ $text = __( 'Requires currency', 'woocommerce-gateway-stripe' );
+
+ $messages[] = $text . '<span class="tips" data-tip="' . $tooltip_content . '"><span class="woocommerce-help-tip" style="margin-top: 0;"></span></span>';
+ }
+
+ return count( $messages ) > 0 ? join( ' – ', $messages ) : '';
+ }
+
+ /**
+ * Checks if payment method allows refund via stripe
+ *
+ * @return bool
+ */
+ public function can_refund_via_stripe() {
+ return $this->can_refund;
+ }
+}
diff --git a/includes/payment-methods/index.html b/includes/payment-methods/index.html
new file mode 100644
index 0000000..d06b2ba
--- /dev/null
+++ b/includes/payment-methods/index.html
@@ -0,0 +1,33 @@
+<html><head><title> - Revision 2844313: /woocommerce-gateway-stripe/trunk/includes/payment-methods</title></head>
+<body>
+ <h2> - Revision 2844313: /woocommerce-gateway-stripe/trunk/includes/payment-methods</h2>
+ <ul>
+ <li><a href="../">..</a></li>
+ <li><a href="class-wc-gateway-stripe-alipay.php">class-wc-gateway-stripe-alipay.php</a></li>
+ <li><a href="class-wc-gateway-stripe-bancontact.php">class-wc-gateway-stripe-bancontact.php</a></li>
+ <li><a href="class-wc-gateway-stripe-boleto.php">class-wc-gateway-stripe-boleto.php</a></li>
+ <li><a href="class-wc-gateway-stripe-eps.php">class-wc-gateway-stripe-eps.php</a></li>
+ <li><a href="class-wc-gateway-stripe-giropay.php">class-wc-gateway-stripe-giropay.php</a></li>
+ <li><a href="class-wc-gateway-stripe-ideal.php">class-wc-gateway-stripe-ideal.php</a></li>
+ <li><a href="class-wc-gateway-stripe-multibanco.php">class-wc-gateway-stripe-multibanco.php</a></li>
+ <li><a href="class-wc-gateway-stripe-oxxo.php">class-wc-gateway-stripe-oxxo.php</a></li>
+ <li><a href="class-wc-gateway-stripe-p24.php">class-wc-gateway-stripe-p24.php</a></li>
+ <li><a href="class-wc-gateway-stripe-sepa.php">class-wc-gateway-stripe-sepa.php</a></li>
+ <li><a href="class-wc-gateway-stripe-sofort.php">class-wc-gateway-stripe-sofort.php</a></li>
+ <li><a href="class-wc-stripe-payment-request.php">class-wc-stripe-payment-request.php</a></li>
+ <li><a href="class-wc-stripe-upe-payment-gateway.php">class-wc-stripe-upe-payment-gateway.php</a></li>
+ <li><a href="class-wc-stripe-upe-payment-method-bancontact.php">class-wc-stripe-upe-payment-method-bancontact.php</a></li>
+ <li><a href="class-wc-stripe-upe-payment-method-boleto.php">class-wc-stripe-upe-payment-method-boleto.php</a></li>
+ <li><a href="class-wc-stripe-upe-payment-method-cc.php">class-wc-stripe-upe-payment-method-cc.php</a></li>
+ <li><a href="class-wc-stripe-upe-payment-method-eps.php">class-wc-stripe-upe-payment-method-eps.php</a></li>
+ <li><a href="class-wc-stripe-upe-payment-method-giropay.php">class-wc-stripe-upe-payment-method-giropay.php</a></li>
+ <li><a href="class-wc-stripe-upe-payment-method-ideal.php">class-wc-stripe-upe-payment-method-ideal.php</a></li>
+ <li><a href="class-wc-stripe-upe-payment-method-link.php">class-wc-stripe-upe-payment-method-link.php</a></li>
+ <li><a href="class-wc-stripe-upe-payment-method-oxxo.php">class-wc-stripe-upe-payment-method-oxxo.php</a></li>
+ <li><a href="class-wc-stripe-upe-payment-method-p24.php">class-wc-stripe-upe-payment-method-p24.php</a></li>
+ <li><a href="class-wc-stripe-upe-payment-method-sepa.php">class-wc-stripe-upe-payment-method-sepa.php</a></li>
+ <li><a href="class-wc-stripe-upe-payment-method-sofort.php">class-wc-stripe-upe-payment-method-sofort.php</a></li>
+ <li><a href="class-wc-stripe-upe-payment-method.php">class-wc-stripe-upe-payment-method.php</a></li>
+ </ul>
+ <hr noshade><em>Powered by <a href="http://subversion.apache.org/">Apache Subversion</a> version 1.9.5 (r1770682).</em>
+</body></html>
\ No newline at end of file