Initial commit
diff --git a/includes/compat/class-wc-stripe-email-failed-authentication-retry.php b/includes/compat/class-wc-stripe-email-failed-authentication-retry.php
new file mode 100644
index 0000000..4920a3d
--- /dev/null
+++ b/includes/compat/class-wc-stripe-email-failed-authentication-retry.php
@@ -0,0 +1,132 @@
+<?php
+/**
+ * Admin email about payment retry failed due to authentication
+ *
+ * Email sent to admins when an attempt to automatically process a subscription renewal payment has failed
+ * with the `authentication_needed` error, and a retry rule has been applied to retry the payment in the future.
+ *
+ * @version     4.3.0
+ * @package     WooCommerce_Stripe/Classes/WC_Stripe_Email_Failed_Authentication_Retry
+ * @extends     WC_Email_Failed_Order
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * An email sent to the admin when payment fails to go through due to authentication_required error.
+ *
+ * @since 4.3.0
+ */
+class WC_Stripe_Email_Failed_Authentication_Retry extends WC_Email_Failed_Order {
+
+	/**
+	 * Constructor
+	 */
+	public function __construct() {
+		$this->id          = 'failed_authentication_requested';
+		$this->title       = __( 'Payment Authentication Requested Email', 'woocommerce-gateway-stripe' );
+		$this->description = __( 'Payment authentication requested emails are sent to chosen recipient(s) when an attempt to automatically process a subscription renewal payment fails because the transaction requires an SCA verification, the customer is requested to authenticate the payment, and a retry rule has been applied to notify the customer again within a certain time period.', 'woocommerce-gateway-stripe' );
+
+		$this->heading = __( 'Automatic renewal payment failed due to authentication required', 'woocommerce-gateway-stripe' );
+		$this->subject = __( '[{site_title}] Automatic payment failed for {order_number}. Customer asked to authenticate payment and will be notified again {retry_time}', 'woocommerce-gateway-stripe' );
+
+		$this->template_html  = 'emails/failed-renewal-authentication-requested.php';
+		$this->template_plain = 'emails/plain/failed-renewal-authentication-requested.php';
+		$this->template_base  = plugin_dir_path( WC_STRIPE_MAIN_FILE ) . 'templates/';
+
+		$this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) );
+
+		// We want all the parent's methods, with none of its properties, so call its parent's constructor, rather than my parent constructor.
+		WC_Email::__construct();
+	}
+
+	/**
+	 * Get the default e-mail subject.
+	 *
+	 * @return string
+	 */
+	public function get_default_subject() {
+		return $this->subject;
+	}
+
+	/**
+	 * Get the default e-mail heading.
+	 *
+	 * @return string
+	 */
+	public function get_default_heading() {
+		return $this->heading;
+	}
+
+	/**
+	 * Trigger.
+	 *
+	 * @param int           $order_id The order ID.
+	 * @param WC_Order|null $order Order object.
+	 */
+	public function trigger( $order_id, $order = null ) {
+		$this->object = $order;
+
+		$this->find['retry-time'] = '{retry_time}';
+		if ( class_exists( 'WCS_Retry_Manager' ) && function_exists( 'wcs_get_human_time_diff' ) ) {
+			$this->retry                 = WCS_Retry_Manager::store()->get_last_retry_for_order( wcs_get_objects_property( $order, 'id' ) );
+			$this->replace['retry-time'] = wcs_get_human_time_diff( $this->retry->get_time() );
+		} else {
+			WC_Stripe_Logger::log( 'WCS_Retry_Manager class or does not exist. Not able to send admnin email about customer notification for authentication required for renewal payment.' );
+			return;
+		}
+
+		$this->find['order-number']    = '{order_number}';
+		$this->replace['order-number'] = $this->object->get_order_number();
+
+		if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
+			return;
+		}
+
+		$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
+	}
+
+	/**
+	 * Get content html.
+	 *
+	 * @return string
+	 */
+	public function get_content_html() {
+		return wc_get_template_html(
+			$this->template_html,
+			[
+				'order'         => $this->object,
+				'retry'         => $this->retry,
+				'email_heading' => $this->get_heading(),
+				'sent_to_admin' => true,
+				'plain_text'    => false,
+				'email'         => $this,
+			],
+			'',
+			$this->template_base
+		);
+	}
+
+	/**
+	 * Get content plain.
+	 *
+	 * @return string
+	 */
+	public function get_content_plain() {
+		return wc_get_template_html(
+			$this->template_plain,
+			[
+				'order'         => $this->object,
+				'retry'         => $this->retry,
+				'email_heading' => $this->get_heading(),
+				'sent_to_admin' => true,
+				'plain_text'    => true,
+				'email'         => $this,
+			],
+			'',
+			$this->template_base
+		);
+	}
+}
diff --git a/includes/compat/class-wc-stripe-email-failed-authentication.php b/includes/compat/class-wc-stripe-email-failed-authentication.php
new file mode 100644
index 0000000..01af464
--- /dev/null
+++ b/includes/compat/class-wc-stripe-email-failed-authentication.php
@@ -0,0 +1,126 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit; // Exit if accessed directly.
+}
+
+/**
+ * Base for Failed Renewal/Pre-Order Authentication Notifications.
+ *
+ * @extends WC_Email
+ */
+abstract class WC_Stripe_Email_Failed_Authentication extends WC_Email {
+	/**
+	 * An instance of the email, which would normally be sent after a failed payment.
+	 *
+	 * @var WC_Email
+	 */
+	public $original_email;
+
+	/**
+	 * Generates the HTML for the email while keeping the `template_base` in mind.
+	 *
+	 * @return string
+	 */
+	public function get_content_html() {
+		ob_start();
+		wc_get_template(
+			$this->template_html,
+			[
+				'order'             => $this->object,
+				'email_heading'     => $this->get_heading(),
+				'sent_to_admin'     => false,
+				'plain_text'        => false,
+				'authorization_url' => $this->get_authorization_url( $this->object ),
+				'email'             => $this,
+			],
+			'',
+			$this->template_base
+		);
+		return ob_get_clean();
+	}
+
+	/**
+	 * Generates the plain text for the email while keeping the `template_base` in mind.
+	 *
+	 * @return string
+	 */
+	public function get_content_plain() {
+		ob_start();
+		wc_get_template(
+			$this->template_plain,
+			[
+				'order'             => $this->object,
+				'email_heading'     => $this->get_heading(),
+				'sent_to_admin'     => false,
+				'plain_text'        => true,
+				'authorization_url' => $this->get_authorization_url( $this->object ),
+				'email'             => $this,
+			],
+			'',
+			$this->template_base
+		);
+		return ob_get_clean();
+	}
+
+	/**
+	 * Generates the URL, which will be used to authenticate the payment.
+	 *
+	 * @param WC_Order $order The order whose payment needs authentication.
+	 * @return string
+	 */
+	public function get_authorization_url( $order ) {
+		return add_query_arg( 'wc-stripe-confirmation', 1, $order->get_checkout_payment_url( false ) );
+	}
+
+	/**
+	 * Uses specific fields from `WC_Email_Customer_Invoice` for this email.
+	 */
+	public function init_form_fields() {
+		parent::init_form_fields();
+		$base_fields = $this->form_fields;
+
+		$this->form_fields = [
+			'enabled'    => [
+				'title'   => _x( 'Enable/Disable', 'an email notification', 'woocommerce-gateway-stripe' ),
+				'type'    => 'checkbox',
+				'label'   => __( 'Enable this email notification', 'woocommerce-gateway-stripe' ),
+				'default' => 'yes',
+			],
+
+			'subject'    => $base_fields['subject'],
+			'heading'    => $base_fields['heading'],
+			'email_type' => $base_fields['email_type'],
+		];
+	}
+
+	/**
+	 * Triggers the email.
+	 *
+	 * @param WC_Order $order The renewal order whose payment failed.
+	 */
+	public function trigger( $order ) {
+		if ( ! $this->is_enabled() ) {
+			return;
+		}
+
+		$this->object = $order;
+
+		if ( method_exists( $order, 'get_billing_email' ) ) {
+			$this->recipient = $order->get_billing_email();
+		} else {
+			$this->recipient = $order->billing_email;
+		}
+
+		$this->find['order_date'] = '{order_date}';
+		if ( function_exists( 'wc_format_datetime' ) ) { // WC 3.0+
+			$this->replace['order_date'] = wc_format_datetime( $order->get_date_created() );
+		} else { // WC < 3.0
+			$this->replace['order_date'] = $order->date_created->date_i18n( wc_date_format() );
+		}
+
+		$this->find['order_number']    = '{order_number}';
+		$this->replace['order_number'] = $order->get_order_number();
+
+		$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
+	}
+}
diff --git a/includes/compat/class-wc-stripe-email-failed-preorder-authentication.php b/includes/compat/class-wc-stripe-email-failed-preorder-authentication.php
new file mode 100644
index 0000000..09c712d
--- /dev/null
+++ b/includes/compat/class-wc-stripe-email-failed-preorder-authentication.php
@@ -0,0 +1,104 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit; // Exit if accessed directly.
+}
+
+/**
+ * Failed Renewal/Pre-Order Authentication Notification
+ *
+ * @extends WC_Stripe_Email_Failed_Authentication
+ */
+class WC_Stripe_Email_Failed_Preorder_Authentication extends WC_Stripe_Email_Failed_Authentication {
+	/**
+	 * Holds the message, which is entered by admins when sending the email.
+	 *
+	 * @var string
+	 */
+	protected $custom_message;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param WC_Email[] $email_classes All existing instances of WooCommerce emails.
+	 */
+	public function __construct( $email_classes = [] ) {
+		$this->id             = 'failed_preorder_sca_authentication';
+		$this->title          = __( 'Pre-order Payment Action Needed', 'woocommerce-gateway-stripe' );
+		$this->description    = __( 'This is an order notification sent to the customer once a pre-order is complete, but additional payment steps are required.', 'woocommerce-gateway-stripe' );
+		$this->customer_email = true;
+
+		$this->template_html  = 'emails/failed-preorder-authentication.php';
+		$this->template_plain = 'emails/plain/failed-preorder-authentication.php';
+		$this->template_base  = plugin_dir_path( WC_STRIPE_MAIN_FILE ) . 'templates/';
+
+		// Use the "authentication required" hook to add the correct, later hook.
+		add_action( 'wc_gateway_stripe_process_payment_authentication_required', [ $this, 'trigger' ] );
+
+		if ( isset( $email_classes['WC_Pre_Orders_Email_Pre_Order_Available'] ) ) {
+			$this->original_email = $email_classes['WC_Pre_Orders_Email_Pre_Order_Available'];
+		}
+
+		// We want all the parent's methods, with none of its properties, so call its parent's constructor, rather than my parent constructor.
+		parent::__construct();
+	}
+
+	/**
+	 * When autnentication is required, this adds another action to `wc_pre_orders_pre_order_completed`
+	 * in order to send the authentication required email when the custom pre-orders message is available.
+	 *
+	 * @param WC_Order $order The order whose payment is failing.
+	 */
+	public function trigger( $order ) {
+		if ( class_exists( 'WC_Pre_Orders_Order' ) && WC_Pre_Orders_Order::order_contains_pre_order( $order->get_id() ) ) {
+			if ( isset( $this->original_email ) ) {
+				remove_action( 'wc_pre_order_status_completed_notification', [ $this->original_email, 'trigger' ], 10, 2 );
+			}
+
+			add_action( 'wc_pre_orders_pre_order_completed', [ $this, 'send_email' ], 10, 2 );
+		}
+	}
+
+	/**
+	 * Triggers the email while also disconnecting the original Pre-Orders email.
+	 *
+	 * @param WC_Order $order The order that is being paid.
+	 * @param string   $message The message, which should be added to the email.
+	 */
+	public function send_email( $order, $message ) {
+		$this->custom_message = $message;
+
+		parent::trigger( $order );
+
+		// Restore the action of the original email for other bulk actions.
+		if ( isset( $this->original_email ) ) {
+			add_action( 'wc_pre_order_status_completed_notification', [ $this->original_email, 'trigger' ], 10, 2 );
+		}
+	}
+
+	/**
+	 * Returns the default subject of the email (modifyable in settings).
+	 *
+	 * @return string
+	 */
+	public function get_default_subject() {
+		return __( 'Payment authorization needed for pre-order {order_number}', 'woocommerce-gateway-stripe' );
+	}
+
+	/**
+	 * Returns the default heading of the email (modifyable in settings).
+	 *
+	 * @return string
+	 */
+	public function get_default_heading() {
+		return __( 'Payment authorization needed for pre-order {order_number}', 'woocommerce-gateway-stripe' );
+	}
+
+	/**
+	 * Returns the custom message, entered by the admin.
+	 *
+	 * @return string
+	 */
+	public function get_custom_message() {
+		return $this->custom_message;
+	}
+}
diff --git a/includes/compat/class-wc-stripe-email-failed-renewal-authentication.php b/includes/compat/class-wc-stripe-email-failed-renewal-authentication.php
new file mode 100644
index 0000000..64c9f15
--- /dev/null
+++ b/includes/compat/class-wc-stripe-email-failed-renewal-authentication.php
@@ -0,0 +1,113 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit; // Exit if accessed directly.
+}
+
+/**
+ * Failed Renewal/Pre-Order Authentication Notification
+ *
+ * @extends WC_Email_Customer_Invoice
+ */
+class WC_Stripe_Email_Failed_Renewal_Authentication extends WC_Stripe_Email_Failed_Authentication {
+	/**
+	 * Constructor.
+	 *
+	 * @param WC_Email[] $email_classes All existing instances of WooCommerce emails.
+	 */
+	public function __construct( $email_classes = [] ) {
+		$this->id             = 'failed_renewal_authentication';
+		$this->title          = __( 'Failed Subscription Renewal SCA Authentication', 'woocommerce-gateway-stripe' );
+		$this->description    = __( 'Sent to a customer when a renewal fails because the transaction requires an SCA verification. The email contains renewal order information and payment links.', 'woocommerce-gateway-stripe' );
+		$this->customer_email = true;
+
+		$this->template_html  = 'emails/failed-renewal-authentication.php';
+		$this->template_plain = 'emails/plain/failed-renewal-authentication.php';
+		$this->template_base  = plugin_dir_path( WC_STRIPE_MAIN_FILE ) . 'templates/';
+
+		// Triggers the email at the correct hook.
+		add_action( 'wc_gateway_stripe_process_payment_authentication_required', [ $this, 'trigger' ] );
+
+		if ( isset( $email_classes['WCS_Email_Customer_Renewal_Invoice'] ) ) {
+			$this->original_email = $email_classes['WCS_Email_Customer_Renewal_Invoice'];
+		}
+
+		// We want all the parent's methods, with none of its properties, so call its parent's constructor, rather than my parent constructor.
+		parent::__construct();
+	}
+
+	/**
+	 * Triggers the email while also disconnecting the original Subscriptions email.
+	 *
+	 * @param WC_Order $order The order that is being paid.
+	 */
+	public function trigger( $order ) {
+		if ( function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order->get_id() ) || wcs_is_subscription( $order->get_id() ) || wcs_order_contains_renewal( $order->get_id() ) ) ) {
+			parent::trigger( $order );
+
+			// Prevent the renewal email from WooCommerce Subscriptions from being sent.
+			if ( isset( $this->original_email ) ) {
+				remove_action( 'woocommerce_generated_manual_renewal_order_renewal_notification', [ $this->original_email, 'trigger' ] );
+				remove_action( 'woocommerce_order_status_failed_renewal_notification', [ $this->original_email, 'trigger' ] );
+			}
+
+			// Prevent the retry email from WooCommerce Subscriptions from being sent.
+			add_filter( 'wcs_get_retry_rule_raw', [ $this, 'prevent_retry_notification_email' ], 100, 3 );
+
+			// Send email to store owner indicating communication is happening with the customer to request authentication.
+			add_filter( 'wcs_get_retry_rule_raw', [ $this, 'set_store_owner_custom_email' ], 100, 3 );
+		}
+	}
+
+	/**
+	 * Returns the default subject of the email (modifyable in settings).
+	 *
+	 * @return string
+	 */
+	public function get_default_subject() {
+		return __( 'Payment authorization needed for renewal of {site_title} order {order_number}', 'woocommerce-gateway-stripe' );
+	}
+
+	/**
+	 * Returns the default heading of the email (modifyable in settings).
+	 *
+	 * @return string
+	 */
+	public function get_default_heading() {
+		return __( 'Payment authorization needed for renewal of order {order_number}', 'woocommerce-gateway-stripe' );
+	}
+
+	/**
+	 * Prevent all customer-facing retry notifications from being sent after this email.
+	 *
+	 * @param array $rule_array   The raw details about the retry rule.
+	 * @param int   $retry_number The number of the retry.
+	 * @param int   $order_id     The ID of the order that needs payment.
+	 * @return array
+	 */
+	public function prevent_retry_notification_email( $rule_array, $retry_number, $order_id ) {
+		if ( wcs_get_objects_property( $this->object, 'id' ) === $order_id ) {
+			$rule_array['email_template_customer'] = '';
+		}
+
+		return $rule_array;
+	}
+
+	/**
+	 * Send store owner a different email when the retry is related to an authentication required error.
+	 *
+	 * @param array $rule_array   The raw details about the retry rule.
+	 * @param int   $retry_number The number of the retry.
+	 * @param int   $order_id     The ID of the order that needs payment.
+	 * @return array
+	 */
+	public function set_store_owner_custom_email( $rule_array, $retry_number, $order_id ) {
+		if (
+			wcs_get_objects_property( $this->object, 'id' ) === $order_id &&
+			'' !== $rule_array['email_template_admin'] // Only send our email if a retry admin email was already going to be sent.
+		) {
+			$rule_array['email_template_admin'] = 'WC_Stripe_Email_Failed_Authentication_Retry';
+		}
+
+		return $rule_array;
+	}
+}
diff --git a/includes/compat/class-wc-stripe-woo-compat-utils.php b/includes/compat/class-wc-stripe-woo-compat-utils.php
new file mode 100644
index 0000000..6fdb4d1
--- /dev/null
+++ b/includes/compat/class-wc-stripe-woo-compat-utils.php
@@ -0,0 +1,41 @@
+<?php
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit; // Exit if accessed directly.
+}
+
+use Automattic\WooCommerce\Admin\Notes\Note;
+use Automattic\WooCommerce\Admin\Notes\Notes;
+use Automattic\WooCommerce\Admin\Notes\WC_Admin_Note;
+use Automattic\WooCommerce\Admin\Notes\WC_Admin_Notes;
+
+/**
+ * Util class for handling compatibilities with different versions of WooCommerce core.
+ */
+class WC_Stripe_Woo_Compat_Utils {
+	/**
+	 * Return non-deprecated class for instantiating WC-Admin notes.
+	 *
+	 * @return string
+	 */
+	public static function get_note_class() {
+		if ( class_exists( 'Automattic\WooCommerce\Admin\Notes\Note' ) ) {
+			return Note::class;
+		}
+
+		return WC_Admin_Note::class;
+	}
+
+	/**
+	 * Return non-deprecated class for instantiating WC-Admin notes.
+	 *
+	 * @return string
+	 */
+	public static function get_notes_class() {
+		if ( class_exists( 'Automattic\WooCommerce\Admin\Notes\Notes' ) ) {
+			return Notes::class;
+		}
+
+		return WC_Admin_Notes::class;
+	}
+}
diff --git a/includes/compat/index.html b/includes/compat/index.html
new file mode 100644
index 0000000..d0153d4
--- /dev/null
+++ b/includes/compat/index.html
@@ -0,0 +1,16 @@
+<html><head><title> - Revision 2844313: /woocommerce-gateway-stripe/trunk/includes/compat</title></head>
+<body>
+ <h2> - Revision 2844313: /woocommerce-gateway-stripe/trunk/includes/compat</h2>
+ <ul>
+  <li><a href="../">..</a></li>
+  <li><a href="class-wc-stripe-email-failed-authentication-retry.php">class-wc-stripe-email-failed-authentication-retry.php</a></li>
+  <li><a href="class-wc-stripe-email-failed-authentication.php">class-wc-stripe-email-failed-authentication.php</a></li>
+  <li><a href="class-wc-stripe-email-failed-preorder-authentication.php">class-wc-stripe-email-failed-preorder-authentication.php</a></li>
+  <li><a href="class-wc-stripe-email-failed-renewal-authentication.php">class-wc-stripe-email-failed-renewal-authentication.php</a></li>
+  <li><a href="class-wc-stripe-woo-compat-utils.php">class-wc-stripe-woo-compat-utils.php</a></li>
+  <li><a href="trait-wc-stripe-pre-orders.php">trait-wc-stripe-pre-orders.php</a></li>
+  <li><a href="trait-wc-stripe-subscriptions-utilities.php">trait-wc-stripe-subscriptions-utilities.php</a></li>
+  <li><a href="trait-wc-stripe-subscriptions.php">trait-wc-stripe-subscriptions.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
diff --git a/includes/compat/trait-wc-stripe-pre-orders.php b/includes/compat/trait-wc-stripe-pre-orders.php
new file mode 100644
index 0000000..b3a8a6a
--- /dev/null
+++ b/includes/compat/trait-wc-stripe-pre-orders.php
@@ -0,0 +1,268 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * Trait for Pre-Orders compatibility.
+ */
+trait WC_Stripe_Pre_Orders_Trait {
+
+	/**
+	 * Initialize pre-orders hook.
+	 *
+	 * @since 5.8.0
+	 */
+	public function maybe_init_pre_orders() {
+		if ( ! $this->is_pre_orders_enabled() ) {
+			return;
+		}
+
+		$this->supports[] = 'pre-orders';
+
+		add_action( 'wc_pre_orders_process_pre_order_completion_payment_' . $this->id, [ $this, 'process_pre_order_release_payment' ] );
+	}
+
+	/**
+	 * Checks if pre-orders are enabled on the site.
+	 *
+	 * @since 5.8.0
+	 *
+	 * @return bool
+	 */
+	public function is_pre_orders_enabled() {
+		return class_exists( 'WC_Pre_Orders' );
+	}
+
+	/**
+	 * Is $order_id a pre-order?
+	 *
+	 * @since 5.8.0
+	 *
+	 * @param  int $order_id
+	 * @return bool
+	 */
+	public function has_pre_order( $order_id ) {
+		return $this->is_pre_orders_enabled() && class_exists( 'WC_Pre_Orders_Order' ) && WC_Pre_Orders_Order::order_contains_pre_order( $order_id );
+	}
+
+	/**
+	 * Returns boolean on whether current cart contains a pre-order item.
+	 *
+	 * @since 5.8.0
+	 *
+	 * @return bool
+	 */
+	public function is_pre_order_item_in_cart() {
+		return $this->is_pre_orders_enabled() && class_exists( 'WC_Pre_Orders_Cart' ) && WC_Pre_Orders_Cart::cart_contains_pre_order();
+	}
+
+	/**
+	 * Returns pre-order product from cart.
+	 *
+	 * @since 5.8.0
+	 *
+	 * @return object|null
+	 */
+	public function get_pre_order_product_from_cart() {
+		if ( ! $this->is_pre_orders_enabled() || ! class_exists( 'WC_Pre_Orders_Cart' ) ) {
+			return false;
+		}
+		return WC_Pre_Orders_Cart::get_pre_order_product();
+	}
+
+	/**
+	 * Returns pre-order product from order.
+	 *
+	 * @since 5.8.0
+	 *
+	 * @param int $order_id
+	 *
+	 * @return object|null
+	 */
+	public function get_pre_order_product_from_order( $order_id ) {
+		if ( ! $this->is_pre_orders_enabled() || ! class_exists( 'WC_Pre_Orders_Order' ) ) {
+			return false;
+		}
+		return WC_Pre_Orders_Order::get_pre_order_product( $order_id );
+	}
+
+	/**
+	 * Returns boolean on whether product is charged upon release.
+	 *
+	 * @since 5.8.0
+	 *
+	 * @param object $product
+	 *
+	 * @return bool
+	 */
+	public function is_pre_order_product_charged_upon_release( $product ) {
+		return $this->is_pre_orders_enabled() && class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( $product );
+	}
+
+	/**
+	 * Returns boolean on whether product is charged upfront.
+	 *
+	 * @since 5.8.0
+	 *
+	 * @param object $product
+	 *
+	 * @return bool
+	 */
+	public function is_pre_order_product_charged_upfront( $product ) {
+		return $this->is_pre_orders_enabled() && class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upfront( $product );
+	}
+
+	/**
+	 * Checks if we need to process pre-orders when
+	 * a pre-order product is in the cart.
+	 *
+	 * @since 5.8.0
+	 *
+	 * @param int $order_id
+	 *
+	 * @return bool
+	 */
+	public function maybe_process_pre_orders( $order_id ) {
+		return (
+			$this->has_pre_order( $order_id ) &&
+			WC_Pre_Orders_Order::order_requires_payment_tokenization( $order_id ) &&
+			! is_wc_endpoint_url( 'order-pay' )
+		);
+	}
+
+	/**
+	 * Remove order meta.
+	 *
+	 * @param object $order
+	 */
+	public function remove_order_source_before_retry( $order ) {
+		$order->delete_meta_data( '_stripe_source_id' );
+		$order->delete_meta_data( '_stripe_card_id' );
+		$order->save();
+	}
+
+	/**
+	 * Marks the order as pre-ordered.
+	 * The native function is wrapped so we can call it separately and more easily mock it in our tests.
+	 *
+	 * @param object $order
+	 */
+	public function mark_order_as_pre_ordered( $order ) {
+		if ( ! class_exists( 'WC_Pre_Orders_Order' ) ) {
+			return;
+		}
+		WC_Pre_Orders_Order::mark_order_as_pre_ordered( $order );
+	}
+
+	/**
+	 * Process the pre-order when pay upon release is used.
+	 *
+	 * @param int $order_id
+	 *
+	 * @return array
+	 */
+	public function process_pre_order( $order_id ) {
+		try {
+			$order = wc_get_order( $order_id );
+
+			// This will throw exception if not valid.
+			$this->validate_minimum_order_amount( $order );
+
+			$prepared_source = $this->prepare_source( get_current_user_id(), true );
+
+			// We need a source on file to continue.
+			if ( empty( $prepared_source->customer ) || empty( $prepared_source->source ) ) {
+				throw new WC_Stripe_Exception( __( 'Unable to store payment details. Please try again.', 'woocommerce-gateway-stripe' ) );
+			}
+
+			// Setup the response early to allow later modifications.
+			$response = [
+				'result'   => 'success',
+				'redirect' => $this->get_return_url( $order ),
+			];
+
+			$this->save_source_to_order( $order, $prepared_source );
+
+			// Try setting up a payment intent.
+			$intent_secret = $this->setup_intent( $order, $prepared_source );
+			if ( ! empty( $intent_secret ) ) {
+				$response['setup_intent_secret'] = $intent_secret;
+				return $response;
+			}
+
+			// Remove cart.
+			WC()->cart->empty_cart();
+
+			// Is pre ordered!
+			$this->mark_order_as_pre_ordered( $order );
+
+			// Return thank you page redirect
+			return $response;
+		} catch ( WC_Stripe_Exception $e ) {
+			wc_add_notice( $e->getLocalizedMessage(), 'error' );
+			WC_Stripe_Logger::log( 'Pre Orders Error: ' . $e->getMessage() );
+
+			return [
+				'result'   => 'success',
+				'redirect' => $order->get_checkout_payment_url( true ),
+			];
+		}
+	}
+
+	/**
+	 * Process a pre-order payment when the pre-order is released.
+	 *
+	 * @param WC_Order $order
+	 * @param bool     $retry
+	 *
+	 * @return void
+	 */
+	public function process_pre_order_release_payment( $order, $retry = true ) {
+		try {
+			$source   = $this->prepare_order_source( $order );
+			$response = $this->create_and_confirm_intent_for_off_session( $order, $source );
+
+			$is_authentication_required = $this->is_authentication_required_for_payment( $response );
+
+			if ( ! empty( $response->error ) && ! $is_authentication_required ) {
+				if ( ! $retry ) {
+					throw new Exception( $response->error->message );
+				}
+				$this->remove_order_source_before_retry( $order );
+				$this->process_pre_order_release_payment( $order, false );
+			} elseif ( $is_authentication_required ) {
+				$charge = end( $response->error->payment_intent->charges->data );
+				$id     = $charge->id;
+
+				$order->set_transaction_id( $id );
+				/* translators: %s is the charge Id */
+				$order->update_status( 'failed', sprintf( __( 'Stripe charge awaiting authentication by user: %s.', 'woocommerce-gateway-stripe' ), $id ) );
+				if ( is_callable( [ $order, 'save' ] ) ) {
+					$order->save();
+				}
+
+				WC_Emails::instance();
+
+				do_action( 'wc_gateway_stripe_process_payment_authentication_required', $order );
+
+				throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
+			} else {
+				// Successful
+				$this->process_response( end( $response->charges->data ), $order );
+			}
+		} catch ( Exception $e ) {
+			$error_message = is_callable( [ $e, 'getLocalizedMessage' ] ) ? $e->getLocalizedMessage() : $e->getMessage();
+			/* translators: error message */
+			$order_note = sprintf( __( 'Stripe Transaction Failed (%s)', 'woocommerce-gateway-stripe' ), $error_message );
+
+			// Mark order as failed if not already set,
+			// otherwise, make sure we add the order note so we can detect when someone fails to check out multiple times
+			if ( ! $order->has_status( 'failed' ) ) {
+				$order->update_status( 'failed', $order_note );
+			} else {
+				$order->add_order_note( $order_note );
+			}
+		}
+	}
+}
diff --git a/includes/compat/trait-wc-stripe-subscriptions-utilities.php b/includes/compat/trait-wc-stripe-subscriptions-utilities.php
new file mode 100644
index 0000000..dded80f
--- /dev/null
+++ b/includes/compat/trait-wc-stripe-subscriptions-utilities.php
@@ -0,0 +1,118 @@
+<?php
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit; // Exit if accessed directly.
+}
+
+/**
+ * Trait for Subscriptions utility functions.
+ *
+ * @since 5.6.0
+ */
+trait WC_Stripe_Subscriptions_Utilities_Trait {
+
+	/**
+	 * Checks if subscriptions are enabled on the site.
+	 *
+	 * @since 5.6.0
+	 *
+	 * @return bool Whether subscriptions is enabled or not.
+	 */
+	public function is_subscriptions_enabled() {
+		return class_exists( 'WC_Subscriptions' ) && version_compare( WC_Subscriptions::$version, '2.2.0', '>=' );
+	}
+
+	/**
+	 * Is $order_id a subscription?
+	 *
+	 * @since 5.6.0
+	 *
+	 * @param  int $order_id
+	 * @return boolean
+	 */
+	public function has_subscription( $order_id ) {
+		return ( function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order_id ) || wcs_is_subscription( $order_id ) || wcs_order_contains_renewal( $order_id ) ) );
+	}
+
+	/**
+	 * Returns whether this user is changing the payment method for a subscription.
+	 *
+	 * @since 5.6.0
+	 *
+	 * @return bool
+	 */
+	public function is_changing_payment_method_for_subscription() {
+		if ( isset( $_GET['change_payment_method'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
+			return wcs_is_subscription( wc_clean( wp_unslash( $_GET['change_payment_method'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification
+		}
+		return false;
+	}
+
+	/**
+	 * Returns boolean value indicating whether payment for an order will be recurring,
+	 * as opposed to single.
+	 *
+	 * @since 5.6.0
+	 *
+	 * @param int $order_id ID for corresponding WC_Order in process.
+	 *
+	 * @return bool
+	 */
+	public function is_payment_recurring( $order_id ) {
+		if ( ! $this->is_subscriptions_enabled() ) {
+			return false;
+		}
+		return $this->is_changing_payment_method_for_subscription() || $this->has_subscription( $order_id );
+	}
+
+	/**
+	 * Returns a boolean value indicating whether the save payment checkbox should be
+	 * displayed during checkout.
+	 *
+	 * Returns `false` if the cart currently has a subscriptions or if the request has a
+	 * `change_payment_method` GET parameter. Returns the value in `$display` otherwise.
+	 *
+	 * @since 5.6.0
+	 *
+	 * @param bool $display Bool indicating whether to show the save payment checkbox in the absence of subscriptions.
+	 *
+	 * @return bool Indicates whether the save payment method checkbox should be displayed or not.
+	 */
+	public function display_save_payment_method_checkbox( $display ) {
+		if ( WC_Subscriptions_Cart::cart_contains_subscription() || $this->is_changing_payment_method_for_subscription() ) {
+			return false;
+		}
+		// Only render the "Save payment method" checkbox if there are no subscription products in the cart.
+		return $display;
+	}
+
+	/**
+	 * Returns boolean on whether current WC_Cart or WC_Subscriptions_Cart
+	 * contains a subscription or subscription renewal item
+	 *
+	 * @since 5.6.0
+	 *
+	 * @return bool
+	 */
+	public function is_subscription_item_in_cart() {
+		if ( $this->is_subscriptions_enabled() ) {
+			return WC_Subscriptions_Cart::cart_contains_subscription() || $this->cart_contains_renewal();
+		}
+		return false;
+	}
+
+	/**
+	 * Checks the cart to see if it contains a subscription product renewal.
+	 *
+	 * @since 5.6.0
+	 *
+	 * @return mixed The cart item containing the renewal as an array, else false.
+	 */
+	public function cart_contains_renewal() {
+		if ( ! function_exists( 'wcs_cart_contains_renewal' ) ) {
+			return false;
+		}
+		return wcs_cart_contains_renewal();
+	}
+
+}
diff --git a/includes/compat/trait-wc-stripe-subscriptions.php b/includes/compat/trait-wc-stripe-subscriptions.php
new file mode 100644
index 0000000..586997e
--- /dev/null
+++ b/includes/compat/trait-wc-stripe-subscriptions.php
@@ -0,0 +1,711 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * Trait for Subscriptions compatibility.
+ */
+trait WC_Stripe_Subscriptions_Trait {
+
+	use WC_Stripe_Subscriptions_Utilities_Trait;
+
+	/**
+	 * Initialize subscription support and hooks.
+	 *
+	 * @since 5.6.0
+	 */
+	public function maybe_init_subscriptions() {
+		if ( ! $this->is_subscriptions_enabled() ) {
+			return;
+		}
+
+		$this->supports = array_merge(
+			$this->supports,
+			[
+				'subscriptions',
+				'subscription_cancellation',
+				'subscription_suspension',
+				'subscription_reactivation',
+				'subscription_amount_changes',
+				'subscription_date_changes',
+				'subscription_payment_method_change',
+				'subscription_payment_method_change_customer',
+				'subscription_payment_method_change_admin',
+				'multiple_subscriptions',
+			]
+		);
+
+		add_action( 'woocommerce_scheduled_subscription_payment_' . $this->id, [ $this, 'scheduled_subscription_payment' ], 10, 2 );
+		add_action( 'woocommerce_subscription_failing_payment_method_updated_' . $this->id, [ $this, 'update_failing_payment_method' ], 10, 2 );
+		add_action( 'wcs_resubscribe_order_created', [ $this, 'delete_resubscribe_meta' ], 10 );
+		add_action( 'wcs_renewal_order_created', [ $this, 'delete_renewal_meta' ], 10 );
+		add_action( 'wc_stripe_payment_fields_' . $this->id, [ $this, 'display_update_subs_payment_checkout' ] );
+		add_action( 'wc_stripe_add_payment_method_' . $this->id . '_success', [ $this, 'handle_add_payment_method_success' ], 10, 2 );
+		add_action( 'woocommerce_subscriptions_change_payment_before_submit', [ $this, 'differentiate_change_payment_method_form' ] );
+
+		// Display the payment method used for a subscription in the "My Subscriptions" table.
+		add_filter( 'woocommerce_my_subscriptions_payment_method', [ $this, 'maybe_render_subscription_payment_method' ], 10, 2 );
+
+		// Allow store managers to manually set Stripe as the payment method on a subscription.
+		add_filter( 'woocommerce_subscription_payment_meta', [ $this, 'add_subscription_payment_meta' ], 10, 2 );
+		add_filter( 'woocommerce_subscription_validate_payment_meta', [ $this, 'validate_subscription_payment_meta' ], 10, 2 );
+		add_filter( 'wc_stripe_display_save_payment_method_checkbox', [ $this, 'display_save_payment_method_checkbox' ] );
+
+		/*
+		* WC subscriptions hooks into the "template_redirect" hook with priority 100.
+		* If the screen is "Pay for order" and the order is a subscription renewal, it redirects to the plain checkout.
+		* See: https://github.com/woocommerce/woocommerce-subscriptions/blob/99a75687e109b64cbc07af6e5518458a6305f366/includes/class-wcs-cart-renewal.php#L165
+		* If we are in the "You just need to authorize SCA" flow, we don't want that redirection to happen.
+		*/
+		add_action( 'template_redirect', [ $this, 'remove_order_pay_var' ], 99 );
+		add_action( 'template_redirect', [ $this, 'restore_order_pay_var' ], 101 );
+	}
+
+	/**
+	 * Displays a checkbox to allow users to update all subs payments with new
+	 * payment.
+	 *
+	 * @since 4.1.11
+	 */
+	public function display_update_subs_payment_checkout() {
+		$subs_statuses = apply_filters( 'wc_stripe_update_subs_payment_method_card_statuses', [ 'active' ] );
+		if (
+			apply_filters( 'wc_stripe_display_update_subs_payment_method_card_checkbox', true ) &&
+			wcs_user_has_subscription( get_current_user_id(), '', $subs_statuses ) &&
+			is_add_payment_method_page()
+		) {
+			$label = esc_html( apply_filters( 'wc_stripe_save_to_subs_text', __( 'Update the Payment Method used for all of my active subscriptions.', 'woocommerce-gateway-stripe' ) ) );
+			$id    = sprintf( 'wc-%1$s-update-subs-payment-method-card', $this->id );
+			woocommerce_form_field(
+				$id,
+				[
+					'type'    => 'checkbox',
+					'label'   => $label,
+					'default' => apply_filters( 'wc_stripe_save_to_subs_checked', false ),
+				]
+			);
+		}
+	}
+
+	/**
+	 * Updates all active subscriptions payment method.
+	 *
+	 * @since 4.1.11
+	 *
+	 * @param string $source_id
+	 * @param object $source_object
+	 */
+	public function handle_add_payment_method_success( $source_id, $source_object ) {
+		if ( isset( $_POST[ 'wc-' . $this->id . '-update-subs-payment-method-card' ] ) ) {
+			$all_subs        = wcs_get_users_subscriptions();
+			$subs_statuses   = apply_filters( 'wc_stripe_update_subs_payment_method_card_statuses', [ 'active' ] );
+			$stripe_customer = new WC_Stripe_Customer( get_current_user_id() );
+
+			if ( ! empty( $all_subs ) ) {
+				foreach ( $all_subs as $sub ) {
+					if ( $sub->has_status( $subs_statuses ) ) {
+						WC_Subscriptions_Change_Payment_Gateway::update_payment_method(
+							$sub,
+							$this->id,
+							[
+								'post_meta' => [
+									'_stripe_source_id'   => [ 'value' => $source_id ],
+									'_stripe_customer_id' => [ 'value' => $stripe_customer->get_id() ],
+								],
+							]
+						);
+					}
+				}
+			}
+		}
+	}
+
+	/**
+	 * Render a dummy element in the "Change payment method" form (that does not appear in the "Pay for order" form)
+	 * which can be checked to determine proper SCA handling to apply for each form.
+	 *
+	 * @since 4.6.1
+	 */
+	public function differentiate_change_payment_method_form() {
+		echo '<input type="hidden" id="wc-stripe-change-payment-method" />';
+	}
+
+	/**
+	 * Maybe process payment method change for subscriptions.
+	 *
+	 * @since 5.6.0
+	 *
+	 * @param int $order_id
+	 * @return bool
+	 */
+	public function maybe_change_subscription_payment_method( $order_id ) {
+		return (
+			$this->is_subscriptions_enabled() &&
+			$this->has_subscription( $order_id ) &&
+			$this->is_changing_payment_method_for_subscription()
+		);
+	}
+
+	/**
+	 * Process the payment method change for subscriptions.
+	 *
+	 * @since 5.6.0
+	 *
+	 * @param int $order_id
+	 * @return array|null
+	 */
+	public function process_change_subscription_payment_method( $order_id ) {
+		try {
+			$subscription    = wc_get_order( $order_id );
+			$prepared_source = $this->prepare_source( get_current_user_id(), true );
+
+			$this->maybe_disallow_prepaid_card( $prepared_source->source_object );
+			$this->check_source( $prepared_source );
+			$this->save_source_to_order( $subscription, $prepared_source );
+
+			do_action( 'wc_stripe_change_subs_payment_method_success', $prepared_source->source, $prepared_source );
+
+			return [
+				'result'   => 'success',
+				'redirect' => $this->get_return_url( $subscription ),
+			];
+		} catch ( WC_Stripe_Exception $e ) {
+			wc_add_notice( $e->getLocalizedMessage(), 'error' );
+			WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
+		}
+	}
+
+	/**
+	 * Scheduled_subscription_payment function.
+	 *
+	 * @param $amount_to_charge float The amount to charge.
+	 * @param $renewal_order WC_Order A WC_Order object created to record the renewal payment.
+	 */
+	public function scheduled_subscription_payment( $amount_to_charge, $renewal_order ) {
+		$this->process_subscription_payment( $amount_to_charge, $renewal_order, true, false );
+	}
+
+	/**
+	 * Process_subscription_payment function.
+	 *
+	 * @since 3.0
+	 * @since 4.0.4 Add third parameter flag to retry.
+	 * @since 4.1.0 Add fourth parameter to log previous errors.
+	 * @since 5.6.0 Process renewal payments for SEPA and UPE.
+	 *
+	 * @param float  $amount
+	 * @param mixed  $renewal_order
+	 * @param bool   $retry Should we retry the process?
+	 * @param object $previous_error
+	 */
+	public function process_subscription_payment( $amount, $renewal_order, $retry = true, $previous_error = false ) {
+		try {
+			$order_id = $renewal_order->get_id();
+
+			// Unlike regular off-session subscription payments, early renewals are treated as on-session payments, involving the customer.
+			// This makes the SCA authorization popup show up for the "Renew early" modal (Subscriptions settings > Accept Early Renewal Payments via a Modal).
+			// Note: Currently available for non-UPE credit card only.
+			if ( isset( $_REQUEST['process_early_renewal'] ) && 'stripe' === $this->id && ! WC_Stripe_Feature_Flags::is_upe_checkout_enabled() ) { // phpcs:ignore WordPress.Security.NonceVerification
+				$response = $this->process_payment( $order_id, true, false, $previous_error, true );
+
+				if ( 'success' === $response['result'] && isset( $response['payment_intent_secret'] ) ) {
+					$verification_url = add_query_arg(
+						[
+							'order'         => $order_id,
+							'nonce'         => wp_create_nonce( 'wc_stripe_confirm_pi' ),
+							'redirect_to'   => esc_url_raw( remove_query_arg( [ 'process_early_renewal', 'subscription_id', 'wcs_nonce' ] ) ),
+							'early_renewal' => true,
+						],
+						WC_AJAX::get_endpoint( 'wc_stripe_verify_intent' )
+					);
+
+					echo wp_json_encode(
+						[
+							'stripe_sca_required' => true,
+							'intent_secret'       => $response['payment_intent_secret'],
+							'redirect_url'        => $verification_url,
+						]
+					);
+
+					exit;
+				}
+
+				// Hijack all other redirects in order to do the redirection in JavaScript.
+				add_action( 'wp_redirect', [ $this, 'redirect_after_early_renewal' ], 100 );
+
+				return;
+			}
+
+			// Check for an existing intent, which is associated with the order.
+			if ( $this->has_authentication_already_failed( $renewal_order ) ) {
+				return;
+			}
+
+			// Get source from order
+			$prepared_source = $this->prepare_order_source( $renewal_order );
+			$source_object   = $prepared_source->source_object;
+
+			if ( ! $prepared_source->customer ) {
+				throw new WC_Stripe_Exception(
+					'Failed to process renewal for order ' . $renewal_order->get_id() . '. Stripe customer id is missing in the order',
+					__( 'Customer not found', 'woocommerce-gateway-stripe' )
+				);
+			}
+
+			WC_Stripe_Logger::log( "Info: Begin processing subscription payment for order {$order_id} for the amount of {$amount}" );
+
+			/*
+			 * If we're doing a retry and source is chargeable, we need to pass
+			 * a different idempotency key and retry for success.
+			 */
+			if ( is_object( $source_object ) && empty( $source_object->error ) && $this->need_update_idempotency_key( $source_object, $previous_error ) ) {
+				add_filter( 'wc_stripe_idempotency_key', [ $this, 'change_idempotency_key' ], 10, 2 );
+			}
+
+			if ( ( $this->is_no_such_source_error( $previous_error ) || $this->is_no_linked_source_error( $previous_error ) ) && apply_filters( 'wc_stripe_use_default_customer_source', true ) ) {
+				// Passing empty source will charge customer default.
+				$prepared_source->source = '';
+			}
+
+			// If the payment gateway is SEPA, use the charges API.
+			// TODO: Remove when SEPA is migrated to payment intents.
+			if ( 'stripe_sepa' === $this->id ) {
+				$request            = $this->generate_payment_request( $renewal_order, $prepared_source );
+				$request['capture'] = 'true';
+				$request['amount']  = WC_Stripe_Helper::get_stripe_amount( $amount, $request['currency'] );
+				$response           = WC_Stripe_API::request( $request );
+
+				$is_authentication_required = false;
+			} else {
+				$this->lock_order_payment( $renewal_order );
+				$response                   = $this->create_and_confirm_intent_for_off_session( $renewal_order, $prepared_source, $amount );
+				$is_authentication_required = $this->is_authentication_required_for_payment( $response );
+			}
+
+			// It's only a failed payment if it's an error and it's not of the type 'authentication_required'.
+			// If it's 'authentication_required', then we should email the user and ask them to authenticate.
+			if ( ! empty( $response->error ) && ! $is_authentication_required ) {
+				// 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_subscription_payment( $amount, $renewal_order, false, $response->error );
+						}
+
+						sleep( $this->retry_interval );
+
+						$this->retry_interval++;
+
+						return $this->process_subscription_payment( $amount, $renewal_order, true, $response->error );
+					} else {
+						$localized_message = __( 'Sorry, we are unable to process your payment at this time. Please retry later.', 'woocommerce-gateway-stripe' );
+						$renewal_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;
+				}
+
+				$renewal_order->add_order_note( $localized_message );
+
+				throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
+			}
+
+			// Either the charge was successfully captured, or it requires further authentication.
+			if ( $is_authentication_required ) {
+				do_action( 'wc_gateway_stripe_process_payment_authentication_required', $renewal_order, $response );
+
+				$error_message = __( 'This transaction requires authentication.', 'woocommerce-gateway-stripe' );
+				$renewal_order->add_order_note( $error_message );
+
+				$charge   = end( $response->error->payment_intent->charges->data );
+				$id       = $charge->id;
+				$order_id = $renewal_order->get_id();
+
+				$renewal_order->set_transaction_id( $id );
+				/* translators: %s is the charge Id */
+				$renewal_order->update_status( 'failed', sprintf( __( 'Stripe charge awaiting authentication by user: %s.', 'woocommerce-gateway-stripe' ), $id ) );
+				if ( is_callable( [ $renewal_order, 'save' ] ) ) {
+					$renewal_order->save();
+				}
+			} else {
+				// The charge was successfully captured
+				do_action( 'wc_gateway_stripe_process_payment', $response, $renewal_order );
+
+				// Use the last charge within the intent or the full response body in case of SEPA.
+				$this->process_response( isset( $response->charges ) ? end( $response->charges->data ) : $response, $renewal_order );
+			}
+
+			// TODO: Remove when SEPA is migrated to payment intents.
+			if ( 'stripe_sepa' !== $this->id ) {
+				$this->unlock_order_payment( $renewal_order );
+			}
+		} catch ( WC_Stripe_Exception $e ) {
+			WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
+
+			do_action( 'wc_gateway_stripe_process_payment_error', $e, $renewal_order );
+
+			/* translators: error message */
+			$renewal_order->update_status( 'failed' );
+		}
+	}
+
+	/**
+	 * Updates other subscription sources.
+	 *
+	 * @since 5.6.0
+	 */
+	public function maybe_update_source_on_subscription_order( $order, $source ) {
+		if ( ! $this->is_subscriptions_enabled() ) {
+			return;
+		}
+
+		$order_id = $order->get_id();
+
+		// Also store it on the subscriptions being purchased or paid for in the order
+		if ( function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order_id ) ) {
+			$subscriptions = wcs_get_subscriptions_for_order( $order_id );
+		} elseif ( function_exists( 'wcs_order_contains_renewal' ) && wcs_order_contains_renewal( $order_id ) ) {
+			$subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id );
+		} else {
+			$subscriptions = [];
+		}
+
+		foreach ( $subscriptions as $subscription ) {
+			$subscription_id = $subscription->get_id();
+			update_post_meta( $subscription_id, '_stripe_customer_id', $source->customer );
+
+			if ( ! empty( $source->payment_method ) ) {
+				update_post_meta( $subscription_id, '_stripe_source_id', $source->payment_method );
+			} else {
+				update_post_meta( $subscription_id, '_stripe_source_id', $source->source );
+			}
+		}
+	}
+
+	/**
+	 * Don't transfer Stripe customer/token meta to resubscribe orders.
+	 *
+	 * @param int $resubscribe_order The order created for the customer to resubscribe to the old expired/cancelled subscription
+	 */
+	public function delete_resubscribe_meta( $resubscribe_order ) {
+		delete_post_meta( $resubscribe_order->get_id(), '_stripe_customer_id' );
+		delete_post_meta( $resubscribe_order->get_id(), '_stripe_source_id' );
+		// For BW compat will remove in future.
+		delete_post_meta( $resubscribe_order->get_id(), '_stripe_card_id' );
+		// Delete payment intent ID.
+		delete_post_meta( $resubscribe_order->get_id(), '_stripe_intent_id' );
+		$this->delete_renewal_meta( $resubscribe_order );
+	}
+
+	/**
+	 * Don't transfer Stripe fee/ID meta to renewal orders.
+	 *
+	 * @param int $resubscribe_order The order created for the customer to resubscribe to the old expired/cancelled subscription
+	 */
+	public function delete_renewal_meta( $renewal_order ) {
+		WC_Stripe_Helper::delete_stripe_fee( $renewal_order );
+		WC_Stripe_Helper::delete_stripe_net( $renewal_order );
+
+		// Delete payment intent ID.
+		delete_post_meta( $renewal_order->get_id(), '_stripe_intent_id' );
+
+		return $renewal_order;
+	}
+
+	/**
+	 * Update the customer_id for a subscription after using Stripe to complete a payment to make up for
+	 * an automatic renewal payment which previously failed.
+	 *
+	 * @param WC_Subscription $subscription The subscription for which the failing payment method relates.
+	 * @param WC_Order        $renewal_order The order which recorded the successful payment (to make up for the failed automatic payment).
+	 * @return void
+	 */
+	public function update_failing_payment_method( $subscription, $renewal_order ) {
+		update_post_meta( $subscription->get_id(), '_stripe_customer_id', $renewal_order->get_meta( '_stripe_customer_id', true ) );
+		update_post_meta( $subscription->get_id(), '_stripe_source_id', $renewal_order->get_meta( '_stripe_source_id', true ) );
+	}
+
+	/**
+	 * Include the payment meta data required to process automatic recurring payments so that store managers can
+	 * manually set up automatic recurring payments for a customer via the Edit Subscriptions screen in 2.0+.
+	 *
+	 * @since 2.5
+	 *
+	 * @param array           $payment_meta associative array of meta data required for automatic payments
+	 * @param WC_Subscription $subscription An instance of a subscription object
+	 * @return array
+	 */
+	public function add_subscription_payment_meta( $payment_meta, $subscription ) {
+		$subscription_id = $subscription->get_id();
+		$source_id       = get_post_meta( $subscription_id, '_stripe_source_id', true );
+
+		// For BW compat will remove in future.
+		if ( empty( $source_id ) ) {
+			$source_id = get_post_meta( $subscription_id, '_stripe_card_id', true );
+
+			// Take this opportunity to update the key name.
+			update_post_meta( $subscription_id, '_stripe_source_id', $source_id );
+			delete_post_meta( $subscription_id, '_stripe_card_id', $source_id );
+		}
+
+		$payment_meta[ $this->id ] = [
+			'post_meta' => [
+				'_stripe_customer_id' => [
+					'value' => get_post_meta( $subscription_id, '_stripe_customer_id', true ),
+					'label' => 'Stripe Customer ID',
+				],
+				'_stripe_source_id'   => [
+					'value' => $source_id,
+					'label' => 'Stripe Source ID',
+				],
+			],
+		];
+
+		return $payment_meta;
+	}
+
+	/**
+	 * Validate the payment meta data required to process automatic recurring payments so that store managers can
+	 * manually set up automatic recurring payments for a customer via the Edit Subscriptions screen in 2.0+.
+	 *
+	 * @since 2.5
+	 * @since 4.0.4 Stripe sourd id field no longer needs to be required.
+	 *
+	 * @param string $payment_method_id The ID of the payment method to validate
+	 * @param array  $payment_meta associative array of meta data required for automatic payments
+	 * @return array
+	 */
+	public function validate_subscription_payment_meta( $payment_method_id, $payment_meta ) {
+		if ( $this->id === $payment_method_id ) {
+
+			if ( ! isset( $payment_meta['post_meta']['_stripe_customer_id']['value'] ) || empty( $payment_meta['post_meta']['_stripe_customer_id']['value'] ) ) {
+
+				// Allow empty stripe customer id during subscription renewal. It will be added when processing payment if required.
+				if ( ! isset( $_POST['wc_order_action'] ) || 'wcs_process_renewal' !== $_POST['wc_order_action'] ) {
+					throw new Exception( __( 'A "Stripe Customer ID" value is required.', 'woocommerce-gateway-stripe' ) );
+				}
+			} elseif ( 0 !== strpos( $payment_meta['post_meta']['_stripe_customer_id']['value'], 'cus_' ) ) {
+				throw new Exception( __( 'Invalid customer ID. A valid "Stripe Customer ID" must begin with "cus_".', 'woocommerce-gateway-stripe' ) );
+			}
+
+			if (
+				! empty( $payment_meta['post_meta']['_stripe_source_id']['value'] ) && (
+					0 !== strpos( $payment_meta['post_meta']['_stripe_source_id']['value'], 'card_' )
+					&& 0 !== strpos( $payment_meta['post_meta']['_stripe_source_id']['value'], 'src_' )
+					&& 0 !== strpos( $payment_meta['post_meta']['_stripe_source_id']['value'], 'pm_' )
+				)
+			) {
+				throw new Exception( __( 'Invalid source ID. A valid source "Stripe Source ID" must begin with "src_", "pm_", or "card_".', 'woocommerce-gateway-stripe' ) );
+			}
+		}
+	}
+
+	/**
+	 * Render the payment method used for a subscription in the "My Subscriptions" table
+	 *
+	 * @since 1.7.5
+	 * @version 5.6.0
+	 *
+	 * @param string          $payment_method_to_display the default payment method text to display
+	 * @param WC_Subscription $subscription the subscription details
+	 * @return string the subscription payment method
+	 */
+	public function maybe_render_subscription_payment_method( $payment_method_to_display, $subscription ) {
+		$customer_user = $subscription->get_customer_id();
+
+		// bail for other payment methods
+		if ( $subscription->get_payment_method() !== $this->id || ! $customer_user ) {
+			return $payment_method_to_display;
+		}
+
+		$stripe_source_id = get_post_meta( $subscription->get_id(), '_stripe_source_id', true );
+
+		// For BW compat will remove in future.
+		if ( empty( $stripe_source_id ) ) {
+			$stripe_source_id = get_post_meta( $subscription->get_id(), '_stripe_card_id', true );
+
+			// Take this opportunity to update the key name.
+			update_post_meta( $subscription->get_id(), '_stripe_source_id', $stripe_source_id );
+		}
+
+		$stripe_customer    = new WC_Stripe_Customer();
+		$stripe_customer_id = get_post_meta( $subscription->get_id(), '_stripe_customer_id', true );
+
+		// If we couldn't find a Stripe customer linked to the subscription, fallback to the user meta data.
+		if ( ! $stripe_customer_id || ! is_string( $stripe_customer_id ) ) {
+			$user_id            = $customer_user;
+			$stripe_customer_id = get_user_option( '_stripe_customer_id', $user_id );
+			$stripe_source_id   = get_user_option( '_stripe_source_id', $user_id );
+
+			// For BW compat will remove in future.
+			if ( empty( $stripe_source_id ) ) {
+				$stripe_source_id = get_user_option( '_stripe_card_id', $user_id );
+
+				// Take this opportunity to update the key name.
+				update_user_option( $user_id, '_stripe_source_id', $stripe_source_id, false );
+			}
+		}
+
+		// If we couldn't find a Stripe customer linked to the account, fallback to the order meta data.
+		if ( ( ! $stripe_customer_id || ! is_string( $stripe_customer_id ) ) && false !== $subscription->get_parent() ) {
+			$stripe_customer_id = get_post_meta( $subscription->get_parent_id(), '_stripe_customer_id', true );
+			$stripe_source_id   = get_post_meta( $subscription->get_parent_id(), '_stripe_source_id', true );
+
+			// For BW compat will remove in future.
+			if ( empty( $stripe_source_id ) ) {
+				$stripe_source_id = get_post_meta( $subscription->get_parent_id(), '_stripe_card_id', true );
+
+				// Take this opportunity to update the key name.
+				update_post_meta( $subscription->get_parent_id(), '_stripe_source_id', $stripe_source_id );
+			}
+		}
+
+		$stripe_customer->set_id( $stripe_customer_id );
+
+		// Retrieve all possible payment methods for subscriptions.
+		$sources                   = array_merge(
+			$stripe_customer->get_payment_methods( 'card' ),
+			$stripe_customer->get_payment_methods( 'sepa_debit' )
+		);
+		$payment_method_to_display = __( 'N/A', 'woocommerce-gateway-stripe' );
+
+		if ( $sources ) {
+			foreach ( $sources as $source ) {
+				if ( $source->id === $stripe_source_id ) {
+					$card = false;
+					if ( isset( $source->type ) && 'card' === $source->type ) {
+						$card = $source->card;
+					} elseif ( isset( $source->object ) && 'card' === $source->object ) {
+						$card = $source;
+					}
+
+					if ( $card ) {
+						/* translators: 1) card brand 2) last 4 digits */
+						$payment_method_to_display = sprintf( __( 'Via %1$s card ending in %2$s', 'woocommerce-gateway-stripe' ), ( isset( $card->brand ) ? $card->brand : __( 'N/A', 'woocommerce-gateway-stripe' ) ), $card->last4 );
+					} elseif ( $source->sepa_debit ) {
+						/* translators: 1) last 4 digits of SEPA Direct Debit */
+						$payment_method_to_display = sprintf( __( 'Via SEPA Direct Debit ending in %1$s', 'woocommerce-gateway-stripe' ), $source->sepa_debit->last4 );
+					}
+
+					break;
+				}
+			}
+		}
+
+		return $payment_method_to_display;
+	}
+
+	/**
+	 * If this is the "Pass the SCA challenge" flow, remove a variable that is checked by WC Subscriptions
+	 * so WC Subscriptions doesn't redirect to the checkout
+	 */
+	public function remove_order_pay_var() {
+		global $wp;
+		if ( isset( $_GET['wc-stripe-confirmation'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
+			$this->order_pay_var         = $wp->query_vars['order-pay'];
+			$wp->query_vars['order-pay'] = null;
+		}
+	}
+
+	/**
+	 * Restore the variable that was removed in remove_order_pay_var()
+	 */
+	public function restore_order_pay_var() {
+		global $wp;
+		if ( isset( $this->order_pay_var ) ) {
+			$wp->query_vars['order-pay'] = $this->order_pay_var;
+		}
+	}
+
+	/**
+	 * Checks if a renewal already failed because a manual authentication is required.
+	 *
+	 * @param WC_Order $renewal_order The renewal order.
+	 * @return boolean
+	 */
+	public function has_authentication_already_failed( $renewal_order ) {
+		$existing_intent = $this->get_intent_from_order( $renewal_order );
+
+		if (
+			! $existing_intent
+			|| 'requires_payment_method' !== $existing_intent->status
+			|| empty( $existing_intent->last_payment_error )
+			|| 'authentication_required' !== $existing_intent->last_payment_error->code
+		) {
+			return false;
+		}
+
+		// Make sure all emails are instantiated.
+		WC_Emails::instance();
+
+		/**
+		 * A payment attempt failed because SCA authentication is required.
+		 *
+		 * @param WC_Order $renewal_order The order that is being renewed.
+		 */
+		do_action( 'wc_gateway_stripe_process_payment_authentication_required', $renewal_order );
+
+		// Fail the payment attempt (order would be currently pending because of retry rules).
+		$charge    = end( $existing_intent->charges->data );
+		$charge_id = $charge->id;
+		/* translators: %s is the stripe charge Id */
+		$renewal_order->update_status( 'failed', sprintf( __( 'Stripe charge awaiting authentication by user: %s.', 'woocommerce-gateway-stripe' ), $charge_id ) );
+
+		return true;
+	}
+
+	/**
+	 * Hijacks `wp_redirect` in order to generate a JS-friendly object with the URL.
+	 *
+	 * @param string $url The URL that Subscriptions attempts a redirect to.
+	 * @return void
+	 */
+	public function redirect_after_early_renewal( $url ) {
+		echo wp_json_encode(
+			[
+				'stripe_sca_required' => false,
+				'redirect_url'        => $url,
+			]
+		);
+
+		exit;
+	}
+
+	/**
+	 * Once an intent has been verified, perform some final actions for early renewals.
+	 *
+	 * @param WC_Order $order The renewal order.
+	 * @param stdClass $intent The Payment Intent object.
+	 */
+	protected function maybe_process_subscription_early_renewal_success( $order, $intent ) {
+		if ( $this->is_subscriptions_enabled() && isset( $_GET['early_renewal'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
+			wcs_update_dates_after_early_renewal( wcs_get_subscription( $order->get_meta( '_subscription_renewal' ) ), $order );
+			wc_add_notice( __( 'Your early renewal order was successful.', 'woocommerce-gateway-stripe' ), 'success' );
+		}
+	}
+
+	/**
+	 * During early renewals, instead of failing the renewal order, delete it and let Subs redirect to the checkout.
+	 *
+	 * @param WC_Order $order The renewal order.
+	 * @param stdClass $intent The Payment Intent object (unused).
+	 */
+	protected function maybe_process_subscription_early_renewal_failure( $order, $intent ) {
+		if ( $this->is_subscriptions_enabled() && isset( $_GET['early_renewal'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
+			$order->delete( true );
+			wc_add_notice( __( 'Payment authorization for the renewal order was unsuccessful, please try again.', 'woocommerce-gateway-stripe' ), 'error' );
+			$renewal_url = wcs_get_early_renewal_url( wcs_get_subscription( $order->get_meta( '_subscription_renewal' ) ) );
+			wp_safe_redirect( $renewal_url );
+			exit;
+		}
+	}
+}